Skip to content

Commit 730bbdd

Browse files
gh-101688: Implement types.get_original_bases (#101827)
Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
1 parent 05b3ce7 commit 730bbdd

File tree

6 files changed

+146
-0
lines changed

6 files changed

+146
-0
lines changed

Doc/library/types.rst

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,46 @@ Dynamic Type Creation
8282

8383
.. versionadded:: 3.7
8484

85+
.. function:: get_original_bases(cls, /)
86+
87+
Return the tuple of objects originally given as the bases of *cls* before
88+
the :meth:`~object.__mro_entries__` method has been called on any bases
89+
(following the mechanisms laid out in :pep:`560`). This is useful for
90+
introspecting :ref:`Generics <user-defined-generics>`.
91+
92+
For classes that have an ``__orig_bases__`` attribute, this
93+
function returns the value of ``cls.__orig_bases__``.
94+
For classes without the ``__orig_bases__`` attribute, ``cls.__bases__`` is
95+
returned.
96+
97+
Examples::
98+
99+
from typing import TypeVar, Generic, NamedTuple, TypedDict
100+
101+
T = TypeVar("T")
102+
class Foo(Generic[T]): ...
103+
class Bar(Foo[int], float): ...
104+
class Baz(list[str]): ...
105+
Eggs = NamedTuple("Eggs", [("a", int), ("b", str)])
106+
Spam = TypedDict("Spam", {"a": int, "b": str})
107+
108+
assert Bar.__bases__ == (Foo, float)
109+
assert get_original_bases(Bar) == (Foo[int], float)
110+
111+
assert Baz.__bases__ == (list,)
112+
assert get_original_bases(Baz) == (list[str],)
113+
114+
assert Eggs.__bases__ == (tuple,)
115+
assert get_original_bases(Eggs) == (NamedTuple,)
116+
117+
assert Spam.__bases__ == (dict,)
118+
assert get_original_bases(Spam) == (TypedDict,)
119+
120+
assert int.__bases__ == (object,)
121+
assert get_original_bases(int) == (object,)
122+
123+
.. versionadded:: 3.12
124+
85125
.. seealso::
86126

87127
:pep:`560` - Core support for typing module and generic types

Doc/reference/datamodel.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2102,6 +2102,10 @@ Resolving MRO entries
21022102
:func:`types.resolve_bases`
21032103
Dynamically resolve bases that are not instances of :class:`type`.
21042104

2105+
:func:`types.get_original_bases`
2106+
Retrieve a class's "original bases" prior to modifications by
2107+
:meth:`~object.__mro_entries__`.
2108+
21052109
:pep:`560`
21062110
Core support for typing module and generic types.
21072111

Doc/whatsnew/3.12.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -407,6 +407,13 @@ threading
407407
profiling functions in all running threads in addition to the calling one.
408408
(Contributed by Pablo Galindo in :gh:`93503`.)
409409

410+
types
411+
-----
412+
413+
* Add :func:`types.get_original_bases` to allow for further introspection of
414+
:ref:`user-defined-generics` when subclassed. (Contributed by
415+
James Hilton-Balfe and Alex Waygood in :gh:`101827`.)
416+
410417
unicodedata
411418
-----------
412419

Lib/test/test_types.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1360,6 +1360,67 @@ class C: pass
13601360
D = types.new_class('D', (A(), C, B()), {})
13611361
self.assertEqual(D.__bases__, (A1, A2, A3, C, B1, B2))
13621362

1363+
def test_get_original_bases(self):
1364+
T = typing.TypeVar('T')
1365+
class A: pass
1366+
class B(typing.Generic[T]): pass
1367+
class C(B[int]): pass
1368+
class D(B[str], float): pass
1369+
self.assertEqual(types.get_original_bases(A), (object,))
1370+
self.assertEqual(types.get_original_bases(B), (typing.Generic[T],))
1371+
self.assertEqual(types.get_original_bases(C), (B[int],))
1372+
self.assertEqual(types.get_original_bases(int), (object,))
1373+
self.assertEqual(types.get_original_bases(D), (B[str], float))
1374+
1375+
class E(list[T]): pass
1376+
class F(list[int]): pass
1377+
1378+
self.assertEqual(types.get_original_bases(E), (list[T],))
1379+
self.assertEqual(types.get_original_bases(F), (list[int],))
1380+
1381+
class ClassBasedNamedTuple(typing.NamedTuple):
1382+
x: int
1383+
1384+
class GenericNamedTuple(typing.NamedTuple, typing.Generic[T]):
1385+
x: T
1386+
1387+
CallBasedNamedTuple = typing.NamedTuple("CallBasedNamedTuple", [("x", int)])
1388+
1389+
self.assertIs(
1390+
types.get_original_bases(ClassBasedNamedTuple)[0], typing.NamedTuple
1391+
)
1392+
self.assertEqual(
1393+
types.get_original_bases(GenericNamedTuple),
1394+
(typing.NamedTuple, typing.Generic[T])
1395+
)
1396+
self.assertIs(
1397+
types.get_original_bases(CallBasedNamedTuple)[0], typing.NamedTuple
1398+
)
1399+
1400+
class ClassBasedTypedDict(typing.TypedDict):
1401+
x: int
1402+
1403+
class GenericTypedDict(typing.TypedDict, typing.Generic[T]):
1404+
x: T
1405+
1406+
CallBasedTypedDict = typing.TypedDict("CallBasedTypedDict", {"x": int})
1407+
1408+
self.assertIs(
1409+
types.get_original_bases(ClassBasedTypedDict)[0],
1410+
typing.TypedDict
1411+
)
1412+
self.assertEqual(
1413+
types.get_original_bases(GenericTypedDict),
1414+
(typing.TypedDict, typing.Generic[T])
1415+
)
1416+
self.assertIs(
1417+
types.get_original_bases(CallBasedTypedDict)[0],
1418+
typing.TypedDict
1419+
)
1420+
1421+
with self.assertRaisesRegex(TypeError, "Expected an instance of type"):
1422+
types.get_original_bases(object())
1423+
13631424
# Many of the following tests are derived from test_descr.py
13641425
def test_prepare_class(self):
13651426
# Basic test of metaclass derivation

Lib/types.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,38 @@ def _calculate_meta(meta, bases):
143143
"of the metaclasses of all its bases")
144144
return winner
145145

