[Python-checkins] bpo-46998: Allow subclassing Any at runtime (GH-31841)

JelleZijlstra webhook-mailer at python.org
Mon Apr 4 22:35:39 EDT 2022


https://github.com/python/cpython/commit/5a4973e29f2f5c4ee8c086f40325786c62381540
commit: 5a4973e29f2f5c4ee8c086f40325786c62381540
branch: main
author: Shantanu <12621235+hauntsaninja at users.noreply.github.com>
committer: JelleZijlstra <jelle.zijlstra at gmail.com>
date: 2022-04-04T19:35:29-07:00
summary:

bpo-46998: Allow subclassing Any at runtime (GH-31841)

Co-authored-by: Jelle Zijlstra <jelle.zijlstra at gmail.com>

files:
A Misc/NEWS.d/next/Library/2022-03-13-08-52-58.bpo-46998.cHh-9O.rst
M Doc/library/typing.rst
M Lib/test/test_functools.py
M Lib/test/test_pydoc.py
M Lib/test/test_typing.py
M Lib/typing.py

diff --git a/Doc/library/typing.rst b/Doc/library/typing.rst
index 37c17c429fa47..0a4e848c67736 100644
--- a/Doc/library/typing.rst
+++ b/Doc/library/typing.rst
@@ -580,6 +580,11 @@ These can be used as types in annotations and do not support ``[]``.
    * Every type is compatible with :data:`Any`.
    * :data:`Any` is compatible with every type.
 
+    .. versionchanged:: 3.11
+       :data:`Any` can now be used as a base class. This can be useful for
+       avoiding type checker errors with classes that can duck type anywhere or
+       are highly dynamic.
+
 .. data:: Never
 
    The `bottom type <https://en.wikipedia.org/wiki/Bottom_type>`_,
diff --git a/Lib/test/test_functools.py b/Lib/test/test_functools.py
index abbd50a47f395..82e73f46a3fba 100644
--- a/Lib/test/test_functools.py
+++ b/Lib/test/test_functools.py
@@ -2802,8 +2802,6 @@ def f(arg):
             f.register(list[int] | str, lambda arg: "types.UnionTypes(types.GenericAlias)")
         with self.assertRaisesRegex(TypeError, "Invalid first argument to "):
             f.register(typing.List[float] | bytes, lambda arg: "typing.Union[typing.GenericAlias]")
-        with self.assertRaisesRegex(TypeError, "Invalid first argument to "):
-            f.register(typing.Any, lambda arg: "typing.Any")
 
         self.assertEqual(f([1]), "default")
         self.assertEqual(f([1.0]), "default")
@@ -2823,8 +2821,6 @@ def f(arg):
             f.register(list[int] | str)
         with self.assertRaisesRegex(TypeError, "Invalid first argument to "):
             f.register(typing.List[int] | str)
-        with self.assertRaisesRegex(TypeError, "Invalid first argument to "):
-            f.register(typing.Any)
 
     def test_register_genericalias_annotation(self):
         @functools.singledispatch
@@ -2847,10 +2843,6 @@ def _(arg: list[int] | str):
             @f.register
             def _(arg: typing.List[float] | bytes):
                 return "typing.Union[typing.GenericAlias]"
-        with self.assertRaisesRegex(TypeError, "Invalid annotation for 'arg'"):
-            @f.register
-            def _(arg: typing.Any):
-                return "typing.Any"
 
         self.assertEqual(f([1]), "default")
         self.assertEqual(f([1.0]), "default")
diff --git a/Lib/test/test_pydoc.py b/Lib/test/test_pydoc.py
index 9c900c3e8ee0a..13c77b6fa6822 100644
--- a/Lib/test/test_pydoc.py
+++ b/Lib/test/test_pydoc.py
@@ -1066,14 +1066,14 @@ def test_union_type(self):
         self.assertIn(types.UnionType.__doc__.strip().splitlines()[0], doc)
 
     def test_special_form(self):
-        self.assertEqual(pydoc.describe(typing.Any), '_SpecialForm')
-        doc = pydoc.render_doc(typing.Any, renderer=pydoc.plaintext)
+        self.assertEqual(pydoc.describe(typing.NoReturn), '_SpecialForm')
+        doc = pydoc.render_doc(typing.NoReturn, renderer=pydoc.plaintext)
         self.assertIn('_SpecialForm in module typing', doc)
