Skip to content

Do not use dictionary in CallableType #19580

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

Merged
merged 3 commits into from
Aug 4, 2025
Merged
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
2 changes: 1 addition & 1 deletion mypy/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -726,7 +726,7 @@ def _visit_overloaded_func_def(self, defn: OverloadedFuncDef) -> None:
assert isinstance(item, Decorator)
item_type = self.extract_callable_type(item.var.type, item)
if item_type is not None:
item_type.definition = item
item_type.definition = item.func
item_types.append(item_type)
if item_types:
defn.type = Overloaded(item_types)
Expand Down
4 changes: 2 additions & 2 deletions mypy/checkexpr.py
Original file line number Diff line number Diff line change
Expand Up @@ -2934,7 +2934,7 @@ def infer_overload_return_type(
if not args_contain_any:
self.chk.store_types(m)
if isinstance(infer_type, ProperType) and isinstance(infer_type, CallableType):
self.chk.check_deprecated(infer_type.definition, context)
self.chk.warn_deprecated(infer_type.definition, context)
return ret_type, infer_type
p_infer_type = get_proper_type(infer_type)
if isinstance(p_infer_type, CallableType):
Expand Down Expand Up @@ -2975,7 +2975,7 @@ def infer_overload_return_type(
if isinstance(inferred_callable, ProperType) and isinstance(
inferred_callable, CallableType
):
self.chk.check_deprecated(inferred_callable.definition, context)
self.chk.warn_deprecated(inferred_callable.definition, context)
return return_types[0], inferred_types[0]

def overload_erased_call_targets(
Expand Down
8 changes: 8 additions & 0 deletions mypy/fixup.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
Overloaded,
Parameters,
ParamSpecType,
ProperType,
TupleType,
TypeAliasType,
TypedDictType,
Expand Down Expand Up @@ -177,6 +178,11 @@ def visit_overloaded_func_def(self, o: OverloadedFuncDef) -> None:
item.accept(self)
if o.impl:
o.impl.accept(self)
if isinstance(o.type, Overloaded):
# For error messages we link the original definition for each item.
for typ, item in zip(o.type.items, o.items):
if isinstance(item, Decorator):
typ.definition = item.func

def visit_decorator(self, d: Decorator) -> None:
if self.current_info is not None:
Expand All @@ -187,6 +193,8 @@ def visit_decorator(self, d: Decorator) -> None:
d.var.accept(self)
for node in d.decorators:
node.accept(self)
if isinstance(d.var.type, ProperType) and isinstance(d.var.type, CallableType):
d.var.type.definition = d.func

def visit_class_def(self, c: ClassDef) -> None:
for v in c.type_vars:
Expand Down
10 changes: 8 additions & 2 deletions mypy/messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -1003,7 +1003,7 @@ def maybe_note_about_special_args(self, callee: CallableType, context: Context)
if self.prefer_simple_messages():
return
# https://github.com/python/mypy/issues/11309
first_arg = callee.def_extras.get("first_arg")
first_arg = get_first_arg(callee)
if first_arg and first_arg not in {"self", "cls", "mcs"}:
self.note(
"Looks like the first special argument in a method "
Expand Down Expand Up @@ -3007,7 +3007,7 @@ def [T <: int] f(self, x: int, y: T) -> None
s = definition_arg_names[0] + s
s = f"{tp.definition.name}({s})"
elif tp.name:
first_arg = tp.def_extras.get("first_arg")
first_arg = get_first_arg(tp)
if first_arg:
if s:
s = ", " + s
Expand Down Expand Up @@ -3050,6 +3050,12 @@ def [T <: int] f(self, x: int, y: T) -> None
return f"def {s}"


def get_first_arg(tp: CallableType) -> str | None:
if not isinstance(tp.definition, FuncDef) or not tp.definition.info or tp.definition.is_static:
return None
return tp.definition.original_first_arg


def variance_string(variance: int) -> str:
if variance == COVARIANT:
return "covariant"
Expand Down
9 changes: 9 additions & 0 deletions mypy/nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -823,6 +823,7 @@ class FuncDef(FuncItem, SymbolNode, Statement):
"dataclass_transform_spec",
"docstring",
"deprecated",
"original_first_arg",
)

__match_args__ = ("name", "arguments", "type", "body")
Expand Down Expand Up @@ -855,6 +856,12 @@ def __init__(
# the majority). In cases where self is not annotated and there are no Self
# in the signature we can simply drop the first argument.
self.is_trivial_self = False
# This is needed because for positional-only arguments the name is set to None,
# but we sometimes still want to show it in error messages.
if arguments:
self.original_first_arg: str | None = arguments[0].variable.name
else:
self.original_first_arg = None

@property
def name(self) -> str:
Expand Down Expand Up @@ -886,6 +893,7 @@ def serialize(self) -> JsonDict:
else self.dataclass_transform_spec.serialize()
),
"deprecated": self.deprecated,
"original_first_arg": self.original_first_arg,
}

@classmethod
Expand All @@ -906,6 +914,7 @@ def deserialize(cls, data: JsonDict) -> FuncDef:
set_flags(ret, data["flags"])
# NOTE: ret.info is set in the fixup phase.
ret.arg_names = data["arg_names"]
ret.original_first_arg = data.get("original_first_arg")
ret.arg_kinds = [ArgKind(x) for x in data["arg_kinds"]]
ret.abstract_status = data["abstract_status"]
ret.dataclass_transform_spec = (
Expand Down
34 changes: 1 addition & 33 deletions mypy/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,7 @@

import mypy.nodes
from mypy.bogus_type import Bogus
from mypy.nodes import (
ARG_POS,
ARG_STAR,
ARG_STAR2,
INVARIANT,
ArgKind,
FakeInfo,
FuncDef,
SymbolNode,
)
from mypy.nodes import ARG_POS, ARG_STAR, ARG_STAR2, INVARIANT, ArgKind, FakeInfo, SymbolNode
from mypy.options import Options
from mypy.state import state
from mypy.util import IdMapper
Expand Down Expand Up @@ -1841,8 +1832,6 @@ class CallableType(FunctionLike):
"from_type_type", # Was this callable generated by analyzing Type[...]
# instantiation?
"is_bound", # Is this a bound method?
"def_extras", # Information about original definition we want to serialize.
# This is used for more detailed error messages.
"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 @@ -1869,7 +1858,6 @@ def __init__(
special_sig: str | None = None,
from_type_type: bool = False,
is_bound: bool = False,
def_extras: dict[str, Any] | None = None,
type_guard: Type | None = None,
type_is: Type | None = None,
from_concatenate: bool = False,
Expand Down Expand Up @@ -1902,22 +1890,6 @@ def __init__(
self.from_concatenate = from_concatenate
self.imprecise_arg_kinds = imprecise_arg_kinds
self.is_bound = is_bound
if def_extras:
self.def_extras = def_extras
elif isinstance(definition, FuncDef):
# This information would be lost if we don't have definition
# after serialization, but it is useful in error messages.
# TODO: decide how to add more info here (file, line, column)
# without changing interface hash.
first_arg: str | None = None
if definition.arg_names and definition.info and not definition.is_static:
if getattr(definition, "arguments", None):
first_arg = definition.arguments[0].variable.name
else:
first_arg = definition.arg_names[0]
self.def_extras = {"first_arg": first_arg}
else:
self.def_extras = {}
self.type_guard = type_guard
self.type_is = type_is
self.unpack_kwargs = unpack_kwargs
Expand All @@ -1939,7 +1911,6 @@ def copy_modified(
special_sig: Bogus[str | None] = _dummy,
from_type_type: Bogus[bool] = _dummy,
is_bound: Bogus[bool] = _dummy,
def_extras: Bogus[dict[str, Any]] = _dummy,
type_guard: Bogus[Type | None] = _dummy,
type_is: Bogus[Type | None] = _dummy,
from_concatenate: Bogus[bool] = _dummy,
Expand All @@ -1964,7 +1935,6 @@ 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,
def_extras=def_extras if def_extras is not _dummy else dict(self.def_extras),
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 @@ -2291,7 +2261,6 @@ def serialize(self) -> JsonDict:
"is_ellipsis_args": self.is_ellipsis_args,
"implicit": self.implicit,
"is_bound": self.is_bound,
"def_extras": dict(self.def_extras),
"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 @@ -2314,7 +2283,6 @@ def deserialize(cls, data: JsonDict) -> CallableType:
is_ellipsis_args=data["is_ellipsis_args"],
implicit=data["implicit"],
is_bound=data["is_bound"],
def_extras=data["def_extras"],
type_guard=(
deserialize_type(data["type_guard"]) if data["type_guard"] is not None else None
),
Expand Down
1 change: 1 addition & 0 deletions test-data/unit/check-serialize.test
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,7 @@ def f(x: int) -> int: pass
[out2]
tmp/a.py:2: note: Revealed type is "builtins.str"
tmp/a.py:3: error: Unexpected keyword argument "x" for "f"
tmp/b.py: note: "f" defined here

[case testSerializeTypeGuardFunction]
import a
Expand Down
Loading