146+
147+
def get_original_bases(cls, /):
148+
"""Return the class's "original" bases prior to modification by `__mro_entries__`.
149+
150+
Examples::
151+
152+
from typing import TypeVar, Generic, NamedTuple, TypedDict
153+
154+
T = TypeVar("T")
155+
class Foo(Generic[T]): ...
156+
class Bar(Foo[int], float): ...
157+
class Baz(list[str]): ...
158+
Eggs = NamedTuple("Eggs", [("a", int), ("b", str)])
159+
Spam = TypedDict("Spam", {"a": int, "b": str})
160+
161+
assert get_original_bases(Bar) == (Foo[int], float)
162+
assert get_original_bases(Baz) == (list[str],)
163+
assert get_original_bases(Eggs) == (NamedTuple,)
164+
assert get_original_bases(Spam) == (TypedDict,)
165+
assert get_original_bases(int) == (object,)
166+
"""
167+
try:
168+
return cls.__orig_bases__
169+
except AttributeError:
170+
try:
171+
return cls.__bases__
172+
except AttributeError:
173+
raise TypeError(
174+
f'Expected an instance of type, not {type(cls).__name__!r}'
175+
) from None
176+
177+
146178
class DynamicClassAttribute:
147179
"""Route attribute access on a class to __getattr__.
148180
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Implement :func:`types.get_original_bases` to provide further introspection
2+
for types.

0 commit comments

Comments
 (0)