diff options
author | James Hilton-Balfe <gobot1234yt@gmail.com> | 2023-04-23 19:24:30 (GMT) |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-04-23 19:24:30 (GMT) |
commit | 730bbddfdf610343a2e132b0312d12254c3c73d6 (patch) | |
tree | 2c4b223f290866cc428d3faa0a7cf97c75128cb8 | |
parent | 05b3ce7339b9ce44eec728e88e80ba1f125436ed (diff) | |
download | cpython-730bbddfdf610343a2e132b0312d12254c3c73d6.zip cpython-730bbddfdf610343a2e132b0312d12254c3c73d6.tar.gz cpython-730bbddfdf610343a2e132b0312d12254c3c73d6.tar.bz2 |
gh-101688: Implement types.get_original_bases (#101827)
Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
-rw-r--r-- | Doc/library/types.rst | 40 | ||||
-rw-r--r-- | Doc/reference/datamodel.rst | 4 | ||||
-rw-r--r-- | Doc/whatsnew/3.12.rst | 7 | ||||
-rw-r--r-- | Lib/test/test_types.py | 61 | ||||
-rw-r--r-- | Lib/types.py | 32 | ||||
-rw-r--r-- | Misc/NEWS.d/next/Library/2023-02-11-15-01-32.gh-issue-101688.kwXmfM.rst | 2 |
6 files changed, 146 insertions, 0 deletions
diff --git a/Doc/library/types.rst b/Doc/library/types.rst index 27b9846..54887f4 100644 --- a/Doc/library/types.rst +++ b/Doc/library/types.rst @@ -82,6 +82,46 @@ Dynamic Type Creation .. versionadded:: 3.7 +.. function:: get_original_bases(cls, /) + + Return the tuple of objects originally given as the bases of *cls* before + the :meth:`~object.__mro_entries__` method has been called on any bases + (following the mechanisms laid out in :pep:`560`). This is useful for + introspecting :ref:`Generics <user-defined-generics>`. + + For classes that have an ``__orig_bases__`` attribute, this + function returns the value of ``cls.__orig_bases__``. + For classes without the ``__orig_bases__`` attribute, ``cls.__bases__`` is + returned. + + 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 Bar.__bases__ == (Foo, float) + assert get_original_bases(Bar) == (Foo[int], float) + + assert Baz.__bases__ == (list,) + assert get_original_bases(Baz) == (list[str],) + + assert Eggs.__bases__ == (tuple,) + assert get_original_bases(Eggs) == (NamedTuple,) + + assert Spam.__bases__ == (dict,) + assert get_original_bases(Spam) == (TypedDict,) + + assert int.__bases__ == (object,) + assert get_original_bases(int) == (object,) + + .. versionadded:: 3.12 + .. seealso:: :pep:`560` - Core support for typing module and generic types diff --git a/Doc/reference/datamodel.rst b/Doc/reference/datamodel.rst index 9f91ade..55431f1 100644 --- a/Doc/reference/datamodel.rst +++ b/Doc/reference/datamodel.rst @@ -2102,6 +2102,10 @@ Resolving MRO entries :func:`types.resolve_bases` Dynamically resolve bases that are not instances of :class:`type`. + :func:`types.get_original_bases` + Retrieve a class's "original bases" prior to modifications by + :meth:`~object.__mro_entries__`. + :pep:`560` Core support for typing module and generic types. diff --git a/Doc/whatsnew/3.12.rst b/Doc/whatsnew/3.12.rst index b98b715..d16c496 100644 --- a/Doc/whatsnew/3.12.rst +++ b/Doc/whatsnew/3.12.rst @@ -407,6 +407,13 @@ threading profiling functions in all running threads in addition to the calling one. (Contributed by Pablo Galindo in :gh:`93503`.) +types +----- + +* Add :func:`types.get_original_bases` to allow for further introspection of + :ref:`user-defined-generics` when subclassed. (Contributed by + James Hilton-Balfe and Alex Waygood in :gh:`101827`.) + unicodedata ----------- diff --git a/Lib/test/test_types.py b/Lib/test/test_types.py index af09563..9fe5812 100644 --- a/Lib/test/test_types.py +++ b/Lib/test/test_types.py @@ -1360,6 +1360,67 @@ class ClassCreationTests(unittest.TestCase): D = types.new_class('D', (A(), C, B()), {}) self.assertEqual(D.__bases__, (A1, A2, A3, C, B1, B2)) + def test_get_original_bases(self): + T = typing.TypeVar('T') + class A: pass + class B(typing.Generic[T]): pass + class C(B[int]): pass + class D(B[str], float): pass + self.assertEqual(types.get_original_bases(A), (object,)) + self.assertEqual(types.get_original_bases(B), (typing.Generic[T],)) + self.assertEqual(types.get_original_bases(C), (B[int],)) + self.assertEqual(types.get_original_bases(int), (object,)) + self.assertEqual(types.get_original_bases(D), (B[str], float)) + + class E(list[T]): pass + class F(list[int]): pass + + self.assertEqual(types.get_original_bases(E), (list[T],)) + self.assertEqual(types.get_original_bases(F), (list[int],)) + + class ClassBasedNamedTuple(typing.NamedTuple): + x: int + + class GenericNamedTuple(typing.NamedTuple, typing.Generic[T]): + x: T + + CallBasedNamedTuple = typing.NamedTuple("CallBasedNamedTuple", [("x", int)]) + + self.assertIs( + types.get_original_bases(ClassBasedNamedTuple)[0], typing.NamedTuple + ) + self.assertEqual( + types.get_original_bases(GenericNamedTuple), + (typing.NamedTuple, typing.Generic[T]) + ) + self.assertIs( + types.get_original_bases(CallBasedNamedTuple)[0], typing.NamedTuple + ) + + class ClassBasedTypedDict(typing.TypedDict): + x: int + + class GenericTypedDict(typing.TypedDict, typing.Generic[T]): + x: T + + CallBasedTypedDict = typing.TypedDict("CallBasedTypedDict", {"x": int}) + + self.assertIs( + types.get_original_bases(ClassBasedTypedDict)[0], + typing.TypedDict + ) + self.assertEqual( + types.get_original_bases(GenericTypedDict), + (typing.TypedDict, typing.Generic[T]) + ) + self.assertIs( + types.get_original_bases(CallBasedTypedDict)[0], + typing.TypedDict + ) + + with self.assertRaisesRegex(TypeError, "Expected an instance of type"): + types.get_original_bases(object()) + # Many of the following tests are derived from test_descr.py def test_prepare_class(self): # Basic test of metaclass derivation diff --git a/Lib/types.py b/Lib/types.py index aa8a1c8..6110e6e 100644 --- a/Lib/types.py +++ b/Lib/types.py @@ -143,6 +143,38 @@ def _calculate_meta(meta, bases): "of the metaclasses of all its bases") return winner + +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 + + class DynamicClassAttribute: """Route attribute access on a class to __getattr__. diff --git a/Misc/NEWS.d/next/Library/2023-02-11-15-01-32.gh-issue-101688.kwXmfM.rst b/Misc/NEWS.d/next/Library/2023-02-11-15-01-32.gh-issue-101688.kwXmfM.rst new file mode 100644 index 0000000..6df6946 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2023-02-11-15-01-32.gh-issue-101688.kwXmfM.rst @@ -0,0 +1,2 @@ +Implement :func:`types.get_original_bases` to provide further introspection +for types. |