Skip to content

gh-108394: Fix subclassing annotated generic alias #108403

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

Open
wants to merge 2 commits into
base: main
Choose a base branch
from

Conversation

sobolevn
Copy link
Member

@sobolevn sobolevn commented Aug 24, 2023

I've decided to go in a different direction from the one suggested by @eltoder

Now, we reuse all __mro__ logic from generic aliases.

self.assertEqual(Z.__mro__, (Z, GenericBase, Generic, Mixin, object))

# Errors:
A1 = Annotated[Literal[1], 'meta']
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we add an error case with a SpecialForm that is not a GenericAlias, like NoReturn or Self?

i = bases.index(self)
except ValueError:
# It might not be there, if `Annotated[]` is used:
i = 0
Copy link
Contributor

@eltoder eltoder Aug 24, 2023

Choose a reason for hiding this comment

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

I think this fails in cases like:

class C(typing.Annotated[typing.Callable[[], int], 123]):
    pass

C.__bases__ should include Generic. Another similar case that already fails is

class C(typing.Generic[T], typing.Annotated[int, 123]):
    pass

C.__bases__ lost Generic.

Copy link
Member

Choose a reason for hiding this comment

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

@eltoder, your fist snippet works fine for me with @sobolevn's PR branch checked out:

Running Release|x64 interpreter...
Python 3.13.0a0 (heads/main:0e8b3fc718, Aug 28 2023, 13:48:35) [MSC v.1932 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> from typing import *
>>> class C(Callable[[], int]): ...
...
>>> C.__mro__
(<class '__main__.C'>, <class 'collections.abc.Callable'>, <class 'typing.Generic'>, <class 'object'>)
>>> class C(Annotated[Callable[[], int], 123]): ...
...
>>> C.__bases__
(<class 'collections.abc.Callable'>, <class 'typing.Generic'>)
>>> C.__mro__
(<class '__main__.C'>, <class 'collections.abc.Callable'>, <class 'typing.Generic'>, <class 'object'>)

Your second case does indeed look buggy, but as you say, the bug already exists on main. This is the behaviour we get on both this PR branch and the main branch:

Python 3.13.0a0 (heads/main:0e8b3fc718, Aug 28 2023, 13:48:35) [MSC v.1932 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> from typing import *
>>> T = TypeVar("T")
>>> class C(Generic[T], Annotated[int, 123]): ...
...
>>> C.__bases__
(<class 'int'>,)

Copy link
Member Author

Choose a reason for hiding this comment

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

Generic and Annotated together is not tested, please open a new issue about it.

Copy link
Member

@AlexWaygood AlexWaygood Aug 28, 2023

Choose a reason for hiding this comment

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

Yes, I think we would need to special-case _AnnotatedAlias in _GenericAlias.__mro_entries__. The cause of the bug is this behaviour:

>>> from typing import *
>>> T = TypeVar("T")
>>> Generic[T].__mro_entries__((Generic[T], Annotated[int, 123]))
()

It's a separate change, unrelated to this one.

Copy link
Contributor

Choose a reason for hiding this comment

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

@AlexWaygood sorry, off-by-one in the first example. Should be

class B: pass
class C(B, typing.Annotated[typing.Callable[[], int], 123]):
    pass

The point is that catching exceptions and setting i = 0 introduces subtle bugs.

Copy link
Member

Choose a reason for hiding this comment

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

The point is that catching exceptions and setting i = 0 introduces subtle bugs.

Indeed, good catch. Minimal repro:

>>> import types
>>> from typing import *
>>> class A: pass
...
>>> types.new_class('B', (A, Callable[[], int])).__mro__
(<class 'abc.B'>, <class '__main__.A'>, <class 'collections.abc.Callable'>, <class 'typing.Generic'>, <class 'object'>)
>>> types.new_class('B', (A, Annotated[Callable[[], int], 123])).__mro__
(<class 'abc.B'>, <class '__main__.A'>, <class 'collections.abc.Callable'>, <class 'object'>)

Comment on lines +2005 to +2006
if isinstance(self.__origin__, (_GenericAlias, GenericAlias)):
return self.__origin__.__mro_entries__(bases)
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 we should delegate to any __mro_entries__ method, if it exists, not just for _GenericAlias and GenericAlias:

Suggested change
if isinstance(self.__origin__, (_GenericAlias, GenericAlias)):
return self.__origin__.__mro_entries__(bases)
if hasattr(self.__origin__, "__mro_entries__"):
return self.__origin__.__mro_entries__(bases)

An example of an improvement this change would lead to: currently you get a nice error message if you try subclassing a NewType instance:

>>> from typing import *
>>> UserID = NewType("UserID", int)
>>> class Foo(UserID): pass
...
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "C:\Users\alexw\coding\cpython\Lib\typing.py", line 3084, in __init_subclass__
    raise TypeError(
TypeError: Cannot subclass an instance of NewType. Perhaps you were looking for: `Foo = NewType('Foo', UserID)`

But the nice error message goes away if you subclass Annotated[newtype_instance]:

>>> class Foo(Annotated[UserID, 'must be above 100']): pass
...
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: NewType.__init__() takes 3 positional arguments but 4 were give

If we make the change I'm suggesting here, we'll just delegate to NewType.__mro__, so we'll still get the nice error message:

>>> class Foo(Annotated[UserID, 'must be above 100']): ...
...
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "C:\Users\alexw\coding\cpython\Lib\typing.py", line 3084, in __init_subclass__
    raise TypeError(
TypeError: Cannot subclass an instance of NewType. Perhaps you were looking for: `Foo = NewType('Foo', UserID)`

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants