gh-101688: Implement types.get_original_bases (#101827)

Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
This commit is contained in:
James Hilton-Balfe 2023-04-23 20:24:30 +01:00 committed by GitHub
parent 05b3ce7339
commit 730bbddfdf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 146 additions and 0 deletions

View File

@ -82,6 +82,46 @@ Dynamic Type Creation
.. versionadded:: 3.7
.. function:: get_original_bases(cls, /)
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 returns the value of ``cls.__orig_bases__``.
For classes without the ``__orig_bases__`` attribute, ``cls.__bases__`` is
returned.
Examples::
from typing import TypeVar, Generic, NamedTuple, TypedDict
T = TypeVar("T")
class Foo(Generic[T]): ...
class Bar(Foo[int], float): ...
class Baz(list[str]): ...
Eggs = NamedTuple("Eggs", [("a", int), ("b", str)])
Spam = TypedDict("Spam", {"a": int, "b": str})
assert Bar.__bases__ == (Foo, float)
assert get_original_bases(Bar) == (Foo[int], float)
assert Baz.__bases__ == (list,)
assert get_original_bases(Baz) == (list[str],)
assert Eggs.__bases__ == (tuple,)
assert get_original_bases(Eggs) == (NamedTuple,)
assert Spam.__bases__ == (dict,)
assert get_original_bases(Spam) == (TypedDict,)
assert int.__bases__ == (object,)
assert get_original_bases(int) == (object,)
.. versionadded:: 3.12
.. seealso::
:pep:`560` - Core support for typing module and generic types

View File

@ -2102,6 +2102,10 @@ Resolving MRO entries
:func:`types.resolve_bases`
Dynamically resolve bases that are not instances of :class:`type`.
:func:`types.get_original_bases`
Retrieve a class's "original bases" prior to modifications by
:meth:`~object.__mro_entries__`.
:pep:`560`
Core support for typing module and generic types.

View File

@ -407,6 +407,13 @@ threading
profiling functions in all running threads in addition to the calling one.
(Contributed by Pablo Galindo in :gh:`93503`.)
types
-----
* Add :func:`types.get_original_bases` to allow for further introspection of
:ref:`user-defined-generics` when subclassed. (Contributed by
James Hilton-Balfe and Alex Waygood in :gh:`101827`.)
unicodedata
-----------

View File

@ -1360,6 +1360,67 @@ class ClassCreationTests(unittest.TestCase):
D = types.new_class('D', (A(), C, B()), {})
self.assertEqual(D.__bases__, (A1, A2, A3, C, B1, B2))
def test_get_original_bases(self):
T = typing.TypeVar('T')
class A: pass
class B(typing.Generic[T]): pass
class C(B[int]): pass
class D(B[str], float): pass
self.assertEqual(types.get_original_bases(A), (object,))
self.assertEqual(types.get_original_bases(B), (typing.Generic[T],))
self.assertEqual(types.get_original_bases(C), (B[int],))
self.assertEqual(types.get_original_bases(int), (object,))
self.assertEqual(types.get_original_bases(D), (B[str], float))
class E(list[T]): pass
class F(list[int]): pass
self.assertEqual(types.get_original_bases(E), (list[T],))
self.assertEqual(types.get_original_bases(F), (list[int],))
class ClassBasedNamedTuple(typing.NamedTuple):
x: int
class GenericNamedTuple(typing.NamedTuple, typing.Generic[T]):
x: T
CallBasedNamedTuple = typing.NamedTuple("CallBasedNamedTuple", [("x", int)])
self.assertIs(
types.get_original_bases(ClassBasedNamedTuple)[0], typing.NamedTuple
)
self.assertEqual(
types.get_original_bases(GenericNamedTuple),
(typing.NamedTuple, typing.Generic[T])
)
self.assertIs(
types.get_original_bases(CallBasedNamedTuple)[0], typing.NamedTuple
)
class ClassBasedTypedDict(typing.TypedDict):
x: int
class GenericTypedDict(typing.TypedDict, typing.Generic[T]):
x: T
CallBasedTypedDict = typing.TypedDict("CallBasedTypedDict", {"x": int})
self.assertIs(
types.get_original_bases(ClassBasedTypedDict)[0],
typing.TypedDict
)
self.assertEqual(
types.get_original_bases(GenericTypedDict),
(typing.TypedDict, typing.Generic[T])
)
self.assertIs(
types.get_original_bases(CallBasedTypedDict)[0],
typing.TypedDict
)
with self.assertRaisesRegex(TypeError, "Expected an instance of type"):
types.get_original_bases(object())
# Many of the following tests are derived from test_descr.py
def test_prepare_class(self):
# Basic test of metaclass derivation

View File

@ -143,6 +143,38 @@ def _calculate_meta(meta, bases):
"of the metaclasses of all its bases")
return winner
def get_original_bases(cls, /):
"""Return the class's "original" bases prior to modification by `__mro_entries__`.
Examples::
from typing import TypeVar, Generic, NamedTuple, TypedDict
T = TypeVar("T")
class Foo(Generic[T]): ...
class Bar(Foo[int], float): ...
class Baz(list[str]): ...
Eggs = NamedTuple("Eggs", [("a", int), ("b", str)])
Spam = TypedDict("Spam", {"a": int, "b": str})
assert get_original_bases(Bar) == (Foo[int], float)
assert get_original_bases(Baz) == (list[str],)
assert get_original_bases(Eggs) == (NamedTuple,)
assert get_original_bases(Spam) == (TypedDict,)
assert get_original_bases(int) == (object,)
"""
try:
return cls.__orig_bases__
except AttributeError:
try:
return cls.__bases__
except AttributeError:
raise TypeError(
f'Expected an instance of type, not {type(cls).__name__!r}'
) from None
class DynamicClassAttribute:
"""Route attribute access on a class to __getattr__.

View File

@ -0,0 +1,2 @@
Implement :func:`types.get_original_bases` to provide further introspection
for types.