Skip to content

Commit b2022dd

Browse files
AlexWaygoodJelleZijlstra
authored andcommitted
Add a backport of types.get_original_bases (python#154)
Co-authored-by: Jelle Zijlstra <jelle.zijlstra@gmail.com>
1 parent 4c939ae commit b2022dd

4 files changed

Lines changed: 141 additions & 1 deletion

File tree

‎CHANGELOG.md‎

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

‎README.md‎

Lines changed: 8 additions & 0 deletions
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‎

Lines changed: 86 additions & 1 deletion
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
@@ -4284,5 +4284,90 @@ def __buffer__(self, flags: int) -> memoryview:
42844284
self.assertIsSubclass(MySubclassedBuffer, Buffer)
42854285

42864286

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

‎src/typing_extensions.py‎

Lines changed: 37 additions & 0 deletions
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)