-        if typing.Any.__doc__:
-            self.assertIn('Any = typing.Any', doc)
-            self.assertIn(typing.Any.__doc__.strip().splitlines()[0], doc)
+        if typing.NoReturn.__doc__:
+            self.assertIn('NoReturn = typing.NoReturn', doc)
+            self.assertIn(typing.NoReturn.__doc__.strip().splitlines()[0], doc)
         else:
-            self.assertIn('Any = class _SpecialForm(_Final)', doc)
+            self.assertIn('NoReturn = class _SpecialForm(_Final)', doc)
 
     def test_typing_pydoc(self):
         def foo(data: typing.List[typing.Any],
diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py
index 0e28655296d14..041b6ad9ed6dd 100644
--- a/Lib/test/test_typing.py
+++ b/Lib/test/test_typing.py
@@ -89,12 +89,6 @@ def test_any_instance_type_error(self):
         with self.assertRaises(TypeError):
             isinstance(42, Any)
 
-    def test_any_subclass_type_error(self):
-        with self.assertRaises(TypeError):
-            issubclass(Employee, Any)
-        with self.assertRaises(TypeError):
-            issubclass(Any, Employee)
-
     def test_repr(self):
         self.assertEqual(repr(Any), 'typing.Any')
 
@@ -104,13 +98,21 @@ def test_errors(self):
         with self.assertRaises(TypeError):
             Any[int]  # Any is not a generic type.
 
-    def test_cannot_subclass(self):
-        with self.assertRaises(TypeError):
-            class A(Any):
-                pass
-        with self.assertRaises(TypeError):
-            class A(type(Any)):
-                pass
+    def test_can_subclass(self):
+        class Mock(Any): pass
+        self.assertTrue(issubclass(Mock, Any))
+        self.assertIsInstance(Mock(), Mock)
+
+        class Something: pass
+        self.assertFalse(issubclass(Something, Any))
+        self.assertNotIsInstance(Something(), Mock)
+
+        class MockSomething(Something, Mock): pass
+        self.assertTrue(issubclass(MockSomething, Any))
+        ms = MockSomething()
+        self.assertIsInstance(ms, MockSomething)
+        self.assertIsInstance(ms, Something)
+        self.assertIsInstance(ms, Mock)
 
     def test_cannot_instantiate(self):
         with self.assertRaises(TypeError):
diff --git a/Lib/typing.py b/Lib/typing.py
index 36f9eceb38c7c..4636798bd6956 100644
--- a/Lib/typing.py
+++ b/Lib/typing.py
@@ -429,8 +429,17 @@ def __getitem__(self, parameters):
         return self._getitem(self, *parameters)
 
 
- at _SpecialForm
-def Any(self, parameters):
+class _AnyMeta(type):
+    def __instancecheck__(self, obj):
+        if self is Any:
+            raise TypeError("typing.Any cannot be used with isinstance()")
+        return super().__instancecheck__(obj)
+
+    def __repr__(self):
+        return "typing.Any"
+
+
+class Any(metaclass=_AnyMeta):
     """Special type indicating an unconstrained type.
 
     - Any is compatible with every type.
@@ -439,9 +448,13 @@ def Any(self, parameters):
 
     Note that all the above statements are true from the point of view of
     static type checkers. At runtime, Any should not be used with instance
-    or class checks.
+    checks.
     """
-    raise TypeError(f"{self} is not subscriptable")
+    def __new__(cls, *args, **kwargs):
+        if cls is Any:
+            raise TypeError("Any cannot be instantiated")
+        return super().__new__(cls, *args, **kwargs)
+
 
 @_SpecialForm
 def NoReturn(self, parameters):
diff --git a/Misc/NEWS.d/next/Library/2022-03-13-08-52-58.bpo-46998.cHh-9O.rst b/Misc/NEWS.d/next/Library/2022-03-13-08-52-58.bpo-46998.cHh-9O.rst
new file mode 100644
index 0000000000000..25b82b5370846
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2022-03-13-08-52-58.bpo-46998.cHh-9O.rst
@@ -0,0 +1 @@
+Allow subclassing of :class:`typing.Any`. Patch by Shantanu Jain.



More information about the Python-checkins mailing list