Skip to content

Commit

Permalink
Give up inherited annotation propagation because of #295
Browse files Browse the repository at this point in the history
  • Loading branch information
tristanlatr committed May 30, 2023
1 parent aa99246 commit dfbc996
Show file tree
Hide file tree
Showing 4 changed files with 38 additions and 49 deletions.
2 changes: 0 additions & 2 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,6 @@ in development
when no terms already contain a wildcard.
* Link recognized constructors in class page.
* Ensure that explicit annotation are honored when there are multiple declarations of the same name.
* Check in super classes or implemented inferfaces for attributes type hints before
trying to infer the type from it's ast value.

pydoctor 22.9.1
^^^^^^^^^^^^^^^
Expand Down
8 changes: 4 additions & 4 deletions pydoctor/astbuilder.py
Original file line number Diff line number Diff line change
Expand Up @@ -495,13 +495,13 @@ def _handleConstant(self, obj: model.Attribute, value: Optional[ast.expr], linen
# Simply ignore it because it's duplication of information.
obj.annotation = infer_type(value) if value else None

@classmethod
def _setAttributeAnnotation(cls, obj: model.Attribute,
@staticmethod
def _setAttributeAnnotation(obj: model.Attribute,
annotation: Optional[ast.expr],) -> None:
if annotation is not None:
# What to do when an attribute has several explicit annotations?
# TODO: What to do when an attribute has several explicit annotations?
# (mypy reports a warning in these kind of cases)
obj.annotation = annotation
obj.explicit_annotation = True

def _handleModuleVar(self,
target: str,
Expand Down
48 changes: 11 additions & 37 deletions pydoctor/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -819,7 +819,6 @@ class FunctionOverload:
class Attribute(Inheritable):
kind: Optional[DocumentableKind] = DocumentableKind.ATTRIBUTE
annotation: Optional[ast.expr] = None
explicit_annotation = False
decorators: Optional[Sequence[ast.expr]] = None
value: Optional[ast.expr] = None
"""
Expand All @@ -828,35 +827,6 @@ class Attribute(Inheritable):
None value means the value is not initialized at the current point of the the process.
"""

def _init_annotation(self) -> None:
"""
If this attribute has not explicit annotation, try to look
int super classes/interface declarations or infer the type from it's ast expression.
"""
if self.annotation is not None:
# do not override annotation
return

# lookup inherited annotations, use docsources()
docsources = self.docsources()
# discard self
next(docsources)

for inherited in docsources:
if isinstance(inherited, Attribute) and inherited.explicit_annotation:
# parse the type of inherited annotation early with the inherited context
# to ensure validity of generated links.
from pydoctor.epydoc2stan import type2stan, ParsedStanOnly
stan = type2stan(inherited)
if stan: self.parsed_type = ParsedStanOnly(stan)
return

# if no inherited annotation is found, try to infer the type from
# it's ast expression, the result will not be propated to
# subclass members that override this symbol.
if self.value is not None:
self.annotation = astutils.infer_type(self.value)

# Work around the attributes of the same name within the System class.
_ModuleT = Module
_PackageT = Package
Expand Down Expand Up @@ -1441,26 +1411,30 @@ def postProcess(self) -> None:
without the risk of drawing incorrect conclusions because modules
were not fully processed yet.
"""

# default post-processing includes:
# - Processing of subclasses
# - MRO computing.
# - Lookup of constructors
# - Checking whether the class is an exception
for cls in self.objectsOfType(Class):

# Initiate the MROs
cls._init_mro()
# Lookup of constructors
cls._init_constructors()

# Compute subclasses
for b in cls.baseobjects:
if b is not None:
b.subclasses.append(cls)

# Checking whether the class is an exception
if is_exception(cls):
cls.kind = DocumentableKind.EXCEPTION

# Infer annotation in post-processing so
# to explicit annotation take precedence.
for attrib in self.objectsOfType(Attribute):
attrib._init_annotation()
# If this attribute has not explicit annotation,
# infer its type from it's ast expression.
if attrib.annotation is None and attrib.value is not None:
# do not override explicit annotation
attrib.annotation = astutils.infer_type(attrib.value)

for post_processor in self._post_processors:
post_processor(self)
Expand Down
29 changes: 23 additions & 6 deletions pydoctor/test/test_astbuilder.py
Original file line number Diff line number Diff line change
Expand Up @@ -2383,6 +2383,9 @@ def __init__(self):

@systemcls_param
def test_explicit_annotation_wins_over_inferred_type(systemcls: Type[model.System]) -> None:
"""
Explicit annotations are the preffered way of presenting the type of an attribute.
"""
src = '''\
class Stuff(object):
thing: List[Tuple[Thing, ...]]
Expand All @@ -2404,8 +2407,10 @@ def __init__(self):
assert flatten_text(epydoc2stan.type2stan(thing)) == "List[Tuple[Thing, ...]]" #type:ignore

@systemcls_param
def test_explicit_inherited_annotation_wins_over_inferred_type(systemcls: Type[model.System]) -> None:

def test_explicit_inherited_annotation_looses_over_inferred_type(systemcls: Type[model.System]) -> None:
"""
Annotation are of inherited.
"""
src = '''\
class _Stuff(object):
thing: List[Tuple[Thing, ...]]
Expand All @@ -2415,10 +2420,14 @@ def __init__(self):
'''
mod = fromText(src, systemcls=systemcls, modname='mod')
thing = mod.system.allobjects['mod.Stuff.thing']
assert flatten_text(epydoc2stan.type2stan(thing)) == "List[Tuple[Thing, ...]]" #type:ignore
assert flatten_text(epydoc2stan.type2stan(thing)) == "list" #type:ignore

@systemcls_param
def test_inferred_type_override(systemcls: Type[model.System]) -> None:
"""
The last visited value will be used to infer the type annotation
of an unnanotated attribute.
"""
src = '''\
class Stuff(object):
thing = 1
Expand All @@ -2431,6 +2440,9 @@ def __init__(self):

@systemcls_param
def test_inferred_type_is_not_propagated_to_subclasses(systemcls: Type[model.System]) -> None:
"""
Inferred type annotation should not be propagated to subclasses.
"""
src = '''\
class _Stuff(object):
def __init__(self):
Expand All @@ -2445,23 +2457,28 @@ def __init__(self, thing):


@systemcls_param
def test_inherited_type_correct_links(systemcls: Type[model.System]) -> None:
def test_inherited_type_is_not_propagated_to_subclasses(systemcls: Type[model.System]) -> None:
"""
We can't repliably propage the annotations from one class to it's subclass because of
issue https://github.com/twisted/pydoctor/issues/295.
"""
src1 = '''\
class _s:...
class _Stuff(object):
def __init__(self):
self.thing:_s = []
'''
src2 = '''\
from base import _Stuff
from base import _Stuff, _s
class Stuff(_Stuff):
def __init__(self, thing):
self.thing = thing
__all__=['Stuff', '_s']
'''
system = systemcls()
builder = system.systemBuilder(system)
builder.addModuleString(src1, 'base')
builder.addModuleString(src2, 'mod')
builder.buildModules()
thing = system.allobjects['mod.Stuff.thing']
assert 'href="base._s.html"' in flatten(epydoc2stan.type2stan(thing)) #type:ignore
assert epydoc2stan.type2stan(thing) is None

0 comments on commit dfbc996

Please sign in to comment.