Skip to content

Commit

Permalink
bpo-46998: Allow subclassing Any at runtime (GH-31841)
Browse files Browse the repository at this point in the history
Co-authored-by: Jelle Zijlstra <jelle.zijlstra@gmail.com>
  • Loading branch information
hauntsaninja and JelleZijlstra committed Apr 5, 2022
1 parent bb86d1d commit 5a4973e
Show file tree
Hide file tree
Showing 6 changed files with 44 additions and 31 deletions.
5 changes: 5 additions & 0 deletions Doc/library/typing.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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>`_,
Expand Down
8 changes: 0 additions & 8 deletions Lib/test/test_functools.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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
Expand All @@ -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")
Expand Down
12 changes: 6 additions & 6 deletions Lib/test/test_pydoc.py
Original file line number Diff line number Diff line change
Expand Up @@ -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],
Expand Down
28 changes: 15 additions & 13 deletions Lib/test/test_typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')

Expand All @@ -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):
Expand Down
21 changes: 17 additions & 4 deletions Lib/typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -429,8 +429,17 @@ def __getitem__(self, parameters):
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.
Expand All @@ -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):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Allow subclassing of :class:`typing.Any`. Patch by Shantanu Jain.

0 comments on commit 5a4973e

Please sign in to comment.