From 5302052db2ad145078379e4c8b58bf70c863bf2e Mon Sep 17 00:00:00 2001 From: tristanlatr Date: Sun, 31 Mar 2024 20:17:36 -0400 Subject: [PATCH 1/7] Add support for type alias statements --- pydoctor/astbuilder.py | 8 +++++ pydoctor/model.py | 30 ++++++++++++++----- .../templatewriter/pages/attributechild.py | 19 ++++++++---- pydoctor/test/test_astbuilder.py | 15 ++++++++++ 4 files changed, 58 insertions(+), 14 deletions(-) diff --git a/pydoctor/astbuilder.py b/pydoctor/astbuilder.py index 00fbe0e8f..567983380 100644 --- a/pydoctor/astbuilder.py +++ b/pydoctor/astbuilder.py @@ -766,6 +766,14 @@ def visit_Assign(self, node: ast.Assign) -> None: def visit_AnnAssign(self, node: ast.AnnAssign) -> None: annotation = unstring_annotation(node.annotation, self.builder.current) self._handleAssignment(node.target, annotation, node.value, node.lineno) + + def visit_TypeAlias(self, node: ast.TypeAlias) -> None: + if isinstance(node.name, ast.Name): + annotation = ast.Attribute( + value=ast.Name(id='typing', ctx=ast.Load()), + attr='TypeAlias', + ctx=ast.Load()) + self._handleAssignment(node.name, annotation, node.value, node.lineno) def visit_AugAssign(self, node:ast.AugAssign) -> None: self._handleAssignment(node.target, None, node.value, diff --git a/pydoctor/model.py b/pydoctor/model.py index 31c30ac91..b77530122 100644 --- a/pydoctor/model.py +++ b/pydoctor/model.py @@ -65,6 +65,12 @@ class LineFromAst(int): class LineFromDocstringField(int): "Simple L{int} wrapper for linenumbers coming from docstrings." +class AttributeValueDisplay(Enum): + HIDDEN = 0 + AS_CODE_BLOCK = 1 + # Not used yet. Default values of Attrs-like classes' attributes will one day (and maybe others). + #INLINE = 2 + class DocLocation(Enum): OWN_PAGE = 1 PARENT_PAGE = 2 @@ -906,7 +912,7 @@ def import_mod_from_file_location(module_full_name:str, path: Path) -> types.Mod class System: """A collection of related documentable objects. - PyDoctor documents collections of objects, often the contents of a + Pydoctor documents collections of objects, often the contents of a package. """ @@ -928,13 +934,6 @@ class System: Additional list of extensions to load alongside default extensions. """ - show_attr_value = (DocumentableKind.CONSTANT, - DocumentableKind.TYPE_VARIABLE, - DocumentableKind.TYPE_ALIAS) - """ - What kind of attributes we should display the value for? - """ - def __init__(self, options: Optional['Options'] = None): self.allobjects: Dict[str, Documentable] = {} self.rootobjects: List[_ModuleT] = [] @@ -1120,6 +1119,21 @@ def objectsOfType(self, cls: Union[Type['DocumentableT'], str]) -> Iterator['Doc if isinstance(o, cls): yield o + # What kind of attributes we should pydoctor display the value for? + _show_attr_value = set((DocumentableKind.CONSTANT, + DocumentableKind.TYPE_VARIABLE, + DocumentableKind.TYPE_ALIAS)) + + def showAttrValue(self, ob: Attribute) -> AttributeValueDisplay: + """ + Whether to display the value of the given attribute. + """ + + if ob.kind not in self._show_attr_value or ob.value is None: + return AttributeValueDisplay.HIDDEN + # Attribute is a constant/type alias (with a value), then display it's value + return AttributeValueDisplay.AS_CODE_BLOCK + def privacyClass(self, ob: Documentable) -> PrivacyClass: ob_fullName = ob.fullName() cached_privacy = self._privacyClassCache.get(ob_fullName) diff --git a/pydoctor/templatewriter/pages/attributechild.py b/pydoctor/templatewriter/pages/attributechild.py index e22e84fc2..ee3534409 100644 --- a/pydoctor/templatewriter/pages/attributechild.py +++ b/pydoctor/templatewriter/pages/attributechild.py @@ -5,7 +5,7 @@ from twisted.web.iweb import ITemplateLoader from twisted.web.template import Tag, renderer, tags -from pydoctor.model import Attribute +from pydoctor.model import Attribute, AttributeValueDisplay, DocumentableKind from pydoctor import epydoc2stan from pydoctor.templatewriter import TemplateElement, util from pydoctor.templatewriter.pages import format_decorators @@ -55,9 +55,13 @@ def decorator(self, request: object, tag: Tag) -> "Flattenable": @renderer def attribute(self, request: object, tag: Tag) -> "Flattenable": - attr: List["Flattenable"] = [tags.span(self.ob.name, class_='py-defname')] + is_type_alias = self.ob.kind is DocumentableKind.TYPE_ALIAS + attr: List["Flattenable"] = [] + if is_type_alias: + attr += [tags.span('type', class_='py-keyword'), ' ',] + attr += [tags.span(self.ob.name, class_='py-defname')] _type = self.docgetter.get_type(self.ob) - if _type: + if _type and not is_type_alias: attr.extend([': ', _type]) return attr @@ -78,7 +82,10 @@ def functionBody(self, request: object, tag: Tag) -> "Flattenable": @renderer def constantValue(self, request: object, tag: Tag) -> "Flattenable": - if self.ob.kind not in self.ob.system.show_attr_value or self.ob.value is None: + showval = self.ob.system.showAttrValue(self.ob) + if showval is AttributeValueDisplay.HIDDEN: return tag.clear() - # Attribute is a constant/type alias (with a value), then display it's value - return epydoc2stan.format_constant_value(self.ob) + elif showval is AttributeValueDisplay.AS_CODE_BLOCK: + return epydoc2stan.format_constant_value(self.ob) + else: + assert False diff --git a/pydoctor/test/test_astbuilder.py b/pydoctor/test/test_astbuilder.py index 95ac3d803..3a50a7a39 100644 --- a/pydoctor/test/test_astbuilder.py +++ b/pydoctor/test/test_astbuilder.py @@ -6,6 +6,7 @@ from pydoctor import epydoc2stan from pydoctor.epydoc.markup import DocstringLinker, ParsedDocstring from pydoctor.options import Options +from pydoctor.astutils import unparse from pydoctor.stanutils import flatten, html2stan, flatten_text from pydoctor.epydoc.markup.epytext import Element, ParsedEpytextDocstring from pydoctor.epydoc2stan import format_summary, get_parsed_type @@ -2142,6 +2143,20 @@ def __init__(self): assert mod.contents['F'].contents['Pouet'].kind == model.DocumentableKind.INSTANCE_VARIABLE assert mod.contents['F'].contents['Q'].kind == model.DocumentableKind.INSTANCE_VARIABLE +@pytest.mark.skipif(sys.version_info < (3,12), reason='Type variable introduced in Python 3.12') +@systemcls_param +def test_type_alias_definition(systemcls: Type[model.System]) -> None: + src = ''' + import typing as t + type One = t.Literal['1', 1] + ''' + mod = fromText(src, systemcls=systemcls) + attr = mod.contents['One'] + assert isinstance(attr, model.Attribute) + assert attr.kind == model.DocumentableKind.TYPE_ALIAS + assert unparse(attr.value).strip() == "t.Literal['1', 1]" + + @systemcls_param def test_typevartuple(systemcls: Type[model.System]) -> None: """ From 7f9fb551f0d8bf04a782b4ec9a271313f62d2015 Mon Sep 17 00:00:00 2001 From: tristanlatr Date: Sun, 31 Mar 2024 20:43:58 -0400 Subject: [PATCH 2/7] Relax is_using_annotations() checks to work better with manually created nodes --- pydoctor/astutils.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pydoctor/astutils.py b/pydoctor/astutils.py index 1eb4c1c15..0397a3fef 100644 --- a/pydoctor/astutils.py +++ b/pydoctor/astutils.py @@ -189,20 +189,20 @@ def is_using_typing_classvar(expr: Optional[ast.AST], return is_using_annotations(expr, ('typing.ClassVar', "typing_extensions.ClassVar"), ctx) def is_using_annotations(expr: Optional[ast.AST], - annotations:Sequence[str], + annotations: Collection[str], ctx:'model.Documentable') -> bool: """ Detect if this expr is firstly composed by one of the specified annotation(s)' full name. """ - full_name = node2fullname(expr, ctx) - if full_name in annotations: + full_name, dotted_name = node2fullname(expr, ctx), '.'.join(node2dottedname(expr)) + if full_name in annotations or dotted_name in annotations: return True if isinstance(expr, ast.Subscript): # Final[...] or typing.Final[...] expressions if isinstance(expr.value, (ast.Name, ast.Attribute)): value = expr.value - full_name = node2fullname(value, ctx) - if full_name in annotations: + full_name, dotted_name = node2fullname(value, ctx), '.'.join(node2dottedname(value)) + if full_name in annotations or dotted_name in annotations: return True return False From 81d919a0382d3fd86b720604e6b057a3c686811a Mon Sep 17 00:00:00 2001 From: tristanlatr Date: Sun, 31 Mar 2024 22:34:20 -0400 Subject: [PATCH 3/7] Fix type error --- pydoctor/astutils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pydoctor/astutils.py b/pydoctor/astutils.py index 0397a3fef..62b917895 100644 --- a/pydoctor/astutils.py +++ b/pydoctor/astutils.py @@ -194,14 +194,14 @@ def is_using_annotations(expr: Optional[ast.AST], """ Detect if this expr is firstly composed by one of the specified annotation(s)' full name. """ - full_name, dotted_name = node2fullname(expr, ctx), '.'.join(node2dottedname(expr)) + full_name, dotted_name = node2fullname(expr, ctx), '.'.join(node2dottedname(expr) or []) if full_name in annotations or dotted_name in annotations: return True if isinstance(expr, ast.Subscript): # Final[...] or typing.Final[...] expressions if isinstance(expr.value, (ast.Name, ast.Attribute)): value = expr.value - full_name, dotted_name = node2fullname(value, ctx), '.'.join(node2dottedname(value)) + full_name, dotted_name = node2fullname(value, ctx), '.'.join(node2dottedname(value) or []) if full_name in annotations or dotted_name in annotations: return True return False From 4a8db898d052e36521f4a7f55afe8365dd307d3d Mon Sep 17 00:00:00 2001 From: tristanlatr Date: Sun, 31 Mar 2024 22:38:53 -0400 Subject: [PATCH 4/7] Fix mypy --- pydoctor/model.py | 3 ++- pydoctor/test/test_astbuilder.py | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/pydoctor/model.py b/pydoctor/model.py index b77530122..a4c190971 100644 --- a/pydoctor/model.py +++ b/pydoctor/model.py @@ -880,6 +880,7 @@ class Attribute(Inheritable): # Work around the attributes of the same name within the System class. _ModuleT = Module _PackageT = Package +_AttributeT = Attribute T = TypeVar('T') @@ -1124,7 +1125,7 @@ def objectsOfType(self, cls: Union[Type['DocumentableT'], str]) -> Iterator['Doc DocumentableKind.TYPE_VARIABLE, DocumentableKind.TYPE_ALIAS)) - def showAttrValue(self, ob: Attribute) -> AttributeValueDisplay: + def showAttrValue(self, ob: _AttributeT) -> AttributeValueDisplay: """ Whether to display the value of the given attribute. """ diff --git a/pydoctor/test/test_astbuilder.py b/pydoctor/test/test_astbuilder.py index 3a50a7a39..de2b6b354 100644 --- a/pydoctor/test/test_astbuilder.py +++ b/pydoctor/test/test_astbuilder.py @@ -2154,6 +2154,7 @@ def test_type_alias_definition(systemcls: Type[model.System]) -> None: attr = mod.contents['One'] assert isinstance(attr, model.Attribute) assert attr.kind == model.DocumentableKind.TYPE_ALIAS + assert attr.value assert unparse(attr.value).strip() == "t.Literal['1', 1]" From 485847964e0b8cf0176664447bfce566f23551a0 Mon Sep 17 00:00:00 2001 From: tristanlatr Date: Sun, 31 Mar 2024 22:48:53 -0400 Subject: [PATCH 5/7] Revert "Relax is_using_annotations() checks to work better with manually created nodes" This reverts commit 7f9fb551f0d8bf04a782b4ec9a271313f62d2015. --- pydoctor/astutils.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pydoctor/astutils.py b/pydoctor/astutils.py index 62b917895..1eb4c1c15 100644 --- a/pydoctor/astutils.py +++ b/pydoctor/astutils.py @@ -189,20 +189,20 @@ def is_using_typing_classvar(expr: Optional[ast.AST], return is_using_annotations(expr, ('typing.ClassVar', "typing_extensions.ClassVar"), ctx) def is_using_annotations(expr: Optional[ast.AST], - annotations: Collection[str], + annotations:Sequence[str], ctx:'model.Documentable') -> bool: """ Detect if this expr is firstly composed by one of the specified annotation(s)' full name. """ - full_name, dotted_name = node2fullname(expr, ctx), '.'.join(node2dottedname(expr) or []) - if full_name in annotations or dotted_name in annotations: + full_name = node2fullname(expr, ctx) + if full_name in annotations: return True if isinstance(expr, ast.Subscript): # Final[...] or typing.Final[...] expressions if isinstance(expr.value, (ast.Name, ast.Attribute)): value = expr.value - full_name, dotted_name = node2fullname(value, ctx), '.'.join(node2dottedname(value) or []) - if full_name in annotations or dotted_name in annotations: + full_name = node2fullname(value, ctx) + if full_name in annotations: return True return False From 03d336d6daf804d2ae34b5fd9f37c0d09f62750c Mon Sep 17 00:00:00 2001 From: tristanlatr Date: Sun, 31 Mar 2024 22:56:19 -0400 Subject: [PATCH 6/7] Support typer aliases at module level in the main ast builder. --- pydoctor/astbuilder.py | 31 +++++++++++++++++++------------ 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/pydoctor/astbuilder.py b/pydoctor/astbuilder.py index 567983380..0611afdba 100644 --- a/pydoctor/astbuilder.py +++ b/pydoctor/astbuilder.py @@ -553,18 +553,22 @@ def _handleModuleVar(self, expr: Optional[ast.expr], lineno: int, augassign:Optional[ast.operator], + typealiasdef:bool|None=None, ) -> None: if target in MODULE_VARIABLES_META_PARSERS: # This is metadata, not a variable that needs to be documented, # and therefore doesn't need an Attribute instance. return + default_kind = (model.DocumentableKind.VARIABLE + if not typealiasdef else + model.DocumentableKind.TYPE_ALIAS) parent = self.builder.current obj = parent.contents.get(target) if obj is None: if augassign: return obj = self.builder.addAttribute(name=target, - kind=model.DocumentableKind.VARIABLE, + kind=default_kind, parent=parent) # If it's not an attribute it means that the name is already denifed as function/class @@ -586,7 +590,7 @@ def _handleModuleVar(self, obj.setLineNumber(lineno) self._handleConstant(obj, annotation, expr, lineno, - model.DocumentableKind.VARIABLE) + defaultKind=default_kind) self._storeAttrValue(obj, expr, augassign) self._storeCurrentAttr(obj, augassign) @@ -596,11 +600,14 @@ def _handleAssignmentInModule(self, expr: Optional[ast.expr], lineno: int, augassign:Optional[ast.operator], + typealiasdef:bool, ) -> None: module = self.builder.current assert isinstance(module, model.Module) if not _handleAliasing(module, target, expr): - self._handleModuleVar(target, annotation, expr, lineno, augassign=augassign) + self._handleModuleVar(target, annotation, expr, lineno, + augassign=augassign, + typealiasdef=typealiasdef) def _handleClassVar(self, name: str, @@ -724,16 +731,19 @@ def warn(msg: str) -> None: def _handleAssignment(self, targetNode: ast.expr, - annotation: Optional[ast.expr], - expr: Optional[ast.expr], + annotation: ast.expr|None, + expr: ast.expr|None, lineno: int, - augassign:Optional[ast.operator]=None, + augassign:ast.operator|None=None, + typealiasdef:bool=False, ) -> None: if isinstance(targetNode, ast.Name): target = targetNode.id scope = self.builder.current if isinstance(scope, model.Module): - self._handleAssignmentInModule(target, annotation, expr, lineno, augassign=augassign) + self._handleAssignmentInModule(target, annotation, expr, lineno, + augassign=augassign, + typealiasdef=typealiasdef) elif isinstance(scope, model.Class): if augassign or not self._handleOldSchoolMethodDecoration(target, expr): self._handleAssignmentInClass(target, annotation, expr, lineno, augassign=augassign) @@ -769,11 +779,8 @@ def visit_AnnAssign(self, node: ast.AnnAssign) -> None: def visit_TypeAlias(self, node: ast.TypeAlias) -> None: if isinstance(node.name, ast.Name): - annotation = ast.Attribute( - value=ast.Name(id='typing', ctx=ast.Load()), - attr='TypeAlias', - ctx=ast.Load()) - self._handleAssignment(node.name, annotation, node.value, node.lineno) + self._handleAssignment(node.name, None, node.value, + node.lineno, typealiasdef=True) def visit_AugAssign(self, node:ast.AugAssign) -> None: self._handleAssignment(node.target, None, node.value, From 3a3a7ae89f004dfc1e0df5106fc43c694e065c1f Mon Sep 17 00:00:00 2001 From: tristanlatr Date: Sun, 31 Mar 2024 23:07:01 -0400 Subject: [PATCH 7/7] Add a readme entry --- README.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/README.rst b/README.rst index d0f199647..9e0ac8644 100644 --- a/README.rst +++ b/README.rst @@ -80,6 +80,7 @@ This is the last major release to support Python 3.7. * Drop support for Python 3.6. * Add support for Python 3.12 and Python 3.13. +* Add support for the ``type`` statement introduced in Python 3.12. * Astor is no longer a requirement starting at Python 3.9. * `ExtRegistrar.register_post_processor()` now supports a `priority` argument that is an int. Highest priority callables will be called first during post-processing.