From 04c7cb924d15ab71ca07bf41ebc4cf6386f27aaf Mon Sep 17 00:00:00 2001 From: tristanlatr Date: Sun, 21 Apr 2024 14:05:52 -0400 Subject: [PATCH 1/3] Fix the function to upgrade annotations. Respect annotations rules like in our linker. --- pydoctor/astutils.py | 20 +++++++++++++++++--- pydoctor/linker.py | 21 +++++++++------------ pydoctor/model.py | 11 +++++++++++ pydoctor/test/test_astbuilder.py | 4 ++++ 4 files changed, 41 insertions(+), 15 deletions(-) diff --git a/pydoctor/astutils.py b/pydoctor/astutils.py index a3ea669ea..c949a462e 100644 --- a/pydoctor/astutils.py +++ b/pydoctor/astutils.py @@ -115,11 +115,21 @@ def node2dottedname(node: Optional[ast.AST]) -> Optional[List[str]]: parts.reverse() return parts -def node2fullname(expr: Optional[ast.AST], ctx: 'model.Documentable') -> Optional[str]: +def node2fullname(expr: Optional[ast.AST], + ctx: model.Documentable | None = None, + *, + expandName:Callable[[str], str] | None = None) -> Optional[str]: + if expandName is None: + if ctx is None: + raise TypeError('this function takes exactly two arguments') + expandName = ctx.expandName + elif ctx is not None: + raise TypeError('this function takes exactly two arguments') + dottedname = node2dottedname(expr) if dottedname is None: return None - return ctx.expandName('.'.join(dottedname)) + return expandName('.'.join(dottedname)) def bind_args(sig: Signature, call: ast.Call) -> BoundArguments: """ @@ -294,7 +304,7 @@ def visit(self, node:ast.AST) -> ast.expr:... def __init__(self, ctx: model.Documentable) -> None: def _node2fullname(node:ast.expr) -> str | None: - return node2fullname(node, ctx) + return node2fullname(node, expandName=ctx.expandAnnotationName) self.node2fullname = _node2fullname def _union_args_to_bitor(self, args: list[ast.expr], ctxnode:ast.AST) -> ast.BinOp: @@ -359,6 +369,10 @@ def visit_Subscript(self, node: ast.Subscript) -> ast.expr: "typing.FrozenSet": 'frozenset', } +# These do not beling in the deprecated builtins aliases, so we make sure it doesn't happen. +assert 'typing.Union' not in DEPRECATED_TYPING_ALIAS_BUILTINS +assert 'typing.Optional' not in DEPRECATED_TYPING_ALIAS_BUILTINS + TYPING_ALIAS = ( "typing.Hashable", "typing.Awaitable", diff --git a/pydoctor/linker.py b/pydoctor/linker.py index f403e64dc..c9728492b 100644 --- a/pydoctor/linker.py +++ b/pydoctor/linker.py @@ -131,8 +131,11 @@ def look_for_intersphinx(self, name: str) -> Optional[str]: """ return self.obj.system.intersphinx.getLink(name) - def link_to(self, identifier: str, label: "Flattenable") -> Tag: - fullID = self.obj.expandName(identifier) + def link_to(self, identifier: str, label: "Flattenable", *, _is_annotation: bool = False) -> Tag: + if _is_annotation: + fullID = self.obj.expandAnnotationName(identifier) + else: + fullID = self.obj.expandName(identifier) target = self.obj.system.objForFullName(fullID) if target is not None: @@ -250,8 +253,7 @@ def __init__(self, obj:'model.Documentable') -> None: self._obj = obj self._module = obj.module self._scope = obj.parent or obj - self._module_linker = self._module.docstring_linker - self._scope_linker = self._scope.docstring_linker + self._scope_linker = _EpydocLinker(self._scope) @property def obj(self) -> 'model.Documentable': @@ -272,11 +274,7 @@ def link_to(self, target: str, label: "Flattenable") -> Tag: with self.switch_context(self._obj): if self._module.isNameDefined(target): self.warn_ambiguous_annotation(target) - return self._module_linker.link_to(target, label) - elif self._scope.isNameDefined(target): - return self._scope_linker.link_to(target, label) - else: - return self._module_linker.link_to(target, label) + return self._scope_linker.link_to(target, label, _is_annotation=True) def link_xref(self, target: str, label: "Flattenable", lineno: int) -> Tag: with self.switch_context(self._obj): @@ -284,6 +282,5 @@ def link_xref(self, target: str, label: "Flattenable", lineno: int) -> Tag: @contextlib.contextmanager def switch_context(self, ob:Optional['model.Documentable']) -> Iterator[None]: - with self._module_linker.switch_context(ob): - with self._scope_linker.switch_context(ob): - yield + with self._scope_linker.switch_context(ob): + yield diff --git a/pydoctor/model.py b/pydoctor/model.py index 31c30ac91..4c4698cbe 100644 --- a/pydoctor/model.py +++ b/pydoctor/model.py @@ -346,6 +346,17 @@ class E: obj = nxt return '.'.join([full_name] + parts[i + 1:]) + def expandAnnotationName(self, name: str) -> str: + """ + Like L{expandName} but gives precedence to the module scope when a + name is defined both in the current scope and the module scope. + """ + if self.module.isNameDefined(name): + return self.module.expandName(name) + elif self.isNameDefined(name): + return self.expandName(name) + return self.module.expandName(name) + def resolveName(self, name: str) -> Optional['Documentable']: """Return the object named by "name" (using Python's lookup rules) in this context, if any is known to pydoctor.""" diff --git a/pydoctor/test/test_astbuilder.py b/pydoctor/test/test_astbuilder.py index 06e8d15ed..6578138d6 100644 --- a/pydoctor/test/test_astbuilder.py +++ b/pydoctor/test/test_astbuilder.py @@ -1408,6 +1408,9 @@ def test_upgrade_annotation(systemcls: Type[model.System]) -> None: f: Union[(str,)] g: Optional[1, 2] # wrong, so we don't process it h: Union[list[str]] + + class List: + Union: Union[a, b] ''', systemcls=systemcls) assert ann_str_and_line(mod.contents['a']) == ('str | int', 2) assert ann_str_and_line(mod.contents['b']) == ('str | None', 3) @@ -1417,6 +1420,7 @@ def test_upgrade_annotation(systemcls: Type[model.System]) -> None: assert ann_str_and_line(mod.contents['f']) == ('str', 7) assert ann_str_and_line(mod.contents['g']) == ('Optional[1, 2]', 8) assert ann_str_and_line(mod.contents['h']) == ('list[str]', 9) + assert ann_str_and_line(mod.contents['List'].contents['Union']) == ('a | b', 12) @pytest.mark.parametrize('annotation', ("[", "pass", "1 ; 2")) @systemcls_param From bfb22ecd89528ce519e4675649681ab98cca144c Mon Sep 17 00:00:00 2001 From: tristanlatr <19967168+tristanlatr@users.noreply.github.com> Date: Sun, 21 Apr 2024 15:03:56 -0400 Subject: [PATCH 2/3] Typo --- pydoctor/astutils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pydoctor/astutils.py b/pydoctor/astutils.py index c949a462e..b96c489ff 100644 --- a/pydoctor/astutils.py +++ b/pydoctor/astutils.py @@ -369,7 +369,7 @@ def visit_Subscript(self, node: ast.Subscript) -> ast.expr: "typing.FrozenSet": 'frozenset', } -# These do not beling in the deprecated builtins aliases, so we make sure it doesn't happen. +# These do not belong in the deprecated builtins aliases, so we make sure it doesn't happen. assert 'typing.Union' not in DEPRECATED_TYPING_ALIAS_BUILTINS assert 'typing.Optional' not in DEPRECATED_TYPING_ALIAS_BUILTINS From b839787646315c51c34500d7076ee76da279d996 Mon Sep 17 00:00:00 2001 From: tristanlatr <19967168+tristanlatr@users.noreply.github.com> Date: Sun, 21 Apr 2024 22:45:12 -0400 Subject: [PATCH 3/3] No private arguments --- pydoctor/linker.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pydoctor/linker.py b/pydoctor/linker.py index c9728492b..a36949339 100644 --- a/pydoctor/linker.py +++ b/pydoctor/linker.py @@ -131,8 +131,8 @@ def look_for_intersphinx(self, name: str) -> Optional[str]: """ return self.obj.system.intersphinx.getLink(name) - def link_to(self, identifier: str, label: "Flattenable", *, _is_annotation: bool = False) -> Tag: - if _is_annotation: + def link_to(self, identifier: str, label: "Flattenable", *, is_annotation: bool = False) -> Tag: + if is_annotation: fullID = self.obj.expandAnnotationName(identifier) else: fullID = self.obj.expandName(identifier) @@ -274,7 +274,7 @@ def link_to(self, target: str, label: "Flattenable") -> Tag: with self.switch_context(self._obj): if self._module.isNameDefined(target): self.warn_ambiguous_annotation(target) - return self._scope_linker.link_to(target, label, _is_annotation=True) + return self._scope_linker.link_to(target, label, is_annotation=True) def link_xref(self, target: str, label: "Flattenable", lineno: int) -> Tag: with self.switch_context(self._obj):