Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Handle typing.Generic case in MRO #847

Open
wants to merge 27 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
c32f39b
Fix #846
tristanlatr Dec 6, 2024
2563cd2
Remove commented code
tristanlatr Dec 6, 2024
2cd1e9d
Add some typing
tristanlatr Dec 6, 2024
6993254
Add comment
tristanlatr Dec 6, 2024
78522b2
Fix implementation of MRO post-processing, do it the right way. It sh…
tristanlatr Dec 8, 2024
c85c785
Merge commit '6993254a372a2dd929105fb8432f1bb0b4714005' into 846-typi…
tristanlatr Dec 8, 2024
6451926
Fix pyflakes
tristanlatr Dec 8, 2024
6334e28
Fix type alias for older python versions
tristanlatr Dec 8, 2024
d2df665
Forgot the __future__ import
tristanlatr Dec 8, 2024
7a23766
Fix mypy
tristanlatr Dec 8, 2024
283d340
Try a simpler alternative for python 3.8
tristanlatr Dec 9, 2024
88f5613
Give-up on python 3.8 for this fix
tristanlatr Dec 9, 2024
0e12841
Merge branch 'master' into 846-typing-Generic-in-MRO
tristanlatr Dec 9, 2024
231667f
Merge branch 'master' into 846-typing-Generic-in-MRO
tristanlatr Dec 9, 2024
0b6fa3a
Apply suggestions from code review
tristanlatr Dec 9, 2024
c838a65
Babysit mypy
tristanlatr Dec 9, 2024
7c772fe
Fix typo
tristanlatr Dec 9, 2024
2b8eb36
Apply suggestions from code review
tristanlatr Dec 9, 2024
7307b94
Merge branch 'master' into 846-typing-Generic-in-MRO
tristanlatr Dec 9, 2024
b5c5fed
Merge branch 'master' into 846-typing-Generic-in-MRO
tristanlatr Dec 11, 2024
8d46b2b
Do not strore strings in the graph
tristanlatr Dec 12, 2024
76841e2
Merge commit 'b5c5fedaf50c520e3847f98d35869bd5fe6967b1' into 846-typi…
tristanlatr Dec 12, 2024
569d754
Merge branch 'master' into 846-typing-Generic-in-MRO
tristanlatr Dec 12, 2024
3baa0c9
Fix typing
tristanlatr Dec 12, 2024
bb4497c
Try to fix typing again
tristanlatr Dec 12, 2024
431a976
Try to fix mypy
tristanlatr Dec 12, 2024
0f32429
Apply suggestions from code review
tristanlatr Dec 12, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,9 @@ What's New?
in development
^^^^^^^^^^^^^^

* Fix a bug in the MRO computing code that would result in an incorrect
``Cannot compute linearization of the class inheritance hierarchy`` message
for valid types extending ``typing.Generic`` as well as other generic classes.
* Fix a bug that would cause a variable marked as `Final` not being considered as a constant if
it was declared under a control-flow block.
* Fix a bug in google and numpy "Attributes" section in module docstring:
Expand Down
65 changes: 52 additions & 13 deletions pydoctor/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

import abc
import ast
from functools import partial
from itertools import chain
from collections import defaultdict
import datetime
Expand Down Expand Up @@ -582,7 +583,7 @@ def is_exception(cls: 'Class') -> bool:
return True
return False

def compute_mro(cls:'Class') -> Sequence[Union['Class', str]]:
def compute_mro(cls:'Class', cleanup_generics: bool = False) -> Sequence[Class | str]:
"""
Compute the method resolution order for this class.
This function will also set the
Expand Down Expand Up @@ -622,23 +623,58 @@ def init_finalbaseobjects(o: 'Class', path:Optional[List['Class']]=None) -> None
o._finalbaseobjects = finalbaseobjects
o._finalbases = finalbases

def localbases(o:'Class') -> Iterator[Union['Class', str]]:
# Since the typing.Generic can be listed more than once in the class hierarchy:
# ignore the ones after the first discovered one.
tristanlatr marked this conversation as resolved.
Show resolved Hide resolved
_bases: dict[Class | str, list[Class | str]] = {}
_has_generic: bool = False
def _getbases(o:'Class', ignore_generic: bool = False) -> Iterator[Class | str]:
"""
Like L{Class.baseobjects} but fallback to the expanded name if the base is not resolved to a L{Class} object.
Like L{Class.baseobjects} but fallback to the expanded
name if the base is not resolved to a L{Class} object.

As well as handle multiple typing.Generic case,
see https://github.com/twisted/pydoctor/issues/846.
"""
for s,b in zip(o.bases, o.baseobjects):
if isinstance(b, Class):
yield b
else:
yield s
# Should we make it work event when typing.py is part of the system ?
# since pydoctor is not used to document the standard library
# it's probably not worth it...
if s == 'typing.Generic':
nonlocal _has_generic
_has_generic = True
if not ignore_generic:
yield s
else:
continue
else:
yield s

def getbases(o:Class | str) -> list[Class | str]:
nonlocal _getbases

def getbases(o:Union['Class', str]) -> List[Union['Class', str]]:
if isinstance(o, str):
return []
return list(localbases(o))

if o in _bases:
return _bases[o]

r = list(_getbases(o))
_bases[o] = r

if _has_generic and cleanup_generics:
_getbases = partial(_getbases, ignore_generic=True)

return r

init_finalbaseobjects(cls)
return mro.mro(cls, getbases)
_mro: list[Class | str] = mro.mro(cls, getbases)

if cleanup_generics:
_mro.sort(key=lambda c: c == 'typing.Generic')
return _mro

def _find_dunder_constructor(cls:'Class') -> Optional['Function']:
"""
Expand Down Expand Up @@ -698,7 +734,7 @@ class Class(CanContainImportsDocumentable):
# set in post-processing:
_finalbaseobjects: Optional[List[Optional['Class']]] = None
_finalbases: Optional[List[str]] = None
_mro: Optional[Sequence[Union['Class', str]]] = None
_mro: Optional[Sequence[Class | str]] = None

def setup(self) -> None:
super().setup()
Expand All @@ -714,15 +750,18 @@ def _init_mro(self) -> None:
"""
try:
self._mro = compute_mro(self)
except ValueError as e:
self.report(str(e), 'mro')
self._mro = list(self.allbases(True))
except ValueError:
try:
self._mro = compute_mro(self, cleanup_generics=True)
except ValueError as e:
self.report(str(e), 'mro')
self._mro = list(self.allbases(True))

@overload
def mro(self, include_external:'Literal[True]', include_self:bool=True) -> Sequence[Union['Class', str]]:...
def mro(self, include_external:'Literal[True]', include_self:bool=True) -> Sequence[Class | str]:...
@overload
def mro(self, include_external:'Literal[False]'=False, include_self:bool=True) -> Sequence['Class']:...
def mro(self, include_external:bool=False, include_self:bool=True) -> Sequence[Union['Class', str]]:
def mro(self, include_external:bool=False, include_self:bool=True) -> Sequence[Class | str]:
"""
Get the method resution order of this class.

Expand Down
44 changes: 44 additions & 0 deletions pydoctor/test/test_mro.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,50 @@ class D(C):...
cycle:4: Cycle found while computing inheritance hierarchy: cycle.D -> cycle.C -> cycle.A -> cycle.D
'''

@systemcls_param
def test_mro_generic(systemcls: Type[model.System], capsys:CapSys) -> None:
src = '''
from typing import Generic, TypeVar
T = TypeVar('T')
class A: ...
class B(Generic[T]): ...
class C(A, Generic[T], B[T]): ...
'''
mod = fromText(src, modname='t', systemcls=systemcls)
assert not capsys.readouterr().out
assert_mro_equals(mod.contents['C'],
["t.C", "t.A", "t.B", "typing.Generic"])

@systemcls_param
def test_mro_generic_2(systemcls: Type[model.System], capsys:CapSys) -> None:
src = '''
from typing import Generic, TypeVar
T1 = TypeVar('T1')
T2 = TypeVar('T2')
class A(Generic[T1]): ...
class B(Generic[T2]): ...
class C(A[T1], B[T2]): ...
'''
mod = fromText(src, modname='t', systemcls=systemcls)
assert not capsys.readouterr().out
assert_mro_equals(mod.contents['C'],
["t.C", "t.A", "t.B", "typing.Generic"])

@systemcls_param
def test_mro_generic_3(systemcls: Type[model.System], capsys:CapSys) -> None:
src = '''
from typing import Generic as TGeneric, TypeVar
T = TypeVar('T')
class Generic: ...
class A(Generic): ...
class B(TGeneric[T]): ...
class C(A, B[T]): ...
'''
mod = fromText(src, modname='t', systemcls=systemcls)
assert not capsys.readouterr().out
assert_mro_equals(mod.contents['C'],
["t.C", "t.A", "t.Generic", "t.B", "typing.Generic"])

def test_inherited_docsources()-> None:
simple = fromText("""\
class A:
Expand Down
Loading