Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

bpo-46998: Allow subclassing Any at runtime #31841

Merged
merged 15 commits into from
Apr 5, 2022
3 changes: 3 additions & 0 deletions Doc/library/typing.rst
Original file line number Diff line number Diff line change
Expand Up @@ -578,6 +578,9 @@ 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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add some explanation about the use cases?

This doesn’t say why someone could want that, nor does the ticket, one has to hunt for the last message in the mailing list thread!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added a sentence here. Let me know if I should make it longer; I tried to keep it short because this is a lot more niche than other things covered in typing.rst

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think adding one short example might be nice

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure about this. On the one hand an example would be useful; on the other hand it risks putting too much emphasis on a pretty obscure use case.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's omit the example then (devguide says "err on the side of being succinct" for docs). I'm hoping to do some work on typing docs sometime soon, which might be a better home for such details.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alrighty!


.. 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")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This might be worth forbidding explicitly because the behavior could be quite unintuitive. Happy to leave that decision to the functools maintainer though.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ambv, do you have any thoughts on the bits of this PR that touch singledispatch?


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):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The metaclass is unfortunate because it restricts what classes can double-inherit from Any (due to metaclass conflicts). Seems unavoidable though.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not entirely unavoidable, if we were willing to give up on instancecheck (and repr), I'd say we could just remove the metaclass entirely

def __instancecheck__(self, obj):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we even have this? isinstance(X, Any) is now a meaningful operation.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm fine with removing it (as this PR currently does for issubclass). Doing so would also allow us to get rid of the metaclass, which will help remove restrictions on what classes can inherit from Any.

My reasoning for keeping it is that isinstance is very commonly used, potentially by typing / Python novices, and isinstance(..., Any) doesn't correspond well to the notion of Any at type check time. Sophisticated users have workarounds available to them for the equivalent isinstance check.

if self is Any:
raise TypeError("typing.Any cannot be used with isinstance()")
return super().__instancecheck__(obj)

def __repr__(self):
JelleZijlstra marked this conversation as resolved.
Show resolved Hide resolved
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.