]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-101688: Implement types.get_original_bases (#101827)
authorJames Hilton-Balfe <gobot1234yt@gmail.com>
Sun, 23 Apr 2023 19:24:30 +0000 (20:24 +0100)
committerGitHub <noreply@github.com>
Sun, 23 Apr 2023 19:24:30 +0000 (20:24 +0100)
Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
Doc/library/types.rst
Doc/reference/datamodel.rst
Doc/whatsnew/3.12.rst
Lib/test/test_types.py
Lib/types.py
Misc/NEWS.d/next/Library/2023-02-11-15-01-32.gh-issue-101688.kwXmfM.rst [new file with mode: 0644]

index 27b9846325914d9f5c08fb998e0b8b89f5fee70d..54887f4c51983aaa369a68040a3ac14fa9b1337a 100644 (file)
@@ -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
index 9f91ade35e50dcb67ba5b9ab09c5af85553f6457..55431f1951e50d692f54c7450a091997583174ff 100644 (file)
@@ -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.
 
index b98b7151a321ea28ac3cc1be537a185423194aaa..d16c496eb9103f8891e2dd10673d5f0d8853ce91 100644 (file)
@@ -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
 -----------
 
index af095632a36fcb73a8cd1914ca7da229180c6b17..9fe5812a14e15d8f5c0ffe74d7f96b68a6b7c019 100644 (file)
@@ -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
index aa8a1c847223994174d9de50b5b10f3fb8f7ba72..6110e6e1de7249eb01b50ea55ea93e9479b918cb 100644 (file)
@@ -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 (file)
index 0000000..6df6946
--- /dev/null
@@ -0,0 +1,2 @@
+Implement :func:`types.get_original_bases` to provide further introspection
+for types.