From 259a84b85e817cdf8f2a468d55a9d8e9776a7808 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sun, 3 Aug 2025 22:24:00 +0100 Subject: [PATCH 1/3] Do not use dictionary in CallableType --- mypy/fixup.py | 8 +++++++ mypy/messages.py | 10 +++++++-- mypy/nodes.py | 9 ++++++++ mypy/types.py | 34 +---------------------------- test-data/unit/check-serialize.test | 1 + 5 files changed, 27 insertions(+), 35 deletions(-) diff --git a/mypy/fixup.py b/mypy/fixup.py index c0f8e401777c..0007fe8faabf 100644 --- a/mypy/fixup.py +++ b/mypy/fixup.py @@ -29,6 +29,7 @@ Overloaded, Parameters, ParamSpecType, + ProperType, TupleType, TypeAliasType, TypedDictType, @@ -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: @@ -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: diff --git a/mypy/messages.py b/mypy/messages.py index 44ed25a19517..6b55da59d183 100644 --- a/mypy/messages.py +++ b/mypy/messages.py @@ -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 " @@ -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 @@ -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" diff --git a/mypy/nodes.py b/mypy/nodes.py index 011e4e703a0c..6ffe579efe71 100644 --- a/mypy/nodes.py +++ b/mypy/nodes.py @@ -823,6 +823,7 @@ class FuncDef(FuncItem, SymbolNode, Statement): "dataclass_transform_spec", "docstring", "deprecated", + "original_first_arg", ) __match_args__ = ("name", "arguments", "type", "body") @@ -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: @@ -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 @@ -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 = ( diff --git a/mypy/types.py b/mypy/types.py index 4b5ef332ccf9..029477c1d5c4 100644 --- a/mypy/types.py +++ b/mypy/types.py @@ -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 @@ -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 @@ -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, @@ -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 @@ -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, @@ -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=( @@ -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, @@ -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 ), diff --git a/test-data/unit/check-serialize.test b/test-data/unit/check-serialize.test index 5265832f5f27..03c185a5694b 100644 --- a/test-data/unit/check-serialize.test +++ b/test-data/unit/check-serialize.test @@ -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 From 10ca489322f175aff913ee4089966e4d23a2e760 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Mon, 4 Aug 2025 01:17:09 +0100 Subject: [PATCH 2/3] Use FuncDef as definition for overload items --- mypy/checker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/checker.py b/mypy/checker.py index dfbfa753d5f2..d3a39a128ed9 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -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) From 365da145cef1082a3c92c8791916ddbf44789a25 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Mon, 4 Aug 2025 02:38:50 +0100 Subject: [PATCH 3/3] Fix a minor bug from a previous PR --- mypy/checkexpr.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 740efb0d2ee4..1b10370b08cb 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -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): @@ -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(