Skip to content

Commit 48b6855

Browse files
Add a backport of types.get_original_bases (#154)
Co-authored-by: Jelle Zijlstra <[email protected]>
1 parent 0273a6e commit 48b6855

File tree

4 files changed

+141
-1
lines changed

4 files changed

+141
-1
lines changed

CHANGELOG.md

+10
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,16 @@
5252
- Add `__orig_bases__` to non-generic TypedDicts, call-based TypedDicts, and
5353
call-based NamedTuples. Other TypedDicts and NamedTuples already had the attribute.
5454
Patch by Adrian Garcia Badaracco.
55+
- Add `typing_extensions.get_original_bases`, a backport of
56+
[`types.get_original_bases`](https://docs.python.org/3.12/library/types.html#types.get_original_bases),
57+
introduced in Python 3.12 (CPython PR
58+
https://github.com/python/cpython/pull/101827, originally by James
59+
Hilton-Balfe). Patch by Alex Waygood.
60+
61+
This function should always produce correct results when called on classes
62+
constructed using features from `typing_extensions`. However, it may
63+
produce incorrect results when called on some `NamedTuple` or `TypedDict`
64+
classes that use `typing.{NamedTuple,TypedDict}` on Python <=3.11.
5565
- Constructing a call-based `TypedDict` using keyword arguments for the fields
5666
now causes a `DeprecationWarning` to be emitted. This matches the behaviour
5767
of `typing.TypedDict` on 3.11 and 3.12.

README.md

+8
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,14 @@ This module currently contains the following:
4343

4444
- `override` (equivalent to `typing.override`; see [PEP 698](https://peps.python.org/pep-0698/))
4545
- `Buffer` (equivalent to `collections.abc.Buffer`; see [PEP 688](https://peps.python.org/pep-0688/))
46+
- `get_original_bases` (equivalent to
47+
[`types.get_original_bases`](https://docs.python.org/3.12/library/types.html#types.get_original_bases)
48+
on 3.12+).
49+
50+
This function should always produce correct results when called on classes
51+
constructed using features from `typing_extensions`. However, it may
52+
produce incorrect results when called on some `NamedTuple` or `TypedDict`
53+
classes that use `typing.{NamedTuple,TypedDict}` on Python <=3.11.
4654

4755
- In `typing` since Python 3.11
4856

src/test_typing_extensions.py

+86-1
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
from typing_extensions import Awaitable, AsyncIterator, AsyncContextManager, Required, NotRequired
3232
from typing_extensions import Protocol, runtime, runtime_checkable, Annotated, final, is_typeddict
3333
from typing_extensions import TypeVarTuple, Unpack, dataclass_transform, reveal_type, Never, assert_never, LiteralString
34-
from typing_extensions import assert_type, get_type_hints, get_origin, get_args
34+
from typing_extensions import assert_type, get_type_hints, get_origin, get_args, get_original_bases
3535
from typing_extensions import clear_overloads, get_overloads, overload
3636
from typing_extensions import NamedTuple
3737
from typing_extensions import override, deprecated, Buffer
@@ -4286,5 +4286,90 @@ def __buffer__(self, flags: int) -> memoryview:
42864286
self.assertIsSubclass(MySubclassedBuffer, Buffer)
42874287

42884288

4289+
class GetOriginalBasesTests(BaseTestCase):
4290+
def test_basics(self):
4291+
T = TypeVar('T')
4292+
class A: pass
4293+
class B(Generic[T]): pass
4294+
class C(B[int]): pass
4295+
class D(B[str], float): pass
4296+
self.assertEqual(get_original_bases(A), (object,))
4297+
self.assertEqual(get_original_bases(B), (Generic[T],))
4298+
self.assertEqual(get_original_bases(C), (B[int],))
4299+
self.assertEqual(get_original_bases(int), (object,))
4300+
self.assertEqual(get_original_bases(D), (B[str], float))
4301+
4302+
with self.assertRaisesRegex(TypeError, "Expected an instance of type"):
4303+
get_original_bases(object())
4304+
4305+
@skipUnless(TYPING_3_9_0, "PEP 585 is yet to be")
4306+
def test_builtin_generics(self):
4307+
class E(list[T]): pass
4308+
class F(list[int]): pass
4309+
4310+
self.assertEqual(get_original_bases(E), (list[T],))
4311+
self.assertEqual(get_original_bases(F), (list[int],))
4312+
4313+
def test_namedtuples(self):
4314+
# On 3.12, this should work well with typing.NamedTuple and typing_extensions.NamedTuple
4315+
# On lower versions, it will only work fully with typing_extensions.NamedTuple
4316+
if sys.version_info >= (3, 12):
4317+
namedtuple_classes = (typing.NamedTuple, typing_extensions.NamedTuple)
4318+
else:
4319+
namedtuple_classes = (typing_extensions.NamedTuple,)
4320+
4321+
for NamedTuple in namedtuple_classes: # noqa: F402
4322+
with self.subTest(cls=NamedTuple):
4323+
class ClassBasedNamedTuple(NamedTuple):
4324+
x: int
4325+
4326+
class GenericNamedTuple(NamedTuple, Generic[T]):
4327+
x: T
4328+
4329+
CallBasedNamedTuple = NamedTuple("CallBasedNamedTuple", [("x", int)])
4330+
4331+
self.assertIs(
4332+
get_original_bases(ClassBasedNamedTuple)[0], NamedTuple
4333+
)
4334+
self.assertEqual(
4335+
get_original_bases(GenericNamedTuple),
4336+
(NamedTuple, Generic[T])
4337+
)
4338+
self.assertIs(
4339+
get_original_bases(CallBasedNamedTuple)[0], NamedTuple
4340+
)
4341+
4342+
def test_typeddicts(self):
4343+
# On 3.12, this should work well with typing.TypedDict and typing_extensions.TypedDict
4344+
# On lower versions, it will only work fully with typing_extensions.TypedDict
4345+
if sys.version_info >= (3, 12):
4346+
typeddict_classes = (typing.TypedDict, typing_extensions.TypedDict)
4347+
else:
4348+
typeddict_classes = (typing_extensions.TypedDict,)
4349+
4350+
for TypedDict in typeddict_classes: # noqa: F402
4351+
with self.subTest(cls=TypedDict):
4352+
class ClassBasedTypedDict(TypedDict):
4353+
x: int
4354+
4355+
class GenericTypedDict(TypedDict, Generic[T]):
4356+
x: T
4357+
4358+
CallBasedTypedDict = TypedDict("CallBasedTypedDict", {"x": int})
4359+
4360+
self.assertIs(
4361+
get_original_bases(ClassBasedTypedDict)[0],
4362+
TypedDict
4363+
)
4364+
self.assertEqual(
4365+
get_original_bases(GenericTypedDict),
4366+
(TypedDict, Generic[T])
4367+
)
4368+
self.assertIs(
4369+
get_original_bases(CallBasedTypedDict)[0],
4370+
TypedDict
4371+
)
4372+
4373+
42894374
if __name__ == '__main__':
42904375
main()

src/typing_extensions.py

+37
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@
5959
'final',
6060
'get_args',
6161
'get_origin',
62+
'get_original_bases',
6263
'get_type_hints',
6364
'IntVar',
6465
'is_typeddict',
@@ -2440,3 +2441,39 @@ class Buffer(abc.ABC):
24402441
Buffer.register(memoryview)
24412442
Buffer.register(bytearray)
24422443
Buffer.register(bytes)
2444+
2445+
2446+
# Backport of types.get_original_bases, available on 3.12+ in CPython
2447+
if hasattr(_types, "get_original_bases"):
2448+
get_original_bases = _types.get_original_bases
2449+
else:
2450+
def get_original_bases(__cls):
2451+
"""Return the class's "original" bases prior to modification by `__mro_entries__`.
2452+
2453+
Examples::
2454+
2455+
from typing import TypeVar, Generic
2456+
from typing_extensions import NamedTuple, TypedDict
2457+
2458+
T = TypeVar("T")
2459+
class Foo(Generic[T]): ...
2460+
class Bar(Foo[int], float): ...
2461+
class Baz(list[str]): ...
2462+
Eggs = NamedTuple("Eggs", [("a", int), ("b", str)])
2463+
Spam = TypedDict("Spam", {"a": int, "b": str})
2464+
2465+
assert get_original_bases(Bar) == (Foo[int], float)
2466+
assert get_original_bases(Baz) == (list[str],)
2467+
assert get_original_bases(Eggs) == (NamedTuple,)
2468+
assert get_original_bases(Spam) == (TypedDict,)
2469+
assert get_original_bases(int) == (object,)
2470+
"""
2471+
try:
2472+
return __cls.__orig_bases__
2473+
except AttributeError:
2474+
try:
2475+
return __cls.__bases__
2476+
except AttributeError:
2477+
raise TypeError(
2478+
f'Expected an instance of type, not {type(__cls).__name__!r}'
2479+
) from None

0 commit comments

Comments
 (0)