diff --git a/mypy/plugins/attrs.py b/mypy/plugins/attrs.py index 16e8891e5f57..381024fcc8e2 100644 --- a/mypy/plugins/attrs.py +++ b/mypy/plugins/attrs.py @@ -883,3 +883,22 @@ def add_method( """ self_type = self_type if self_type is not None else self.self_type add_method(self.ctx, method_name, args, ret_type, self_type, tvd) + + +def evolve_callback(ctx: mypy.plugin.FunctionSigContext) -> FunctionLike: + """Callback to provide an accurate signature for attrs.evolve.""" + if len(ctx.args[0]) < 1: + return ctx.default_signature + + metadata = ctx.args[0][0].node.type.type.metadata + + args = { + md_attribute['name']: ctx.api.named_generic_type(md_attribute['init_type'], args=[]) + for md_attribute in metadata.get('attrs', {}).get('attributes', []) + } + + return ctx.default_signature.copy_modified( + arg_kinds=ctx.default_signature.arg_kinds[:1] + [ARG_NAMED_OPT] * len(args), + arg_names=ctx.default_signature.arg_names[:1] + list(args.keys()), + arg_types=ctx.default_signature.arg_types[:1] + list(args.values()), + ) diff --git a/mypy/plugins/default.py b/mypy/plugins/default.py index 04971868e8f4..d9e96ee083c5 100644 --- a/mypy/plugins/default.py +++ b/mypy/plugins/default.py @@ -9,6 +9,7 @@ AttributeContext, ClassDefContext, FunctionContext, + FunctionSigContext, MethodContext, MethodSigContext, Plugin, @@ -45,6 +46,15 @@ def get_function_hook(self, fullname: str) -> Callable[[FunctionContext], Type] return singledispatch.create_singledispatch_function_callback return None + def get_function_signature_hook( + self, fullname: str + ) -> Callable[[FunctionSigContext], FunctionLike] | None: + from mypy.plugins import attrs + + if fullname in ("attr.evolve", "attr.assoc"): + return attrs.evolve_callback + return None + def get_method_signature_hook( self, fullname: str ) -> Callable[[MethodSigContext], FunctionLike] | None: diff --git a/test-data/unit/check-attr.test b/test-data/unit/check-attr.test index f555f2ea7011..9b90ec48935c 100644 --- a/test-data/unit/check-attr.test +++ b/test-data/unit/check-attr.test @@ -1866,4 +1866,38 @@ reveal_type(D) # N: Revealed type is "def (a: builtins.int, b: builtins.str) -> D(1, "").a = 2 # E: Cannot assign to final attribute "a" D(1, "").b = "2" # E: Cannot assign to final attribute "b" -[builtins fixtures/property.pyi] \ No newline at end of file +[builtins fixtures/property.pyi] + +[case testEvolve] +import attr + +@attr.s(auto_attribs=True) +class C: + name: str + +c = C(name='foo') +attr.evolve() # E: Missing positional argument "inst" in call to "evolve" +attr.evolve(c) +attr.evolve(c, name='bar') +attr.evolve( + c, + name=42, # E: Argument "name" to "evolve" has incompatible type "int"; expected "str" +) +attr.evolve( + c, + age=42, # type: ignore[call-arg] +) + +# 'assoc' is the deprecated one: + +attr.assoc( + c, + name=42, # E: Argument "name" to "assoc" has incompatible type "int"; expected "str" +) +attr.assoc( + c, + age=42, # type: ignore[call-arg] +) + +[builtins fixtures/dict.pyi] +[typing fixtures/typing-medium.pyi] diff --git a/test-data/unit/lib-stub/attr/__init__.pyi b/test-data/unit/lib-stub/attr/__init__.pyi index 795e5d3f4f69..1a3838aa3ab1 100644 --- a/test-data/unit/lib-stub/attr/__init__.pyi +++ b/test-data/unit/lib-stub/attr/__init__.pyi @@ -244,3 +244,6 @@ def field( order: Optional[bool] = ..., on_setattr: Optional[object] = ..., ) -> Any: ... + +def evolve(inst: _T, **changes: Any) -> _T: ... +def assoc(inst: _T, **changes: Any) -> _T: ...