Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 63 additions & 7 deletions mypy/checkexpr.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@
validate_instance,
)
from mypy.typeops import (
bind_self,
callable_type,
custom_special_method,
erase_to_union_or_bound,
Expand Down Expand Up @@ -1001,6 +1002,7 @@ def typeddict_callable_from_context(
self.named_type("builtins.type"),
variables=variables,
is_bound=True,
original_self_type=AnyType(TypeOfAny.implementation_artifact),
)

def check_typeddict_call_with_kwargs(
Expand Down Expand Up @@ -2911,10 +2913,63 @@ def infer_overload_return_type(
args_contain_any = any(map(has_any_type, arg_types))
type_maps: list[dict[Expression, Type]] = []

# If we have a selftype overload, it should contribute to `any_causes_overload_ambiguity`
# check. Pretend that we're checking `Foo.func(instance, ...)` instead of
# `instance.func(...)`.
p_object_type = get_proper_type(object_type) if object_type is not None else None

def is_trivial_self(t: CallableType) -> bool:
if isinstance(t.definition, FuncDef):
return t.definition.is_trivial_self
if isinstance(t.definition, Decorator):
return t.definition.func.is_trivial_self
return False

prepend_self = (
isinstance(p_object_type, Instance)
and has_any_type(p_object_type)
and any(
typ.is_bound and typ.original_self_type is not None and not is_trivial_self(typ)
for typ in plausible_targets
)
)
if prepend_self:
assert object_type is not None

args = [TempNode(object_type)] + args
arg_types = [object_type] + arg_types
arg_kinds = [ARG_POS] + arg_kinds
arg_names = [None, *arg_names] if arg_names is not None else None

def maybe_bind_self(t: Type) -> Type:
if prepend_self and isinstance(t, ProperType) and isinstance(t, FunctionLike):
return bind_self(t, object_type)
return t

for typ in plausible_targets:
assert self.msg is self.chk.msg
with self.msg.filter_errors() as w:
with self.chk.local_type_map as m:
with self.msg.filter_errors() as w, self.chk.local_type_map as m:
if prepend_self:
param = typ.original_self_type
assert param is not None, "Overload bound only partially?"
assert isinstance(p_object_type, Instance)
param = expand_type_by_instance(param, p_object_type)
ret_type, infer_type = self.check_call(
callee=typ.copy_modified(
arg_types=[param] + typ.arg_types,
arg_kinds=[ARG_POS] + typ.arg_kinds,
arg_names=[None, *typ.arg_names],
is_bound=False,
original_self_type=None,
),
args=args,
arg_kinds=arg_kinds,
arg_names=arg_names,
context=context,
callable_name=callable_name,
object_type=None,
)
else:
ret_type, infer_type = self.check_call(
callee=typ,
args=args,
Expand All @@ -2928,9 +2983,9 @@ def infer_overload_return_type(
if is_match:
# Return early if possible; otherwise record info, so we can
# check for ambiguity due to 'Any' below.
if not args_contain_any:
if not args_contain_any and not prepend_self:
self.chk.store_types(m)
return ret_type, infer_type
return ret_type, maybe_bind_self(infer_type)
p_infer_type = get_proper_type(infer_type)
if isinstance(p_infer_type, CallableType):
# Prefer inferred types if possible, this will avoid false triggers for
Expand All @@ -2949,10 +3004,10 @@ def infer_overload_return_type(
# We try returning a precise type if we can. If not, we give up and just return 'Any'.
if all_same_types(return_types):
self.chk.store_types(type_maps[0])
return return_types[0], inferred_types[0]
return return_types[0], maybe_bind_self(inferred_types[0])
elif all_same_types([erase_type(typ) for typ in return_types]):
self.chk.store_types(type_maps[0])
return erase_type(return_types[0]), erase_type(inferred_types[0])
return erase_type(return_types[0]), maybe_bind_self(erase_type(inferred_types[0]))
else:
return self.check_call(
callee=AnyType(TypeOfAny.special_form),
Expand All @@ -2966,7 +3021,7 @@ def infer_overload_return_type(
else:
# Success! No ambiguity; return the first match.
self.chk.store_types(type_maps[0])
return return_types[0], inferred_types[0]
return return_types[0], maybe_bind_self(inferred_types[0])

def overload_erased_call_targets(
self,
Expand Down Expand Up @@ -5017,6 +5072,7 @@ def apply_type_arguments_to_callable(
name="tuple",
definition=tp.definition,
is_bound=tp.is_bound,
original_self_type=tp.original_self_type,
)
self.msg.incompatible_type_application(
min_arg_count, len(type_vars), len(args), ctx
Expand Down
1 change: 1 addition & 0 deletions mypy/checkmember.py
Original file line number Diff line number Diff line change
Expand Up @@ -1519,6 +1519,7 @@ def bind_self_fast(method: F, original_type: Type | None = None) -> F:
arg_kinds=method.arg_kinds[1:],
arg_names=method.arg_names[1:],
is_bound=True,
original_self_type=method.arg_types[0],
)


Expand Down
3 changes: 3 additions & 0 deletions mypy/exportjson.py
Original file line number Diff line number Diff line change
Expand Up @@ -467,6 +467,9 @@ def convert_callable_type(self: CallableType) -> Json:
"is_ellipsis_args": self.is_ellipsis_args,
"implicit": self.implicit,
"is_bound": self.is_bound,
"original_self_type": (
convert_type(self.original_self_type) if self.original_self_type is not None else None
),
"type_guard": convert_type(self.type_guard) if self.type_guard is not None else None,
"type_is": convert_type(self.type_is) if self.type_is is not None else None,
"from_concatenate": self.from_concatenate,
Expand Down
2 changes: 2 additions & 0 deletions mypy/fixup.py
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,8 @@ def visit_callable_type(self, ct: CallableType) -> None:
ct.ret_type.accept(self)
for v in ct.variables:
v.accept(self)
if ct.original_self_type is not None:
ct.original_self_type.accept(self)
if ct.type_guard is not None:
ct.type_guard.accept(self)
if ct.type_is is not None:
Expand Down
1 change: 1 addition & 0 deletions mypy/server/astdiff.py
Original file line number Diff line number Diff line change
Expand Up @@ -471,6 +471,7 @@ def visit_callable_type(self, typ: CallableType) -> SnapshotItem:
typ.is_ellipsis_args,
snapshot_types(typ.variables),
typ.is_bound,
snapshot_optional_type(typ.original_self_type),
)

def normalize_callable_variables(self, typ: CallableType) -> CallableType:
Expand Down
2 changes: 2 additions & 0 deletions mypy/typeops.py
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,7 @@ def type_object_type(info: TypeInfo, named_type: Callable[[str], Instance]) -> P
arg_names=["_args", "_kwds"],
ret_type=any_type,
is_bound=True,
original_self_type=any_type,
fallback=named_type("builtins.function"),
)
result: FunctionLike = class_callable(sig, info, fallback, None, is_new=False)
Expand Down Expand Up @@ -490,6 +491,7 @@ class B(A): pass
arg_names=func.arg_names[1:],
variables=variables,
is_bound=True,
original_self_type=func.arg_types[0],
)
return cast(F, res)

Expand Down
19 changes: 19 additions & 0 deletions mypy/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -2169,6 +2169,7 @@ class CallableType(FunctionLike):
"from_type_type", # Was this callable generated by analyzing Type[...]
# instantiation?
"is_bound", # Is this a bound method?
"original_self_type", # If bound, what was the type of `self` before?
"type_guard", # T, if -> TypeGuard[T] (ret_type is bool in this case).
"type_is", # T, if -> TypeIs[T] (ret_type is bool in this case).
"from_concatenate", # whether this callable is from a concatenate object
Expand All @@ -2195,6 +2196,7 @@ def __init__(
special_sig: str | None = None,
from_type_type: bool = False,
is_bound: bool = False,
original_self_type: Type | None = None,
type_guard: Type | None = None,
type_is: Type | None = None,
from_concatenate: bool = False,
Expand Down Expand Up @@ -2232,6 +2234,7 @@ def __init__(
self.from_concatenate = from_concatenate
self.imprecise_arg_kinds = imprecise_arg_kinds
self.is_bound = is_bound
self.original_self_type = original_self_type
self.type_guard = type_guard
self.type_is = type_is
self.unpack_kwargs = unpack_kwargs
Expand All @@ -2253,6 +2256,7 @@ def copy_modified(
special_sig: Bogus[str | None] = _dummy,
from_type_type: Bogus[bool] = _dummy,
is_bound: Bogus[bool] = _dummy,
original_self_type: Bogus[Type | None] = _dummy,
type_guard: Bogus[Type | None] = _dummy,
type_is: Bogus[Type | None] = _dummy,
from_concatenate: Bogus[bool] = _dummy,
Expand All @@ -2277,6 +2281,9 @@ def copy_modified(
special_sig=special_sig if special_sig is not _dummy else self.special_sig,
from_type_type=from_type_type if from_type_type is not _dummy else self.from_type_type,
is_bound=is_bound if is_bound is not _dummy else self.is_bound,
original_self_type=(
original_self_type if original_self_type is not _dummy else self.original_self_type
),
type_guard=type_guard if type_guard is not _dummy else self.type_guard,
type_is=type_is if type_is is not _dummy else self.type_is,
from_concatenate=(
Expand Down Expand Up @@ -2598,6 +2605,11 @@ def serialize(self) -> JsonDict:
"is_ellipsis_args": self.is_ellipsis_args,
"implicit": self.implicit,
"is_bound": self.is_bound,
"original_self_type": (
self.original_self_type.serialize()
if self.original_self_type is not None
else None
),
"type_guard": self.type_guard.serialize() if self.type_guard is not None else None,
"type_is": (self.type_is.serialize() if self.type_is is not None else None),
"from_concatenate": self.from_concatenate,
Expand All @@ -2620,6 +2632,11 @@ def deserialize(cls, data: JsonDict) -> CallableType:
is_ellipsis_args=data["is_ellipsis_args"],
implicit=data["implicit"],
is_bound=data["is_bound"],
original_self_type=(
deserialize_type(data["original_self_type"])
if data["original_self_type"] is not None
else None
),
type_guard=(
deserialize_type(data["type_guard"]) if data["type_guard"] is not None else None
),
Expand All @@ -2641,6 +2658,7 @@ def write(self, data: Buffer) -> None:
write_bool(data, self.is_ellipsis_args)
write_bool(data, self.implicit)
write_bool(data, self.is_bound)
write_type_opt(data, self.original_self_type)
write_type_opt(data, self.type_guard)
write_type_opt(data, self.type_is)
write_bool(data, self.from_concatenate)
Expand All @@ -2663,6 +2681,7 @@ def read(cls, data: Buffer) -> CallableType:
is_ellipsis_args=read_bool(data),
implicit=read_bool(data),
is_bound=read_bool(data),
original_self_type=read_type_opt(data),
type_guard=read_type_opt(data),
type_is=read_type_opt(data),
from_concatenate=read_bool(data),
Expand Down
17 changes: 17 additions & 0 deletions test-data/unit/check-overloading.test
Original file line number Diff line number Diff line change
Expand Up @@ -6852,3 +6852,20 @@ if isinstance(headers, dict):

reveal_type(headers) # N: Revealed type is "Union[__main__.Headers, typing.Iterable[tuple[builtins.bytes, builtins.bytes]]]"
[builtins fixtures/isinstancelist.pyi]

[case testSelfOverloadWithAnySelf]
from typing import Any, Generic, TypeVar, overload

T = TypeVar("T")

class A(Generic[T]):
@overload
def run(self: A[int]) -> int: ...
@overload
def run(self: A[str]) -> str: ...
def run(self: "A[int] | A[str]") -> "int | str": ...

foo: A[Any]
reveal_type(foo.run()) # N: Revealed type is "Any"
reveal_type(A.run(foo)) # N: Revealed type is "Any"
[builtins fixtures/tuple.pyi]
1 change: 1 addition & 0 deletions test-data/unit/exportjson.test
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@ def foo(a: int) -> None: ...
"is_ellipsis_args": false,
"implicit": false,
"is_bound": false,
"original_self_type": null,
"type_guard": null,
"type_is": null,
"from_concatenate": false,
Expand Down