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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy