[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