Skip to content

Commit

Permalink
Unstring ALL type aliases (#705)
Browse files Browse the repository at this point in the history
* Fixes #704
  • Loading branch information
tristanlatr authored Jun 13, 2023
1 parent 72b155f commit 965ed95
Show file tree
Hide file tree
Showing 5 changed files with 60 additions and 12 deletions.
1 change: 1 addition & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ in development
scope when possible, when impossible, the theoretical runtime scopes are used. A warning can
be reported when an annotation name is ambiguous (can be resolved to different names
depending on the scope context) with option ``-v``.
* Fix presentation of type aliases in string form.
* Improve the AST colorizer to output less parenthesis when it's not required.
* Fix colorization of dictionary unpacking.
* Improve the class hierarchy such that it links top level names with intersphinx when possible.
Expand Down
16 changes: 7 additions & 9 deletions pydoctor/astbuilder.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,18 +92,11 @@ def _isTypeAlias(self, ob: model.Attribute) -> bool:
Return C{True} if the Attribute is a type alias.
"""
if ob.value is not None:

if is_using_annotations(ob.annotation, ('typing.TypeAlias', 'typing_extensions.TypeAlias'), ob):
try:
ob.value = unstring_annotation(ob.value, ob)
except SyntaxError as e:
ob.report(f"invalid type alias: {e}")
return False
if is_using_annotations(ob.annotation, ('typing.TypeAlias',
'typing_extensions.TypeAlias'), ob):
return True

if is_typing_annotation(ob.value, ob.parent):
return True

return False

def visit_Assign(self, node: Union[ast.Assign, ast.AnnAssign]) -> None:
Expand All @@ -117,7 +110,12 @@ def visit_Assign(self, node: Union[ast.Assign, ast.AnnAssign]) -> None:
return
if self._isTypeAlias(attr) is True:
attr.kind = model.DocumentableKind.TYPE_ALIAS
# unstring type aliases
attr.value = unstring_annotation(
# this cast() is safe because _isTypeAlias() return True only if value is not None
cast(ast.expr, attr.value), attr, section='type alias')
elif self._isTypeVariable(attr) is True:
# TODO: unstring bound argument of type variables
attr.kind = model.DocumentableKind.TYPE_VARIABLE

visit_AnnAssign = visit_Assign
Expand Down
4 changes: 2 additions & 2 deletions pydoctor/astutils.py
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ def is_none_literal(node: ast.expr) -> bool:
"""Does this AST node represent the literal constant None?"""
return isinstance(node, (ast.Constant, ast.NameConstant)) and node.value is None

def unstring_annotation(node: ast.expr, ctx:'model.Documentable') -> ast.expr:
def unstring_annotation(node: ast.expr, ctx:'model.Documentable', section:str='annotation') -> ast.expr:
"""Replace all strings in the given expression by parsed versions.
@return: The unstringed node. If parsing fails, an error is logged
and the original node is returned.
Expand All @@ -205,7 +205,7 @@ def unstring_annotation(node: ast.expr, ctx:'model.Documentable') -> ast.expr:
except SyntaxError as ex:
module = ctx.module
assert module is not None
module.report(f'syntax error in annotation: {ex}', lineno_offset=node.lineno)
module.report(f'syntax error in {section}: {ex}', lineno_offset=node.lineno, section=section)
return node
else:
assert isinstance(expr, ast.expr), expr
Expand Down
20 changes: 19 additions & 1 deletion pydoctor/test/test_astbuilder.py
Original file line number Diff line number Diff line change
Expand Up @@ -2395,4 +2395,22 @@ def __init__(self):
'''

mod = fromText(src, systemcls=systemcls)
assert getConstructorsText(mod.contents['Animal']) == "Animal()"
assert getConstructorsText(mod.contents['Animal']) == "Animal()"

@systemcls_param
def test_typealias_unstring(systemcls: Type[model.System]) -> None:
"""
The type aliases are unstringed by the astbuilder
"""

mod = fromText('''
from typing import Callable
ParserFunction = Callable[[str, List['ParseError']], 'ParsedDocstring']
''', modname='pydoctor.epydoc.markup', systemcls=systemcls)

typealias = mod.contents['ParserFunction']
assert isinstance(typealias, model.Attribute)
assert typealias.value
with pytest.raises(StopIteration):
# there is not Constant nodes in the type alias anymore
next(n for n in ast.walk(typealias.value) if isinstance(n, ast.Constant))
31 changes: 31 additions & 0 deletions pydoctor/test/test_templatewriter.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
HtmlTemplate, UnsupportedTemplateVersion,
OverrideTemplateNotAllowed)
from pydoctor.templatewriter.pages.table import ChildTable
from pydoctor.templatewriter.pages.attributechild import AttributeChild
from pydoctor.templatewriter.summary import isClassNodePrivate, isPrivate, moduleSummary, ClassIndexPage
from pydoctor.test.test_astbuilder import fromText, systemcls_param
from pydoctor.test.test_packages import processPackage, testpackages
Expand Down Expand Up @@ -56,6 +57,12 @@ def getHTMLOf(ob: model.Documentable) -> str:
wr._writeDocsForOne(ob, f)
return f.getvalue().decode()

def getHTMLOfAttribute(ob: model.Attribute) -> str:
assert isinstance(ob, model.Attribute)
tlookup = TemplateLookup(template_dir)
stan = AttributeChild(util.DocGetter(), ob, [],
AttributeChild.lookup_loader(tlookup),)
return flatten(stan)

def test_sidebar() -> None:
src = '''
Expand Down Expand Up @@ -774,6 +781,29 @@ def __new__(cls, name):
assert 'Constructor: ' in html
assert 'Animal(name)' in html

def test_typealias_string_form_linked() -> None:
"""
The type aliases should be unstring before beeing presented to reader, such that
all elements can be linked.
Test for issue https://github.com/twisted/pydoctor/issues/704
"""

mod = fromText('''
from typing import Callable
ParserFunction = Callable[[str, List['ParseError']], 'ParsedDocstring']
class ParseError:
...
class ParsedDocstring:
...
''', modname='pydoctor.epydoc.markup')

typealias = mod.contents['ParserFunction']
assert isinstance(typealias, model.Attribute)
html = getHTMLOfAttribute(typealias)
assert 'href="pydoctor.epydoc.markup.ParseError.html"' in html
assert 'href="pydoctor.epydoc.markup.ParsedDocstring.html"' in html

def test_class_hierarchy_links_top_level_names() -> None:
system = model.System()
system.intersphinx = InMemoryInventory() # type:ignore
Expand All @@ -785,3 +815,4 @@ class Stuff(socket):
mod = fromText(src, system=system)
index = flatten(ClassIndexPage(mod.system, TemplateLookup(template_dir)))
assert 'href="https://docs.python.org/3/library/socket.html#socket.socket"' in index

0 comments on commit 965ed95

Please sign in to comment.