summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJames Hilton-Balfe <gobot1234yt@gmail.com>2023-04-23 19:24:30 (GMT)
committerGitHub <noreply@github.com>2023-04-23 19:24:30 (GMT)
commit730bbddfdf610343a2e132b0312d12254c3c73d6 (patch)
tree2c4b223f290866cc428d3faa0a7cf97c75128cb8
parent05b3ce7339b9ce44eec728e88e80ba1f125436ed (diff)
downloadcpython-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.rst40
-rw-r--r--Doc/reference/datamodel.rst4
-rw-r--r--Doc/whatsnew/3.12.rst7
-rw-r--r--Lib/test/test_types.py61
-rw-r--r--Lib/types.py32
-rw-r--r--Misc/NEWS.d/next/Library/2023-02-11-15-01-32.gh-issue-101688.kwXmfM.rst2
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.