]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
bpo-46998: Allow subclassing Any at runtime (GH-31841)
authorShantanu <12621235+hauntsaninja@users.noreply.github.com>
Tue, 5 Apr 2022 02:35:29 +0000 (19:35 -0700)
committerGitHub <noreply@github.com>
Tue, 5 Apr 2022 02:35:29 +0000 (19:35 -0700)
Co-authored-by: Jelle Zijlstra <jelle.zijlstra@gmail.com>
Doc/library/typing.rst
Lib/test/test_functools.py
Lib/test/test_pydoc.py
Lib/test/test_typing.py
Lib/typing.py
Misc/NEWS.d/next/Library/2022-03-13-08-52-58.bpo-46998.cHh-9O.rst [new file with mode: 0644]

index 37c17c429fa47cf4559d31555496555c8ad25b25..0a4e848c67736c3c0bcbdd3b0d66f1e045b5dea9 100644 (file)
@@ -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>`_,
index abbd50a47f395f8594380111be7789697f60d564..82e73f46a3fbae3db65c7c681df9b694228386d4 100644 (file)
@@ -2802,8 +2802,6 @@ class TestSingleDispatch(unittest.TestCase):
             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 @@ class TestSingleDispatch(unittest.TestCase):
             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 @@ class TestSingleDispatch(unittest.TestCase):
             @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")
index 9c900c3e8ee0afb479ce76b9ba12a19a7f2c74ba..13c77b6fa6822c8725ca510d5b153e7fd0d6ea9c 100644 (file)
@@ -1066,14 +1066,14 @@ class TestDescriptions(unittest.TestCase):
         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],
index 0e28655296d14f16dc9729289d1aabf7a62dfd00..041b6ad9ed6dd1388bb818f39495898daf6d87c9 100644 (file)
@@ -89,12 +89,6 @@ class AnyTests(BaseTestCase):
         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 @@ class AnyTests(BaseTestCase):
         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):
index 36f9eceb38c7cf577b1e2f3d086687ba817cc8a0..4636798bd6956ee6a32ab0a2851b9afb29ae50f1 100644 (file)
@@ -429,8 +429,17 @@ class _LiteralSpecialForm(_SpecialForm, _root=True):
         return self._getitem(self, *parameters)
 
 
-@_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 (file)
index 0000000..25b82b5
--- /dev/null
@@ -0,0 +1 @@
+Allow subclassing of :class:`typing.Any`. Patch by Shantanu Jain.