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. diff --git a/pydoctor/astbuilder.py b/pydoctor/astbuilder.py index 00fbe0e8f..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) @@ -766,6 +776,11 @@ 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): + 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, diff --git a/pydoctor/model.py b/pydoctor/model.py index 31c30ac91..a4c190971 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 @@ -874,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') @@ -906,7 +913,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 +935,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 +1120,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: _AttributeT) -> 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..de2b6b354 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,21 @@ 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 attr.value + assert unparse(attr.value).strip() == "t.Literal['1', 1]" + + @systemcls_param def test_typevartuple(systemcls: Type[model.System]) -> None: """