Open sanderr opened 1 year ago
Haven't had time to look into this yet, but generally Generic
is supposed to be the last base class. Maybe that's something we should document or enforce better.
That would've been my alternative suggestion, and I do still think it would be a nice improvement and a satisfcatory resolution. But the lines I linked gave me the impression that it should in fact bepossible to use it anywhere.
@JelleZijlstra have you had any chance to give this some thought? This behavior has been a bit of a nuisance to us: we added a generic base to one of our library's classes, which broke some usages of the form class ClientClass(Generic[T], LibraryClassThatIsNowGeneric)
. As a result we've had to jump through some hoops to ensure backwards compatibility.
It would be wonderful if this just worked, as with my patch, but failing that it would be a great help if we could expect library clients to put Generic
as the last base class and be backed up by the documentation, then we would be free to make any such changes in the future at least.
I played with your proposed patch for a bit and couldn't find anything obviously broken by it, but I don't understand that code well enough to be confident the change is correct.
I would prefer documenting that Generic[T]
should always be the last base class. The new PEP-695 syntax automatically inserts Generic[T]
as the last base.
Thanks for your input. Do you know of anyone who might be more acquainted with this code? Because I do still feel like my patch would be consistent with what is already there, and therefore the current behavior could be considered a (albeit minor) bug. Then again, I can't claim to fully understand all nuances of this code myself.
If not, I'll see if I can open a small documentation PR with the requirement to put Generic
as the last base class, as you suggest.
is supposed to be the last base class. Maybe that's something we should document or enforce better.
I've seen lots of real life uses of Generic
as the first base class:
typeshed
has this: https://github.com/python/typeshed/blob/a094aa09c2aa47721664d3fdef91eda4fac24ebb/stdlib/csv.pyi#L72Moreover, in some cases different Generic
placement will produce different runtime results:
>>> class L1(Generic[T], list[T]): ...
...
>>> class L2(list[T], Generic[T]): ...
...
>>> L1.__mro__
(<class '__main__.L1'>, <class 'typing.Generic'>, <class 'list'>, <class 'object'>)
>>> L2.__mro__
(<class '__main__.L2'>, <class 'list'>, <class 'typing.Generic'>, <class 'object'>)
>>> type(L1[int])
<class 'typing._GenericAlias'>
>>> type(L2[int])
<class 'types.GenericAlias'>
Bug report
When
Generic
appears twice in a class' inheritance tree, Python may fail to find a consistent MRO. An effort seems to be taken to address this in typing._BaseGenericAlias and typing._GenericAlias. However, when one of the bases is a subscripted generic and the other is a child of a subscripted generic (no longer generic itself) this falls short. I believe the root cause is the fact that_GenericAlias
, unlike its parent, only checks forisinstance(b, _BaseGenericAlias
and not forissubclass(b, Generic)
when considering whether it should skip theGeneric
base for this MRO entry.Consider the following example:
The
Works
class does not present a problem becauseGeneric[T]
appears as the last base. TheWorksToo
class works becauseA[int]
is recognized by_GenericAlias.__mro_entries__
as another_BaseGenericAlias
, thereforeGeneric
is only included as MRO entry for the latter.Broken
results in an exception, even thoughB
is semantically equivalent toA[int]
.I managed to get it to run correctly by making the following change to
typing.py
:I am not sufficiently familiar with typing's internals (or even MRO) to be completely confident of this patch, but I believe it to be sound. If this issue gets confirmed I'd be willing to open a pull request with this change.
Your environment
Linked PRs