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
gh-101688: Implement types.get_original_bases #101827
base: main
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Error looks unrelated? |
Yes, it is. |
Misc/NEWS.d/next/Library/2023-02-11-15-01-32.gh-issue-101688.kwXmfM.rst
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Final round of nits:
Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM, thank you! I'll wait a few days before merging, in case @JelleZijlstra or @gvanrossum have any objections or nits :)
I am very confused. The intent of the Moreover, the examples in the docs assign values to Unless I am missing something dramatically, it feels to me that this is solving a non-problem in a strange way. |
Whilst this is true,
Double check the number of equals |
.. function:: get_original_bases(cls, /) | ||
|
||
Return the objects in the bases list of the class's definition as they | ||
existed prior to any modification by :meth:`~object.__mro_entries__`. This |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What does "modification by __mro_entries__
" refer to? In my mind that is a read-only function that doesn't modify anything.
But I'm probably missing another subtle distinction. :-)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It doesn't modify anything itself I guess, but it does cause the __bases__
to be changed when creating the type.
existed prior to any modification by :meth:`~object.__mro_entries__`. This | |
existed before :meth:`~object.__mro_entries__` caused them to change. This |
Does this reflect the nature of the internals here better do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I still don't follow. Can you point me to the code where __mro_entries__
modifies something?
FWIW In general it's better not to describe things in terms of "internals", but instead refer to the specification of an object or operation in the documentation. (And once something is documented it's no longer internal, even if it has a dunder name.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider the following class:
from typing import Generic, TypeVar
T = TypeVar("T")
class Foo(Generic[T]): ...
Naively, from looking at the code, you might expect the __bases__
attribute of Foo
to be equal to (Generic[T],)
, if you're not familiar with how typing generics and __mro_entries__
work. But it's not:
>>> from typing import TypeVar, Generic
>>> T = TypeVar("T")
>>> class Foo(Generic[T]): ...
...
>>> Foo.__bases__
(<class 'typing.Generic'>,)
>>> Foo.__bases__[0] == Generic[T]
False
This is because Python looks at Generic[T]
(an instance of typing._GenericAlias
), sees it's not an instance of type
, looks for an __mro_entries__
method on Generic[T]
, finds that it does have an __mro_entries__
method, and calls Generic[T].__mro_entries__((Generic[T],))
to determine what the Foo
class's "real" bases should be:
>>> Generic[T].__mro_entries__((Generic[T],))
(<class 'typing.Generic'>,)
So whereas the Foo
class's "original bases" are (Generic[T],)
, the class's "actual bases, after __mro_entries__
has been called on all bases that aren't instances of type
and have __mro_entries__
methods on them" are (Generic,)
.
That's the information we're trying to get across here. The trouble is getting it across in a way that's both concise and clear!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay, I have to admit I was again fooled by misreading. :-( I might as well come clear, I didn't remember the ugly PEP 560 mechanism and thought you were referencing __mro__
. :-(
Maybe we can work a PEP 560 reference in the text, e.g. "Return the tuple of base classes originally passed to class creation, before they were replaced the PEP 560 mechanism using __mro_entries__
."
I'm really sorry for being so dense.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well, you did ask me for a review. :-) PEP 560 is one of the most obscure mechanisms in all of typing, so deserves a mention whenever it is used. Also, perhaps a mention that this essentially just returns __orig_bases__
, if it exists, might be helpful.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure, let's add the PEP-560 link then. Though I don't know how helpful mentioning __orig_bases__
would be for most users, since the attribute is currently completely undocumented AFAIK (other than an offhand reference in PEP-560, shortly above a paragraph that says that the attribute should remain an implementation detail of CPython that shouldn't be used outside the stdlib)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well, you did ask me for a review. :-)
And I very much value your feedback — this is a tough function to describe!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Though I don't know how helpful mentioning
__orig_bases__
would be for most users, since the attribute is currently completely undocumented AFAIK
But the use cases for the new addition are all people who are trying to use __orig_bases__
! They should be able to find this through searching.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hah, fair point :-)
A Python core developer has requested some changes be made to your pull request before we can consider merging it. If you could please address their requests along with any other requests in other reviews from core developers that would be appreciated. Once you have made the requested changes, please leave a comment on this pull request containing the phrase |
FWIW, I fully agree that that "just because it's a dunder doesn't mean it isn't stable or can't be a documented public API". For me, the biggest problem with the status quo is that most people who want to access the You could argue, "Couldn't type checkers just special-case this attribute?", and others have: microsoft/pyright#3442. However, they're (reasonably) very reluctant to do so. As such, the proposal is to add a simple "getter" function to CPython, which we can easily add a stub for in typeshed, as a complement to the |
Yeah, I agree with this for sure. |
Return the objects in the bases list of the class's definition as they | ||
existed prior to any modification by :meth:`~object.__mro_entries__`. This | ||
is useful for introspecting :ref:`Generics <user-defined-generics>`. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How about this, to address @gvanrossum's feedback?
Return the objects in the bases list of the class's definition as they | |
existed prior to any modification by :meth:`~object.__mro_entries__`. This | |
is useful for introspecting :ref:`Generics <user-defined-generics>`. | |
Return the tuple of objects originally given as the bases of *cls* before | |
the :meth:`~object.__mro_entries__` method has been called on any bases | |
(following the mechanisms laid out in :pep:`560`). This is useful for | |
introspecting :ref:`Generics <user-defined-generics>`. | |
For classes that have an ``__orig_bases__`` attribute, this | |
function simply returns the value of ``cls.__orig_bases__``. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Isn't
For classes that have an :attr:`~type.__orig_bases__` attribute, this
going to not resolve properly?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Isn't
For classes that have an :attr:`~type.__orig_bases__` attribute, this
going to not resolve properly?
I changed my suggested change to not include that (you might need to refresh your browser if you're still seeing the old suggestion) :)
This reverts commit dc433c4.
Implements the methods described.
A couple of questions:
__orig_class__
to the slots of all the types that currently don't have them to provide better introspection support (currentlyget_orig_class(list[int]())
is None)