-
-
Notifications
You must be signed in to change notification settings - Fork 2.8k
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
Fix joining a function against metaclass-using object constructors #13648
Conversation
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.
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this 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 :)
test-data/unit/check-classes.test
Outdated
|
||
def f1(cond: bool) -> Any: | ||
join = func if cond else WithMetaclass | ||
return reveal_type(join()) # N: Revealed type is "Union[__main__.WithMetaclass, None]" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
return reveal_type(join()) # N: Revealed type is "Union[__main__.WithMetaclass, None]" | |
reveal_type(join()) # N: Revealed type is "Union[__main__.WithMetaclass, None]" |
test-data/unit/check-classes.test
Outdated
|
||
def f2(cond: bool) -> Any: | ||
join = WithMetaclass if cond else func | ||
return reveal_type(join()) # N: Revealed type is "Union[__main__.WithMetaclass, None]" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
return reveal_type(join()) # N: Revealed type is "Union[__main__.WithMetaclass, None]" | |
reveal_type(join()) # N: Revealed type is "Union[__main__.WithMetaclass, None]" |
test-data/unit/check-classes.test
Outdated
|
||
def f3(cond: bool) -> Any: | ||
join = NormalClass if cond else WithMetaclass | ||
return reveal_type(join()) # N: Revealed type is "builtins.object" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
return reveal_type(join()) # N: Revealed type is "builtins.object" | |
reveal_type(join()) # N: Revealed type is "builtins.object" |
test-data/unit/check-classes.test
Outdated
|
||
def f4(cond: bool) -> Any: | ||
join = WithMetaclass if cond else NormalClass | ||
return reveal_type(join()) # N: Revealed type is "builtins.object" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
return reveal_type(join()) # N: Revealed type is "builtins.object" | |
reveal_type(join()) # N: Revealed type is "builtins.object" |
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? |
Actually, yes, I agree that a better way to trigger join is to use |
According to mypy_primer, this change has no effect on the checked open source code. 🤖🎉 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM!
Yay Michael! |
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 injoin_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
causesCallable.is_type_obj()
to return true, which causes us to enter a codepath where we callCallable.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.