Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
87 changes: 87 additions & 0 deletions Lib/test/test_typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -4804,12 +4804,99 @@ class C(A[T, VT], Generic[VT, T, KT], B[KT, T]):

self.assertEqual(C.__parameters__, (VT, T, KT))

def test_multiple_inheritance_multiple_layers(self):
S = TypeVar('S')
class GenericMixin(Generic[S]): ...
class GenericBase(Generic[S]): ...
class FromGenericAlias(GenericBase[S]): ...
class FromConcreteGenericAlias(GenericBase[int]): ...

# Generic is second:
class A1(FromGenericAlias, Generic[S]): ...
self.assertEqual(A1.__mro__,
(A1, FromGenericAlias, GenericBase, Generic, object))

class A2(FromGenericAlias[S], Generic[S]): ...
self.assertEqual(A2.__mro__,
(A2, FromGenericAlias, GenericBase, Generic, object))

class A3(FromGenericAlias[int], Generic[S]): ...
self.assertEqual(A3.__mro__,
(A3, FromGenericAlias, GenericBase, Generic, object))

class A4(FromConcreteGenericAlias, Generic[S]): ...
self.assertEqual(A4.__mro__,
(A4, FromConcreteGenericAlias, GenericBase, Generic, object))

class A5(FromGenericAlias[S], GenericMixin[S], Generic[S]): ...
self.assertEqual(A5.__mro__,
(A5, FromGenericAlias, GenericBase, GenericMixin, Generic, object))

# Generic is first:
class B1(Generic[S], FromGenericAlias): ...
self.assertEqual(B1.__mro__,
(B1, FromGenericAlias, GenericBase, Generic, object))

class B2(Generic[S], FromGenericAlias[S]): ...
self.assertEqual(B2.__mro__,
(B2, FromGenericAlias, GenericBase, Generic, object))

class B3(Generic[S], FromGenericAlias[int]): ...
self.assertEqual(B3.__mro__,
(B3, FromGenericAlias, GenericBase, Generic, object))

class B4(Generic[S], FromConcreteGenericAlias): ...
self.assertEqual(B4.__mro__,
(B4, FromConcreteGenericAlias, GenericBase, Generic, object))

class B5(FromGenericAlias[S], GenericMixin[S], Generic[S]): ...
self.assertEqual(B5.__mro__,
(B5, FromGenericAlias, GenericBase, GenericMixin, Generic, object))

def test_multiple_inheritance_special(self):
S = TypeVar('S')
class B(Generic[S]): ...
class C(List[int], B): ...
self.assertEqual(C.__mro__, (C, list, B, Generic, object))

def test_multiple_inheritance_special_multiple_layers(self):
S = TypeVar('S')
class L(List[S]): ...
class G(list[S]): ...
class M(Generic[S]): ...

# Direct subclasses:
class D1(List[S], Generic[S]): ...
self.assertEqual(D1.__mro__, (D1, list, Generic, object))

class D2(Generic[S], List[S]): ...
self.assertEqual(D2.__mro__, (D2, list, Generic, object))

# Nested subclasses:
class L1(L[S], Generic[S]): ...
self.assertEqual(L1.__mro__, (L1, L, list, Generic, object))

class L2(Generic[S], L[S]): ...
self.assertEqual(L2.__mro__, (L2, L, list, Generic, object))
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This feels wrong: Generic is before L in the bases, so it should be earlier in the MRO.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But L has Generic in its own bases, so it has to come first. Either that or reject it.


class L3(L[S], M[S], Generic[S]): ...
self.assertEqual(L3.__mro__, (L3, L, list, M, Generic, object))

class L4(Generic[S], L[S], M[S]): ...
self.assertEqual(L4.__mro__, (L4, L, list, M, Generic, object))

class G1(G[S], Generic[S]): ...
self.assertEqual(G1.__mro__, (G1, G, list, Generic, object))

class G2(Generic[S], G[S]): ...
self.assertEqual(G2.__mro__, (G2, Generic, G, list, object))

class G3(G[S], M[S], Generic[S]): ...
self.assertEqual(G3.__mro__, (G3, G, list, M, Generic, object))

class G4(Generic[S], G[S], M[S]): ...
self.assertEqual(G4.__mro__, (G4, G, list, M, Generic, object))

def test_init_subclass_super_called(self):
class FinalException(Exception):
pass
Expand Down
6 changes: 5 additions & 1 deletion Lib/typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -1393,7 +1393,11 @@ def __mro_entries__(self, bases):
return ()
i = bases.index(self)
for b in bases[i+1:]:
if isinstance(b, _BaseGenericAlias) and b is not self:
if b is self:
continue
if isinstance(b, _BaseGenericAlias):
Copy link
Copy Markdown
Member

@AlexWaygood AlexWaygood Aug 28, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both of these diffs would make all tests pass:

Suggested change
if isinstance(b, _BaseGenericAlias):
if isinstance(b, (_BaseGenericAlias, types.GenericAlias)):
Suggested change
if isinstance(b, _BaseGenericAlias):
if isinstance(b, types.GenericAlias):
b = b.__origin__
if isinstance(b, _BaseGenericAlias):

But, they can't both be right ;)

This indicates that we're missing some tests for some edge cases.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The first breaks when you use list instead of List (see diff here) because it excludes Generic if any of the following base types is types.GenericAlias while we should only exclude it if it is both that and has Generic in its MRO.

The second might be correct, I can't say off the top of my head.

Not to say that I disagree on your conclusion that this indicates one or more test cases are missing.

return ()
if Generic in getattr(b, '__mro__', ()):
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still don't think this is the proper solution here. Exactly which dunder attributes types.GenericAlias delegates to the "underlying" class has changed in the past. It could change again in the future. This feels to me like it's a fragile solution that could easily break.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can say even more: I don't yet understand what ideal __mro__ should look like for all the cases I've added in tests.

return ()
Comment thread
AlexWaygood marked this conversation as resolved.
return (self.__origin__,)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fix mro resolution for nested generic classes.