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

Fix joining a function against metaclass-using object constructors #13648

Merged

Conversation

Michael0x2a
Copy link
Collaborator

@Michael0x2a Michael0x2a commented Sep 12, 2022

This pull request fixes #9838.

It turns out that when an object is using a metaclass, it uses that metaclass as the fallback instead of builtins.type.

This caused the if t.fallback.type.fullname != "builtins.type" check we were performing in join_similar_callables and combine_similar_callables` to pick the wrong fallback in the case where we were attempting to join a function against a constructor for an object that used a metaclass.

This ended up causing a crash later for basically the exact same reason discussed in #13576: using abc.ABCMeta causes Callable.is_type_obj() to return true, which causes us to enter a codepath where we call Callable.type_object(). But this function is not prepared to handle the case where the return type of the callable is a Union, causing an assert to fail.

I opted to fix this by adjusting the join algorithm so it does if t.fallback.type.fullname == "builtins.function".

One question I did punt on -- what should happen in the case where one of the fallbacks is builtins.type and the other is a metaclass?

I suspect it's impossible for this case to actually occur: I think mypy would opt to use the algorithm for joining two Type[...] entities instead of these callable joining algorithms. While I'm not 100% sure of this, the current approach of just arbitrarily picking one of the two fallbacks seemed good enough for now.

This pull request fixes python#9838.

It turns out that when an object is using a metaclass, it uses
that metaclass as the fallback instead of `builtins.type`.

This caused the `if t.fallback.type.fullname != "builtins.type"`
check we were performing in `join_similar_callables` and
`combine_similar_callables` to pick the wrong fallback in the case
where we were attempting to join a function against a constructor
for an object that used a metaclass.

This ended up causing a crash later for basically the exact same
reason python#13576 caused a crash: using `abc.ABCMeta` causes
`Callable.is_type_obj()` to return true, which causes us to enter
a codepath where we call `Callable.type_object()`. But this function
is not prepared to handle the case where the return type of the callable
is a Union, causing an assert to fail.

I opted to fix this by adjusting the join algorithm so it does
`if t.fallback.type.fullname == "builtins.function"`.

One question I did punt on -- what should happen in the case where
one of the fallbacks is `builtins.type` and the other is a metaclass?

I suspect it's impossible for this case to actually occur: I think
mypy would opt to use the algorithm for joining two `Type[...]` entities
instead of these callable joining algorithms. While I'm not 100% sure of
this, the current approach of just arbitrarily picking one of the two
fallbacks seemed good enough for now.
@github-actions

This comment has been minimized.

Copy link
Member

@sobolevn sobolevn left a comment

Choose a reason for hiding this comment

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

LGTM, except this minor nit :)


def f1(cond: bool) -> Any:
join = func if cond else WithMetaclass
return reveal_type(join()) # N: Revealed type is "Union[__main__.WithMetaclass, None]"
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
return reveal_type(join()) # N: Revealed type is "Union[__main__.WithMetaclass, None]"
reveal_type(join()) # N: Revealed type is "Union[__main__.WithMetaclass, None]"


def f2(cond: bool) -> Any:
join = WithMetaclass if cond else func
return reveal_type(join()) # N: Revealed type is "Union[__main__.WithMetaclass, None]"
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
return reveal_type(join()) # N: Revealed type is "Union[__main__.WithMetaclass, None]"
reveal_type(join()) # N: Revealed type is "Union[__main__.WithMetaclass, None]"


def f3(cond: bool) -> Any:
join = NormalClass if cond else WithMetaclass
return reveal_type(join()) # N: Revealed type is "builtins.object"
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
return reveal_type(join()) # N: Revealed type is "builtins.object"
reveal_type(join()) # N: Revealed type is "builtins.object"


def f4(cond: bool) -> Any:
join = WithMetaclass if cond else NormalClass
return reveal_type(join()) # N: Revealed type is "builtins.object"
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
return reveal_type(join()) # N: Revealed type is "builtins.object"
reveal_type(join()) # N: Revealed type is "builtins.object"

@AlexWaygood
Copy link
Member

AlexWaygood commented Sep 12, 2022

I'm guessing all of these tests would break if something like #12712 was ever merged (there seems to be a consensus that it would be good to do something like that if the regressions could be kept down to a minimum — #12395 (comment)).

Is there a way of making the tests more future-proof for that? Or not worth bothering about?

@ilevkivskyi
Copy link
Member

Actually, yes, I agree that a better way to trigger join is to use f(x: T, y: T) -> T: ....

@github-actions
Copy link
Contributor

According to mypy_primer, this change has no effect on the checked open source code. 🤖🎉

Copy link
Member

@AlexWaygood AlexWaygood left a comment

Choose a reason for hiding this comment

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

LGTM!

@Michael0x2a Michael0x2a merged commit f5ce4ee into python:master Sep 25, 2022
@Michael0x2a Michael0x2a deleted the fix-function-joins-with-metaclasses branch September 25, 2022 21:27
@gvanrossum
Copy link
Member

Yay Michael!

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

Successfully merging this pull request may close these issues.

problem with ABCMeta as base class inside an iter without annotation
5 participants