-
-
Notifications
You must be signed in to change notification settings - Fork 2.3k
Initial support for PEP 695 type aliases #13508
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -54,6 +54,11 @@ | |
[Sphinx, _AutodocObjType, str, Any, dict[str, bool], list[str]], None | ||
] | ||
|
||
if sys.version_info[:2] < (3, 12): | ||
from typing_extensions import TypeAliasType | ||
else: | ||
from typing import TypeAliasType | ||
|
||
logger = logging.getLogger(__name__) | ||
|
||
|
||
|
@@ -1690,11 +1695,13 @@ def __init__(self, *args: Any) -> None: | |
|
||
@classmethod | ||
def can_document_member( | ||
cls: type[Documenter], member: Any, membername: str, isattr: bool, parent: Any | ||
cls, member: Any, membername: str, isattr: bool, parent: Any | ||
) -> bool: | ||
return isinstance(member, type) or ( | ||
isattr and isinstance(member, NewType | TypeVar) | ||
) | ||
return isinstance(member, type) or (isattr and cls._is_typelike(member)) | ||
|
||
@staticmethod | ||
def _is_typelike(obj: Any) -> bool: | ||
return isinstance(obj, NewType | TypeVar | TypeAliasType) | ||
|
||
def import_object(self, raiseerror: bool = False) -> bool: | ||
ret = super().import_object(raiseerror) | ||
|
@@ -1705,7 +1712,7 @@ def import_object(self, raiseerror: bool = False) -> bool: | |
self.doc_as_attr = self.objpath[-1] != self.object.__name__ | ||
else: | ||
self.doc_as_attr = True | ||
if isinstance(self.object, NewType | TypeVar): | ||
if self._is_typelike(self.object): | ||
modname = getattr(self.object, '__module__', self.modname) | ||
if modname != self.modname and self.modname.startswith(modname): | ||
bases = self.modname[len(modname) :].strip('.').split('.') | ||
|
@@ -1714,7 +1721,7 @@ def import_object(self, raiseerror: bool = False) -> bool: | |
return ret | ||
|
||
def _get_signature(self) -> tuple[Any | None, str | None, Signature | None]: | ||
if isinstance(self.object, NewType | TypeVar): | ||
if self._is_typelike(self.object): | ||
# Suppress signature | ||
return None, None, None | ||
|
||
|
@@ -1925,6 +1932,8 @@ def add_directive_header(self, sig: str) -> None: | |
|
||
if self.doc_as_attr: | ||
self.directivetype = 'attribute' | ||
if isinstance(self.object, TypeAliasType): | ||
self.directivetype = 'type' | ||
super().add_directive_header(sig) | ||
|
||
if isinstance(self.object, NewType | TypeVar): | ||
|
@@ -1942,6 +1951,11 @@ def add_directive_header(self, sig: str) -> None: | |
): | ||
self.add_line(' :canonical: %s' % canonical_fullname, sourcename) | ||
|
||
if isinstance(self.object, TypeAliasType): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
See litestar-org/litestar#3982 (comment) for more details. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thank you. Doesn't seem relevant in this context, though. Only one of them will be imported at any given time (0912a5e#diff-e43bdd6f8f37a12d2536e09e57c5e8999cb8de18b9c7ba49126f90576c4328acR57), so the parsed code will always be interpreted consistently as either one or the other. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is there a way for the above |
||
aliased = stringify_annotation(self.object.__value__) | ||
self.add_line(' :canonical: %s' % aliased, sourcename) | ||
return | ||
|
||
# add inheritance info, if wanted | ||
if not self.doc_as_attr and self.options.show_inheritance: | ||
if inspect.getorigbases(self.object): | ||
|
Original file line number | Diff line number | Diff line change | ||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -9,6 +9,7 @@ | |||||||||||||
import itertools | ||||||||||||||
import operator | ||||||||||||||
import re | ||||||||||||||
import sys | ||||||||||||||
import tokenize | ||||||||||||||
from token import DEDENT, INDENT, NAME, NEWLINE, NUMBER, OP, STRING | ||||||||||||||
from tokenize import COMMENT, NL | ||||||||||||||
|
@@ -332,6 +333,48 @@ def get_line(self, lineno: int) -> str: | |||||||||||||
"""Returns specified line.""" | ||||||||||||||
return self.buffers[lineno - 1] | ||||||||||||||
|
||||||||||||||
def collect_doc_comment( | ||||||||||||||
self, | ||||||||||||||
# exists for >= 3.12, irrelevant for runtime | ||||||||||||||
node: ast.Assign | ast.TypeAlias, # type: ignore[name-defined] | ||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't really like this suppression so how about having an "AssignmentLike = ast.Assign" for < 3.12 and |
||||||||||||||
varnames: list[str], | ||||||||||||||
current_line: str, | ||||||||||||||
) -> None: | ||||||||||||||
# check comments after assignment | ||||||||||||||
parser = AfterCommentParser([ | ||||||||||||||
current_line[node.col_offset :], | ||||||||||||||
*self.buffers[node.lineno :], | ||||||||||||||
]) | ||||||||||||||
parser.parse() | ||||||||||||||
if parser.comment and comment_re.match(parser.comment): | ||||||||||||||
for varname in varnames: | ||||||||||||||
self.add_variable_comment( | ||||||||||||||
varname, comment_re.sub('\\1', parser.comment) | ||||||||||||||
) | ||||||||||||||
self.add_entry(varname) | ||||||||||||||
return | ||||||||||||||
|
||||||||||||||
# check comments before assignment | ||||||||||||||
if indent_re.match(current_line[: node.col_offset]): | ||||||||||||||
comment_lines = [] | ||||||||||||||
for i in range(node.lineno - 1): | ||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. instead of going from 0 to node.lineno - 2 and compute node.lineno - 1 - i, why not using a reversed range? |
||||||||||||||
before_line = self.get_line(node.lineno - 1 - i) | ||||||||||||||
if comment_re.match(before_line): | ||||||||||||||
comment_lines.append(comment_re.sub('\\1', before_line)) | ||||||||||||||
else: | ||||||||||||||
break | ||||||||||||||
|
||||||||||||||
if comment_lines: | ||||||||||||||
comment = dedent_docstring('\n'.join(reversed(comment_lines))) | ||||||||||||||
for varname in varnames: | ||||||||||||||
self.add_variable_comment(varname, comment) | ||||||||||||||
self.add_entry(varname) | ||||||||||||||
return | ||||||||||||||
|
||||||||||||||
# not commented (record deforders only) | ||||||||||||||
for varname in varnames: | ||||||||||||||
self.add_entry(varname) | ||||||||||||||
|
||||||||||||||
def visit(self, node: ast.AST) -> None: | ||||||||||||||
"""Updates self.previous to the given node.""" | ||||||||||||||
super().visit(node) | ||||||||||||||
|
@@ -381,53 +424,19 @@ def visit_Assign(self, node: ast.Assign) -> None: | |||||||||||||
elif hasattr(node, 'type_comment') and node.type_comment: | ||||||||||||||
for varname in varnames: | ||||||||||||||
self.add_variable_annotation(varname, node.type_comment) # type: ignore[arg-type] | ||||||||||||||
|
||||||||||||||
# check comments after assignment | ||||||||||||||
parser = AfterCommentParser([ | ||||||||||||||
current_line[node.col_offset :], | ||||||||||||||
*self.buffers[node.lineno :], | ||||||||||||||
]) | ||||||||||||||
parser.parse() | ||||||||||||||
if parser.comment and comment_re.match(parser.comment): | ||||||||||||||
for varname in varnames: | ||||||||||||||
self.add_variable_comment( | ||||||||||||||
varname, comment_re.sub('\\1', parser.comment) | ||||||||||||||
) | ||||||||||||||
self.add_entry(varname) | ||||||||||||||
return | ||||||||||||||
|
||||||||||||||
# check comments before assignment | ||||||||||||||
if indent_re.match(current_line[: node.col_offset]): | ||||||||||||||
comment_lines = [] | ||||||||||||||
for i in range(node.lineno - 1): | ||||||||||||||
before_line = self.get_line(node.lineno - 1 - i) | ||||||||||||||
if comment_re.match(before_line): | ||||||||||||||
comment_lines.append(comment_re.sub('\\1', before_line)) | ||||||||||||||
else: | ||||||||||||||
break | ||||||||||||||
|
||||||||||||||
if comment_lines: | ||||||||||||||
comment = dedent_docstring('\n'.join(reversed(comment_lines))) | ||||||||||||||
for varname in varnames: | ||||||||||||||
self.add_variable_comment(varname, comment) | ||||||||||||||
self.add_entry(varname) | ||||||||||||||
return | ||||||||||||||
|
||||||||||||||
# not commented (record deforders only) | ||||||||||||||
for varname in varnames: | ||||||||||||||
self.add_entry(varname) | ||||||||||||||
self.collect_doc_comment(node, varnames, current_line) | ||||||||||||||
|
||||||||||||||
def visit_AnnAssign(self, node: ast.AnnAssign) -> None: | ||||||||||||||
"""Handles AnnAssign node and pick up a variable comment.""" | ||||||||||||||
self.visit_Assign(node) # type: ignore[arg-type] | ||||||||||||||
|
||||||||||||||
def visit_Expr(self, node: ast.Expr) -> None: | ||||||||||||||
"""Handles Expr node and pick up a comment if string.""" | ||||||||||||||
if ( | ||||||||||||||
isinstance(self.previous, ast.Assign | ast.AnnAssign) | ||||||||||||||
and isinstance(node.value, ast.Constant) | ||||||||||||||
and isinstance(node.value.value, str) | ||||||||||||||
if not ( | ||||||||||||||
isinstance(node.value, ast.Constant) and isinstance(node.value.value, str) | ||||||||||||||
): | ||||||||||||||
return | ||||||||||||||
if isinstance(self.previous, ast.Assign | ast.AnnAssign): | ||||||||||||||
try: | ||||||||||||||
targets = get_assign_targets(self.previous) | ||||||||||||||
varnames = get_lvar_names(targets[0], self.get_self()) | ||||||||||||||
|
@@ -441,6 +450,13 @@ def visit_Expr(self, node: ast.Expr) -> None: | |||||||||||||
self.add_entry(varname) | ||||||||||||||
except TypeError: | ||||||||||||||
pass # this assignment is not new definition! | ||||||||||||||
if (sys.version_info[:2] >= (3, 12)) and isinstance( | ||||||||||||||
self.previous, ast.TypeAlias | ||||||||||||||
): | ||||||||||||||
Comment on lines
+453
to
+455
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||
varname = self.previous.name.id | ||||||||||||||
docstring = node.value.value | ||||||||||||||
self.add_variable_comment(varname, dedent_docstring(docstring)) | ||||||||||||||
self.add_entry(varname) | ||||||||||||||
|
||||||||||||||
def visit_Try(self, node: ast.Try) -> None: | ||||||||||||||
"""Handles Try node and processes body and else-clause. | ||||||||||||||
|
@@ -485,6 +501,17 @@ def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef) -> None: | |||||||||||||
"""Handles AsyncFunctionDef node and set context.""" | ||||||||||||||
self.visit_FunctionDef(node) # type: ignore[arg-type] | ||||||||||||||
|
||||||||||||||
if sys.version_info[:2] >= (3, 12): | ||||||||||||||
|
||||||||||||||
def visit_TypeAlias(self, node: ast.TypeAlias) -> None: | ||||||||||||||
"""Handles TypeAlias node and picks up a variable comment. | ||||||||||||||
|
||||||||||||||
.. note:: TypeAlias node refers to `type Foo = Bar` (PEP 695) assignment, | ||||||||||||||
NOT `Foo: TypeAlias = Bar` (PEP 613). | ||||||||||||||
""" | ||||||||||||||
current_line = self.get_line(node.lineno) | ||||||||||||||
self.collect_doc_comment(node, [node.name.id], current_line) | ||||||||||||||
|
||||||||||||||
|
||||||||||||||
class DefinitionFinder(TokenProcessor): | ||||||||||||||
"""Python source code parser to detect location of functions, | ||||||||||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
from typing import NewType | ||
|
||
|
||
class Foo: | ||
"""This is class Foo.""" | ||
|
||
|
||
type Pep695Alias = Foo | ||
"""This is PEP695 type alias.""" | ||
|
||
type Pep695AliasC = dict[ | ||
str, Foo | ||
] #: This is PEP695 complex type alias with doc comment. | ||
|
||
type Pep695AliasUnion = str | int | ||
"""This is PEP695 type alias for union.""" | ||
|
||
type Pep695AliasOfAlias = Pep695AliasC | ||
"""This is PEP695 type alias of PEP695 alias.""" | ||
|
||
Bar = NewType('Bar', Pep695Alias) | ||
"""This is newtype of Pep695Alias.""" | ||
|
||
|
||
def ret_pep695(a: Pep695Alias) -> Pep695Alias: | ||
"""This fn accepts and returns PEP695 alias.""" | ||
... |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.