Skip to content

Commit f5ce4ee

Browse files
authored
Fix joining a function against metaclass-using object constructors (#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.
1 parent 5094460 commit f5ce4ee

File tree

2 files changed

+25
-7
lines changed

2 files changed

+25
-7
lines changed

mypy/join.py

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -559,10 +559,10 @@ def join_similar_callables(t: CallableType, s: CallableType) -> CallableType:
559559
arg_types: list[Type] = []
560560
for i in range(len(t.arg_types)):
561561
arg_types.append(meet_types(t.arg_types[i], s.arg_types[i]))
562-
# TODO in combine_similar_callables also applies here (names and kinds)
563-
# The fallback type can be either 'function' or 'type'. The result should have 'type' as
564-
# fallback only if both operands have it as 'type'.
565-
if t.fallback.type.fullname != "builtins.type":
562+
# TODO in combine_similar_callables also applies here (names and kinds; user metaclasses)
563+
# The fallback type can be either 'function', 'type', or some user-provided metaclass.
564+
# The result should always use 'function' as a fallback if either operands are using it.
565+
if t.fallback.type.fullname == "builtins.function":
566566
fallback = t.fallback
567567
else:
568568
fallback = s.fallback
@@ -580,9 +580,10 @@ def combine_similar_callables(t: CallableType, s: CallableType) -> CallableType:
580580
for i in range(len(t.arg_types)):
581581
arg_types.append(join_types(t.arg_types[i], s.arg_types[i]))
582582
# TODO kinds and argument names
583-
# The fallback type can be either 'function' or 'type'. The result should have 'type' as
584-
# fallback only if both operands have it as 'type'.
585-
if t.fallback.type.fullname != "builtins.type":
583+
# TODO what should happen if one fallback is 'type' and the other is a user-provided metaclass?
584+
# The fallback type can be either 'function', 'type', or some user-provided metaclass.
585+
# The result should always use 'function' as a fallback if either operands are using it.
586+
if t.fallback.type.fullname == "builtins.function":
586587
fallback = t.fallback
587588
else:
588589
fallback = s.fallback

test-data/unit/check-classes.test

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -865,6 +865,23 @@ class C(B): pass
865865
class D(C): pass
866866
class D2(C): pass
867867

868+
[case testConstructorJoinsWithCustomMetaclass]
869+
# flags: --strict-optional
870+
from typing import TypeVar
871+
import abc
872+
873+
def func() -> None: pass
874+
class NormalClass: pass
875+
class WithMetaclass(metaclass=abc.ABCMeta): pass
876+
877+
T = TypeVar('T')
878+
def join(x: T, y: T) -> T: pass
879+
880+
f1 = join(func, WithMetaclass)
881+
reveal_type(f1()) # N: Revealed type is "Union[__main__.WithMetaclass, None]"
882+
883+
f2 = join(WithMetaclass, func)
884+
reveal_type(f2()) # N: Revealed type is "Union[__main__.WithMetaclass, None]"
868885

869886
-- Attribute access in class body
870887
-- ------------------------------

0 commit comments

Comments
 (0)