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

Co-authored-by: Jelle Zijlstra <jelle.zijlstra@gmail.com>
This commit is contained in:
Shantanu 2022-04-04 19:35:29 -07:00 • committed by GitHub
parent bb86d1d9fb
commit 5a4973e29f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 44 additions and 31 deletions

View 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>`_,

View 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")

View 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],

View 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):

View 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):

View file

@ -0,0 +1 @@
Allow subclassing of :class:`typing.Any`. Patch by Shantanu Jain.