From 5a4973e29f2f5c4ee8c086f40325786c62381540 Mon Sep 17 00:00:00 2001 From: Shantanu <12621235+hauntsaninja@users.noreply.github.com> Date: Mon, 4 Apr 2022 19:35:29 -0700 Subject: bpo-46998: Allow subclassing Any at runtime (GH-31841) Co-authored-by: Jelle Zijlstra --- Doc/library/typing.rst | 5 ++++ Lib/test/test_functools.py | 8 ------- Lib/test/test_pydoc.py | 12 +++++----- Lib/test/test_typing.py | 28 ++++++++++++---------- Lib/typing.py | 21 ++++++++++++---- .../2022-03-13-08-52-58.bpo-46998.cHh-9O.rst | 1 + 6 files changed, 44 insertions(+), 31 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2022-03-13-08-52-58.bpo-46998.cHh-9O.rst diff --git a/Doc/library/typing.rst b/Doc/library/typing.rst index 37c17c4..0a4e848 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 `_, diff --git a/Lib/test/test_functools.py b/Lib/test/test_functools.py index abbd50a..82e73f4 100644 --- a/Lib/test/test_functools.py +++ b/Lib/test/test_functools.py @@ -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") diff --git a/Lib/test/test_pydoc.py b/Lib/test/test_pydoc.py index 9c900c3..13c77b6 100644 --- a/Lib/test/test_pydoc.py +++ b/Lib/test/test_pydoc.py @@ -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], diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index 0e28655..041b6ad 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -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): diff --git a/Lib/typing.py b/Lib/typing.py index 36f9ece..4636798 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -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 index 0000000..25b82b5 --- /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. -- cgit v0.12