From 775e9f489aea228c9f41454ab99aa18b11a33a2c Mon Sep 17 00:00:00 2001 From: AlexWaygood Date: Mon, 24 Apr 2023 11:45:38 -0600 Subject: [PATCH 1/4] Add a backport of `types.get_original_bases` --- CHANGELOG.md | 4 ++ src/test_typing_extensions.py | 69 ++++++++++++++++++++++++++++++++++- src/typing_extensions.py | 36 ++++++++++++++++++ 3 files changed, 108 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a3f62b0..3b9fab26 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -49,6 +49,10 @@ Patch by Alex Waygood. - Speedup `isinstance(3, typing_extensions.SupportsIndex)` by >10x on Python <3.12. Patch by Alex Waygood. +- Add `typing_extensions.get_original_bases`, a backport of + `types.get_original_bases`, introduced in Python 3.12 (CPython PR + https://github.com/python/cpython/pull/101827, originally by James + Hilton-Balfe). Patch by Alex Waygood. # Release 4.5.0 (February 14, 2023) diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index 7fa310ce..974a495b 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -31,7 +31,7 @@ from typing_extensions import Awaitable, AsyncIterator, AsyncContextManager, Required, NotRequired from typing_extensions import Protocol, runtime, runtime_checkable, Annotated, final, is_typeddict from typing_extensions import TypeVarTuple, Unpack, dataclass_transform, reveal_type, Never, assert_never, LiteralString -from typing_extensions import assert_type, get_type_hints, get_origin, get_args +from typing_extensions import assert_type, get_type_hints, get_origin, get_args, get_original_bases from typing_extensions import clear_overloads, get_overloads, overload from typing_extensions import NamedTuple from typing_extensions import override, deprecated, Buffer @@ -4153,5 +4153,72 @@ def __buffer__(self, flags: int) -> memoryview: self.assertIsSubclass(MySubclassedBuffer, Buffer) +class GetOriginalBasesTests(BaseTestCase): + def test_basics(self): + T = TypeVar('T') + class A: pass + class B(Generic[T]): pass + class C(B[int]): pass + class D(B[str], float): pass + self.assertEqual(get_original_bases(A), (object,)) + self.assertEqual(get_original_bases(B), (Generic[T],)) + self.assertEqual(get_original_bases(C), (B[int],)) + self.assertEqual(get_original_bases(int), (object,)) + self.assertEqual(get_original_bases(D), (B[str], float)) + + with self.assertRaisesRegex(TypeError, "Expected an instance of type"): + get_original_bases(object()) + + @skipUnless(TYPING_3_9_0, "PEP 585 is yet to be") + def test_builtin_generics(self): + class E(list[T]): pass + class F(list[int]): pass + + self.assertEqual(get_original_bases(E), (list[T],)) + self.assertEqual(get_original_bases(F), (list[int],)) + + def test_namedtuples(self): + class ClassBasedNamedTuple(NamedTuple): + x: int + + class GenericNamedTuple(NamedTuple, Generic[T]): + x: T + + CallBasedNamedTuple = NamedTuple("CallBasedNamedTuple", [("x", int)]) + + self.assertIs( + get_original_bases(ClassBasedNamedTuple)[0], NamedTuple + ) + self.assertEqual( + get_original_bases(GenericNamedTuple), + (NamedTuple, Generic[T]) + ) + self.assertIs( + get_original_bases(CallBasedNamedTuple)[0], NamedTuple + ) + + def test_typeddicts(self): + class ClassBasedTypedDict(TypedDict): + x: int + + class GenericTypedDict(TypedDict, Generic[T]): + x: T + + CallBasedTypedDict = TypedDict("CallBasedTypedDict", {"x": int}) + + self.assertIs( + get_original_bases(ClassBasedTypedDict)[0], + TypedDict + ) + self.assertEqual( + get_original_bases(GenericTypedDict), + (TypedDict, Generic[T]) + ) + self.assertIs( + get_original_bases(CallBasedTypedDict)[0], + TypedDict + ) + + if __name__ == '__main__': main() diff --git a/src/typing_extensions.py b/src/typing_extensions.py index b6b6bd49..9f5a8789 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -59,6 +59,7 @@ 'final', 'get_args', 'get_origin', + 'get_original_bases', 'get_type_hints', 'IntVar', 'is_typeddict', @@ -2423,3 +2424,38 @@ class Buffer(abc.ABC): Buffer.register(memoryview) Buffer.register(bytearray) Buffer.register(bytes) + + +# Backport of types.get_original_bases, available on 3.12+ in CPython +if hasattr(_types, "get_original_bases"): + get_original_bases = _types.get_original_bases +else: + 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 From bc0d0a0366095bcd146157b5593b2e79f83fdf7b Mon Sep 17 00:00:00 2001 From: AlexWaygood Date: Mon, 24 Apr 2023 13:57:46 -0600 Subject: [PATCH 2/4] More docs & tests --- CHANGELOG.md | 14 ++++-- README.md | 8 ++++ src/test_typing_extensions.py | 82 +++++++++++++++++++++-------------- 3 files changed, 68 insertions(+), 36 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d82229b..b8d8b628 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -49,13 +49,19 @@ Patch by Alex Waygood. - Speedup `isinstance(3, typing_extensions.SupportsIndex)` by >10x on Python <3.12. Patch by Alex Waygood. -- Add `typing_extensions.get_original_bases`, a backport of - `types.get_original_bases`, introduced in Python 3.12 (CPython PR - https://github.com/python/cpython/pull/101827, originally by James - Hilton-Balfe). Patch by Alex Waygood. - Add `__orig_bases__` to non-generic TypedDicts, call-based TypedDicts, and call-based NamedTuples. Other TypedDicts and NamedTuples already had the attribute. Patch by Adrian Garcia Badaracco. +- Add `typing_extensions.get_original_bases`, a backport of + [`types.get_original_bases`](https://docs.python.org/3.12/library/types.html#types.get_original_bases), + introduced in Python 3.12 (CPython PR + https://github.com/python/cpython/pull/101827, originally by James + Hilton-Balfe). Patch by Alex Waygood. + + This function should always produce correct results when called on classes + constructed using features from `typing_extensions`. However, it may + produce incorrect results when called on some `NamedTuple` or `TypedDict` + classes that use `typing.{NamedTuple,TypedDict}` on Python <=3.11. - Constructing a call-based `TypedDict` using keyword arguments for the fields now causes a `DeprecationWarning` to be emitted. This matches the behaviour of `typing.TypedDict` on 3.11 and 3.12. diff --git a/README.md b/README.md index 59d5d3d0..94d157f4 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,14 @@ This module currently contains the following: - `override` (equivalent to `typing.override`; see [PEP 698](https://peps.python.org/pep-0698/)) - `Buffer` (equivalent to `collections.abc.Buffer`; see [PEP 688](https://peps.python.org/pep-0688/)) + - `get_original_bases` (equivalent to + [`types.get_original_bases`](https://docs.python.org/3.12/library/types.html#types.get_original_bases) + on 3.12+). + + This function should always produce correct results when called on classes + constructed using features from `typing_extensions`. However, it may + produce incorrect results when called on some `NamedTuple` or `TypedDict` + classes that use `typing.{NamedTuple,TypedDict}` on Python <=3.11. - In `typing` since Python 3.11 diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index 36be0f4a..b77bc745 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -4311,46 +4311,64 @@ class F(list[int]): pass self.assertEqual(get_original_bases(F), (list[int],)) def test_namedtuples(self): - class ClassBasedNamedTuple(NamedTuple): - x: int + # On 3.12, this should work well with typing.NamedTuple and typing_extensions.NamedTuple + # On lower versions, it will only work fully with typing_extensions.NamedTuple + if sys.version_info >= (3, 12): + namedtuple_classes = (typing.NamedTuple, typing_extensions.NamedTuple) + else: + namedtuple_classes = (typing_extensions.NamedTuple,) - class GenericNamedTuple(NamedTuple, Generic[T]): - x: T + for NamedTuple in namedtuple_classes: + with self.subTest(cls=NamedTuple): + class ClassBasedNamedTuple(NamedTuple): + x: int - CallBasedNamedTuple = NamedTuple("CallBasedNamedTuple", [("x", int)]) + class GenericNamedTuple(NamedTuple, Generic[T]): + x: T - self.assertIs( - get_original_bases(ClassBasedNamedTuple)[0], NamedTuple - ) - self.assertEqual( - get_original_bases(GenericNamedTuple), - (NamedTuple, Generic[T]) - ) - self.assertIs( - get_original_bases(CallBasedNamedTuple)[0], NamedTuple - ) + CallBasedNamedTuple = NamedTuple("CallBasedNamedTuple", [("x", int)]) + + self.assertIs( + get_original_bases(ClassBasedNamedTuple)[0], NamedTuple + ) + self.assertEqual( + get_original_bases(GenericNamedTuple), + (NamedTuple, Generic[T]) + ) + self.assertIs( + get_original_bases(CallBasedNamedTuple)[0], NamedTuple + ) def test_typeddicts(self): - class ClassBasedTypedDict(TypedDict): - x: int + # On 3.12, this should work well with typing.TypedDict and typing_extensions.TypedDict + # On lower versions, it will only work fully with typing_extensions.TypedDict + if sys.version_info >= (3, 12): + typeddict_classes = (typing.TypedDict, typing_extensions.TypedDict) + else: + typeddict_classes = (typing_extensions.TypedDict,) - class GenericTypedDict(TypedDict, Generic[T]): - x: T + for TypedDict in typeddict_classes: + with self.subTest(cls=TypedDict): + class ClassBasedTypedDict(TypedDict): + x: int - CallBasedTypedDict = TypedDict("CallBasedTypedDict", {"x": int}) + class GenericTypedDict(TypedDict, Generic[T]): + x: T - self.assertIs( - get_original_bases(ClassBasedTypedDict)[0], - TypedDict - ) - self.assertEqual( - get_original_bases(GenericTypedDict), - (TypedDict, Generic[T]) - ) - self.assertIs( - get_original_bases(CallBasedTypedDict)[0], - TypedDict - ) + CallBasedTypedDict = TypedDict("CallBasedTypedDict", {"x": int}) + + self.assertIs( + get_original_bases(ClassBasedTypedDict)[0], + TypedDict + ) + self.assertEqual( + get_original_bases(GenericTypedDict), + (TypedDict, Generic[T]) + ) + self.assertIs( + get_original_bases(CallBasedTypedDict)[0], + TypedDict + ) if __name__ == '__main__': From 1375e7cf5a0316d3d8388188936de25d8769cbd5 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Mon, 24 Apr 2023 13:58:35 -0600 Subject: [PATCH 3/4] Update src/typing_extensions.py --- src/typing_extensions.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/typing_extensions.py b/src/typing_extensions.py index 28c69152..2ee36eb3 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -2452,7 +2452,8 @@ def get_original_bases(__cls): Examples:: - from typing import TypeVar, Generic, NamedTuple, TypedDict + from typing import TypeVar, Generic + from typing_extensions import NamedTuple, TypedDict T = TypeVar("T") class Foo(Generic[T]): ... From b1f997d713f745dfb3ca4a08a71e701ffd1e1895 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Mon, 24 Apr 2023 14:02:52 -0600 Subject: [PATCH 4/4] Apply suggestions from code review --- src/test_typing_extensions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index b77bc745..7f3c0ef2 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -4318,7 +4318,7 @@ def test_namedtuples(self): else: namedtuple_classes = (typing_extensions.NamedTuple,) - for NamedTuple in namedtuple_classes: + for NamedTuple in namedtuple_classes: # noqa: F402 with self.subTest(cls=NamedTuple): class ClassBasedNamedTuple(NamedTuple): x: int @@ -4347,7 +4347,7 @@ def test_typeddicts(self): else: typeddict_classes = (typing_extensions.TypedDict,) - for TypedDict in typeddict_classes: + for TypedDict in typeddict_classes: # noqa: F402 with self.subTest(cls=TypedDict): class ClassBasedTypedDict(TypedDict): x: int