Skip to content

Commit

Permalink
Fix joining a function against metaclass-using object constructors (#…
Browse files Browse the repository at this point in the history
…13648)

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.
Michael0x2a authored Sep 25, 2022

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
1 parent 5094460 commit f5ce4ee
Showing 2 changed files with 25 additions and 7 deletions.
15 changes: 8 additions & 7 deletions mypy/join.py
Original file line number Diff line number Diff line change
@@ -559,10 +559,10 @@ def join_similar_callables(t: CallableType, s: CallableType) -> CallableType:
arg_types: list[Type] = []
for i in range(len(t.arg_types)):
arg_types.append(meet_types(t.arg_types[i], s.arg_types[i]))
# TODO in combine_similar_callables also applies here (names and kinds)
# The fallback type can be either 'function' or 'type'. The result should have 'type' as
# fallback only if both operands have it as 'type'.
if t.fallback.type.fullname != "builtins.type":
# TODO in combine_similar_callables also applies here (names and kinds; user metaclasses)
# The fallback type can be either 'function', 'type', or some user-provided metaclass.
# The result should always use 'function' as a fallback if either operands are using it.
if t.fallback.type.fullname == "builtins.function":
fallback = t.fallback
else:
fallback = s.fallback
@@ -580,9 +580,10 @@ def combine_similar_callables(t: CallableType, s: CallableType) -> CallableType:
for i in range(len(t.arg_types)):
arg_types.append(join_types(t.arg_types[i], s.arg_types[i]))
# TODO kinds and argument names
# The fallback type can be either 'function' or 'type'. The result should have 'type' as
# fallback only if both operands have it as 'type'.
if t.fallback.type.fullname != "builtins.type":
# TODO what should happen if one fallback is 'type' and the other is a user-provided metaclass?
# The fallback type can be either 'function', 'type', or some user-provided metaclass.
# The result should always use 'function' as a fallback if either operands are using it.
if t.fallback.type.fullname == "builtins.function":
fallback = t.fallback
else:
fallback = s.fallback
17 changes: 17 additions & 0 deletions test-data/unit/check-classes.test
Original file line number Diff line number Diff line change
@@ -865,6 +865,23 @@ class C(B): pass
class D(C): pass
class D2(C): pass

[case testConstructorJoinsWithCustomMetaclass]
# flags: --strict-optional
from typing import TypeVar
import abc

def func() -> None: pass
class NormalClass: pass
class WithMetaclass(metaclass=abc.ABCMeta): pass

T = TypeVar('T')
def join(x: T, y: T) -> T: pass

f1 = join(func, WithMetaclass)
reveal_type(f1()) # N: Revealed type is "Union[__main__.WithMetaclass, None]"

f2 = join(WithMetaclass, func)
reveal_type(f2()) # N: Revealed type is "Union[__main__.WithMetaclass, None]"

-- Attribute access in class body
-- ------------------------------

0 comments on commit f5ce4ee

Please sign in to comment.