From 0afc1159557ef0fe1fcb22e66416b7542c9d79f1 Mon Sep 17 00:00:00 2001 From: Tin Tvrtkovic Date: Sun, 9 Apr 2023 03:44:42 +0200 Subject: [PATCH 01/10] Add support for `attrs.fields` --- mypy/plugins/attrs.py | 26 ++++++++++++++++++++- mypy/plugins/default.py | 4 +++- test-data/unit/check-plugin-attrs.test | 27 ++++++++++++++++++++++ test-data/unit/lib-stub/attr/__init__.pyi | 2 ++ test-data/unit/lib-stub/attrs/__init__.pyi | 2 ++ 5 files changed, 59 insertions(+), 2 deletions(-) diff --git a/mypy/plugins/attrs.py b/mypy/plugins/attrs.py index e4328d764be62..3deade74f4193 100644 --- a/mypy/plugins/attrs.py +++ b/mypy/plugins/attrs.py @@ -46,7 +46,7 @@ Var, is_class_var, ) -from mypy.plugin import SemanticAnalyzerPluginInterface +from mypy.plugin import FunctionContext, SemanticAnalyzerPluginInterface from mypy.plugins.common import ( _get_argument, _get_bool_argument, @@ -1060,3 +1060,27 @@ def evolve_function_sig_callback(ctx: mypy.plugin.FunctionSigContext) -> Callabl fallback=ctx.default_signature.fallback, name=f"{ctx.default_signature.name} of {inst_type_str}", ) + + +def _get_cls_from_init(t: Type) -> TypeInfo | None: + if isinstance(t, CallableType): + return t.type_object() + return None + + +def fields_function_callback(ctx: FunctionContext) -> Type: + """Provide the proper return value for `attrs.fields`.""" + if ctx.arg_types and ctx.arg_types[0] and ctx.arg_types[0][0]: + first_arg_type = ctx.arg_types[0][0] + cls = _get_cls_from_init(first_arg_type) + if cls is not None: + if MAGIC_ATTR_NAME in cls.names: + # This is a proper attrs class. + ret_type = cls.names[MAGIC_ATTR_NAME].type + return ret_type + else: + ctx.api.fail( + f'Argument 1 to "fields" has incompatible type "{format_type_bare(first_arg_type)}"; expected an attrs class', + ctx.context, + ) + return ctx.default_return_type diff --git a/mypy/plugins/default.py b/mypy/plugins/default.py index 1edc91a1183c0..49b6b76e22e57 100644 --- a/mypy/plugins/default.py +++ b/mypy/plugins/default.py @@ -39,12 +39,14 @@ class DefaultPlugin(Plugin): """Type checker plugin that is enabled by default.""" def get_function_hook(self, fullname: str) -> Callable[[FunctionContext], Type] | None: - from mypy.plugins import ctypes, singledispatch + from mypy.plugins import attrs, ctypes, singledispatch if fullname == "_ctypes.Array": return ctypes.array_constructor_callback elif fullname == "functools.singledispatch": return singledispatch.create_singledispatch_function_callback + elif fullname in ("attr.fields", "attrs.fields"): + return attrs.fields_function_callback return None def get_function_signature_hook( diff --git a/test-data/unit/check-plugin-attrs.test b/test-data/unit/check-plugin-attrs.test index ce1d670431c78..b63affb819ccd 100644 --- a/test-data/unit/check-plugin-attrs.test +++ b/test-data/unit/check-plugin-attrs.test @@ -1548,6 +1548,33 @@ takes_attrs_cls(A(1, "")) # E: Argument 1 to "takes_attrs_cls" has incompatible takes_attrs_instance(A) # E: Argument 1 to "takes_attrs_instance" has incompatible type "Type[A]"; expected "AttrsInstance" # N: ClassVar protocol member AttrsInstance.__attrs_attrs__ can never be matched by a class object [builtins fixtures/plugin_attrs.pyi] +[case testAttrsFields] +import attr +from attrs import fields + +@attr.define +class A: + b: int + c: str + +reveal_type(fields(A)) # N: Revealed type is "Tuple[attr.Attribute[builtins.int], attr.Attribute[builtins.str], fallback=__main__.A.____main___A_AttrsAttributes__]" +reveal_type(fields(A)[0]) # N: Revealed type is "attr.Attribute[builtins.int]" +reveal_type(fields(A).b) # N: Revealed type is "attr.Attribute[builtins.int]" +fields(A).x # E: "____main___A_AttrsAttributes__" has no attribute "x" + +[builtins fixtures/attr.pyi] + +[case testNonattrsFields] +from attrs import fields + +class A: + b: int + c: str + +fields(A) # E: Argument 1 to "fields" has incompatible type "Type[A]"; expected an attrs class + +[builtins fixtures/attr.pyi] + [case testAttrsInitMethodAlwaysGenerates] from typing import Tuple import attr diff --git a/test-data/unit/lib-stub/attr/__init__.pyi b/test-data/unit/lib-stub/attr/__init__.pyi index 1a3838aa3ab1f..17105e92dd9d8 100644 --- a/test-data/unit/lib-stub/attr/__init__.pyi +++ b/test-data/unit/lib-stub/attr/__init__.pyi @@ -247,3 +247,5 @@ def field( def evolve(inst: _T, **changes: Any) -> _T: ... def assoc(inst: _T, **changes: Any) -> _T: ... + +def fields(cls: _C) -> Any: ... diff --git a/test-data/unit/lib-stub/attrs/__init__.pyi b/test-data/unit/lib-stub/attrs/__init__.pyi index 8e9aa1fdced57..a8c4ef4a82cd2 100644 --- a/test-data/unit/lib-stub/attrs/__init__.pyi +++ b/test-data/unit/lib-stub/attrs/__init__.pyi @@ -129,3 +129,5 @@ def field( def evolve(inst: _T, **changes: Any) -> _T: ... def assoc(inst: _T, **changes: Any) -> _T: ... + +def fields(cls: _C) -> Any: ... From eb9cccfa72e49e60c36f6f541a3fe9b5b057a7b2 Mon Sep 17 00:00:00 2001 From: Tin Tvrtkovic Date: Sun, 9 Apr 2023 04:02:40 +0200 Subject: [PATCH 02/10] Fix self check --- mypy/plugins/attrs.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/mypy/plugins/attrs.py b/mypy/plugins/attrs.py index 3deade74f4193..05d2176d50d5a 100644 --- a/mypy/plugins/attrs.py +++ b/mypy/plugins/attrs.py @@ -1063,8 +1063,9 @@ def evolve_function_sig_callback(ctx: mypy.plugin.FunctionSigContext) -> Callabl def _get_cls_from_init(t: Type) -> TypeInfo | None: - if isinstance(t, CallableType): - return t.type_object() + proper_type = get_proper_type(t) + if isinstance(proper_type, CallableType): + return proper_type.type_object() return None @@ -1077,7 +1078,8 @@ def fields_function_callback(ctx: FunctionContext) -> Type: if MAGIC_ATTR_NAME in cls.names: # This is a proper attrs class. ret_type = cls.names[MAGIC_ATTR_NAME].type - return ret_type + if ret_type is not None: + return ret_type else: ctx.api.fail( f'Argument 1 to "fields" has incompatible type "{format_type_bare(first_arg_type)}"; expected an attrs class', From 7126c053724126bbb2ec0431aaa0db6c614c8de9 Mon Sep 17 00:00:00 2001 From: Tin Tvrtkovic Date: Sun, 9 Apr 2023 21:14:34 +0200 Subject: [PATCH 03/10] Refactor to support typevars, and more tests --- mypy/plugins/attrs.py | 50 ++++++++++++++-------- mypy/plugins/default.py | 7 +-- test-data/unit/check-plugin-attrs.test | 33 ++++++++++++-- test-data/unit/lib-stub/attr/__init__.pyi | 2 +- test-data/unit/lib-stub/attrs/__init__.pyi | 2 +- 5 files changed, 67 insertions(+), 27 deletions(-) diff --git a/mypy/plugins/attrs.py b/mypy/plugins/attrs.py index 05d2176d50d5a..ce04fd0dfb4bd 100644 --- a/mypy/plugins/attrs.py +++ b/mypy/plugins/attrs.py @@ -46,7 +46,7 @@ Var, is_class_var, ) -from mypy.plugin import FunctionContext, SemanticAnalyzerPluginInterface +from mypy.plugin import SemanticAnalyzerPluginInterface from mypy.plugins.common import ( _get_argument, _get_bool_argument, @@ -1062,27 +1062,41 @@ def evolve_function_sig_callback(ctx: mypy.plugin.FunctionSigContext) -> Callabl ) -def _get_cls_from_init(t: Type) -> TypeInfo | None: - proper_type = get_proper_type(t) - if isinstance(proper_type, CallableType): - return proper_type.type_object() - return None +def fields_function_sig_callback(ctx: mypy.plugin.FunctionSigContext) -> CallableType: + """Provide the proper signature for `attrs.fields`.""" + if ctx.args and len(ctx.args) == 1 and ctx.args[0] and ctx.args[0][0]: + # + assert isinstance(ctx.api, TypeChecker) + inst_type = ctx.api.expr_checker.accept(ctx.args[0][0]) + # + proper_type = get_proper_type(inst_type) + if isinstance(proper_type, AnyType): # fields(Any) -> Any + return ctx.default_signature + + cls = None + arg_types = ctx.default_signature.arg_types + + if isinstance(proper_type, TypeVarType): + inner = get_proper_type(proper_type.upper_bound) + if isinstance(inner, Instance): + # We need to work arg_types to compensate for the attrs stubs. + arg_types = [inst_type] + cls = inner.type + elif isinstance(proper_type, CallableType): + cls = proper_type.type_object() -def fields_function_callback(ctx: FunctionContext) -> Type: - """Provide the proper return value for `attrs.fields`.""" - if ctx.arg_types and ctx.arg_types[0] and ctx.arg_types[0][0]: - first_arg_type = ctx.arg_types[0][0] - cls = _get_cls_from_init(first_arg_type) if cls is not None: if MAGIC_ATTR_NAME in cls.names: # This is a proper attrs class. ret_type = cls.names[MAGIC_ATTR_NAME].type if ret_type is not None: - return ret_type - else: - ctx.api.fail( - f'Argument 1 to "fields" has incompatible type "{format_type_bare(first_arg_type)}"; expected an attrs class', - ctx.context, - ) - return ctx.default_return_type + return ctx.default_signature.copy_modified( + arg_types=arg_types, ret_type=ret_type + ) + + ctx.api.fail( + f'Argument 1 to "fields" has incompatible type "{format_type_bare(proper_type)}"; expected an attrs class', + ctx.context, + ) + return ctx.default_signature diff --git a/mypy/plugins/default.py b/mypy/plugins/default.py index 49b6b76e22e57..500eef76a9d98 100644 --- a/mypy/plugins/default.py +++ b/mypy/plugins/default.py @@ -39,14 +39,13 @@ class DefaultPlugin(Plugin): """Type checker plugin that is enabled by default.""" def get_function_hook(self, fullname: str) -> Callable[[FunctionContext], Type] | None: - from mypy.plugins import attrs, ctypes, singledispatch + from mypy.plugins import ctypes, singledispatch if fullname == "_ctypes.Array": return ctypes.array_constructor_callback elif fullname == "functools.singledispatch": return singledispatch.create_singledispatch_function_callback - elif fullname in ("attr.fields", "attrs.fields"): - return attrs.fields_function_callback + return None def get_function_signature_hook( @@ -56,6 +55,8 @@ def get_function_signature_hook( if fullname in ("attr.evolve", "attrs.evolve", "attr.assoc", "attrs.assoc"): return attrs.evolve_function_sig_callback + elif fullname in ("attr.fields", "attrs.fields"): + return attrs.fields_function_sig_callback return None def get_method_signature_hook( diff --git a/test-data/unit/check-plugin-attrs.test b/test-data/unit/check-plugin-attrs.test index b63affb819ccd..6636e16a42cb8 100644 --- a/test-data/unit/check-plugin-attrs.test +++ b/test-data/unit/check-plugin-attrs.test @@ -1549,6 +1549,24 @@ takes_attrs_instance(A) # E: Argument 1 to "takes_attrs_instance" has incompati [builtins fixtures/plugin_attrs.pyi] [case testAttrsFields] +import attr +from attrs import fields as f # Common usage. + +@attr.define +class A: + b: int + c: str + +reveal_type(f(A)) # N: Revealed type is "Tuple[attr.Attribute[builtins.int], attr.Attribute[builtins.str], fallback=__main__.A.____main___A_AttrsAttributes__]" +reveal_type(f(A)[0]) # N: Revealed type is "attr.Attribute[builtins.int]" +reveal_type(f(A).b) # N: Revealed type is "attr.Attribute[builtins.int]" +f(A).x # E: "____main___A_AttrsAttributes__" has no attribute "x" + +[builtins fixtures/attr.pyi] + +[case testAttrsGenericFields] +from typing import TypeVar + import attr from attrs import fields @@ -1557,14 +1575,19 @@ class A: b: int c: str -reveal_type(fields(A)) # N: Revealed type is "Tuple[attr.Attribute[builtins.int], attr.Attribute[builtins.str], fallback=__main__.A.____main___A_AttrsAttributes__]" -reveal_type(fields(A)[0]) # N: Revealed type is "attr.Attribute[builtins.int]" -reveal_type(fields(A).b) # N: Revealed type is "attr.Attribute[builtins.int]" -fields(A).x # E: "____main___A_AttrsAttributes__" has no attribute "x" +TA = TypeVar('TA', bound=A) + +def f(t: TA) -> None: + reveal_type(fields(t)) # N: Revealed type is "Tuple[attr.Attribute[builtins.int], attr.Attribute[builtins.str], fallback=__main__.A.____main___A_AttrsAttributes__]" + reveal_type(fields(t)[0]) # N: Revealed type is "attr.Attribute[builtins.int]" + reveal_type(fields(t).b) # N: Revealed type is "attr.Attribute[builtins.int]" + fields(t).x # E: "____main___A_AttrsAttributes__" has no attribute "x" + [builtins fixtures/attr.pyi] [case testNonattrsFields] +from typing import Any, cast from attrs import fields class A: @@ -1572,6 +1595,8 @@ class A: c: str fields(A) # E: Argument 1 to "fields" has incompatible type "Type[A]"; expected an attrs class +fields(None) # E: Argument 1 to "fields" has incompatible type "None"; expected an attrs class +fields(cast(Any, 42)) [builtins fixtures/attr.pyi] diff --git a/test-data/unit/lib-stub/attr/__init__.pyi b/test-data/unit/lib-stub/attr/__init__.pyi index 17105e92dd9d8..24ffc0f3f275c 100644 --- a/test-data/unit/lib-stub/attr/__init__.pyi +++ b/test-data/unit/lib-stub/attr/__init__.pyi @@ -248,4 +248,4 @@ def field( def evolve(inst: _T, **changes: Any) -> _T: ... def assoc(inst: _T, **changes: Any) -> _T: ... -def fields(cls: _C) -> Any: ... +def fields(cls: type) -> Any: ... diff --git a/test-data/unit/lib-stub/attrs/__init__.pyi b/test-data/unit/lib-stub/attrs/__init__.pyi index a8c4ef4a82cd2..cc09ce9b0b493 100644 --- a/test-data/unit/lib-stub/attrs/__init__.pyi +++ b/test-data/unit/lib-stub/attrs/__init__.pyi @@ -130,4 +130,4 @@ def field( def evolve(inst: _T, **changes: Any) -> _T: ... def assoc(inst: _T, **changes: Any) -> _T: ... -def fields(cls: _C) -> Any: ... +def fields(cls: type) -> Any: ... From c95478d93b621bf8a93b74f800bf96964c772390 Mon Sep 17 00:00:00 2001 From: Tin Tvrtkovic Date: Thu, 11 May 2023 01:10:16 +0200 Subject: [PATCH 04/10] Update tests --- mypy/plugins/attrs.py | 2 +- test-data/unit/check-plugin-attrs.test | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/mypy/plugins/attrs.py b/mypy/plugins/attrs.py index ce04fd0dfb4bd..8d916fa76caff 100644 --- a/mypy/plugins/attrs.py +++ b/mypy/plugins/attrs.py @@ -1096,7 +1096,7 @@ def fields_function_sig_callback(ctx: mypy.plugin.FunctionSigContext) -> Callabl ) ctx.api.fail( - f'Argument 1 to "fields" has incompatible type "{format_type_bare(proper_type)}"; expected an attrs class', + f'Argument 1 to "fields" has incompatible type "{format_type_bare(proper_type, ctx.api.options)}"; expected an attrs class', ctx.context, ) return ctx.default_signature diff --git a/test-data/unit/check-plugin-attrs.test b/test-data/unit/check-plugin-attrs.test index 6636e16a42cb8..fd24226b75b83 100644 --- a/test-data/unit/check-plugin-attrs.test +++ b/test-data/unit/check-plugin-attrs.test @@ -1562,7 +1562,7 @@ reveal_type(f(A)[0]) # N: Revealed type is "attr.Attribute[builtins.int]" reveal_type(f(A).b) # N: Revealed type is "attr.Attribute[builtins.int]" f(A).x # E: "____main___A_AttrsAttributes__" has no attribute "x" -[builtins fixtures/attr.pyi] +[builtins fixtures/plugin_attrs.pyi] [case testAttrsGenericFields] from typing import TypeVar @@ -1584,7 +1584,7 @@ def f(t: TA) -> None: fields(t).x # E: "____main___A_AttrsAttributes__" has no attribute "x" -[builtins fixtures/attr.pyi] +[builtins fixtures/plugin_attrs.pyi] [case testNonattrsFields] from typing import Any, cast @@ -1598,7 +1598,7 @@ fields(A) # E: Argument 1 to "fields" has incompatible type "Type[A]"; expected fields(None) # E: Argument 1 to "fields" has incompatible type "None"; expected an attrs class fields(cast(Any, 42)) -[builtins fixtures/attr.pyi] +[builtins fixtures/plugin_attrs.pyi] [case testAttrsInitMethodAlwaysGenerates] from typing import Tuple From e7d152dd819369facc536523146c3f648f65a794 Mon Sep 17 00:00:00 2001 From: Tin Tvrtkovic Date: Fri, 12 May 2023 00:49:48 +0200 Subject: [PATCH 05/10] attrs.fields: ignore type[Any] --- mypy/plugins/attrs.py | 8 +++++++- test-data/unit/check-plugin-attrs.test | 3 ++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/mypy/plugins/attrs.py b/mypy/plugins/attrs.py index 8d916fa76caff..eef45e32fdb49 100644 --- a/mypy/plugins/attrs.py +++ b/mypy/plugins/attrs.py @@ -69,6 +69,7 @@ TupleType, Type, TypeOfAny, + TypeType, TypeVarType, UninhabitedType, UnionType, @@ -1071,7 +1072,12 @@ def fields_function_sig_callback(ctx: mypy.plugin.FunctionSigContext) -> Callabl # proper_type = get_proper_type(inst_type) - if isinstance(proper_type, AnyType): # fields(Any) -> Any + # fields(Any) -> Any, fields(type[Any]) -> Any + if ( + isinstance(proper_type, AnyType) + or isinstance(proper_type, TypeType) + and isinstance(proper_type.item, AnyType) + ): return ctx.default_signature cls = None diff --git a/test-data/unit/check-plugin-attrs.test b/test-data/unit/check-plugin-attrs.test index fd24226b75b83..8af6f081bf340 100644 --- a/test-data/unit/check-plugin-attrs.test +++ b/test-data/unit/check-plugin-attrs.test @@ -1587,7 +1587,7 @@ def f(t: TA) -> None: [builtins fixtures/plugin_attrs.pyi] [case testNonattrsFields] -from typing import Any, cast +from typing import Any, cast, Type from attrs import fields class A: @@ -1597,6 +1597,7 @@ class A: fields(A) # E: Argument 1 to "fields" has incompatible type "Type[A]"; expected an attrs class fields(None) # E: Argument 1 to "fields" has incompatible type "None"; expected an attrs class fields(cast(Any, 42)) +fields(cast(Type[Any], 43)) [builtins fixtures/plugin_attrs.pyi] From f81aff2ffcb453edfa7fd88c3abd5644241ba89b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tin=20Tvrtkovi=C4=87?= Date: Fri, 12 May 2023 01:25:36 +0200 Subject: [PATCH 06/10] Update mypy/plugins/attrs.py Co-authored-by: Ilya Priven --- mypy/plugins/attrs.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/mypy/plugins/attrs.py b/mypy/plugins/attrs.py index eef45e32fdb49..418979c011386 100644 --- a/mypy/plugins/attrs.py +++ b/mypy/plugins/attrs.py @@ -1092,8 +1092,7 @@ def fields_function_sig_callback(ctx: mypy.plugin.FunctionSigContext) -> Callabl elif isinstance(proper_type, CallableType): cls = proper_type.type_object() - if cls is not None: - if MAGIC_ATTR_NAME in cls.names: + if cls is not None and MAGIC_ATTR_NAME in cls.names: # This is a proper attrs class. ret_type = cls.names[MAGIC_ATTR_NAME].type if ret_type is not None: From 03846783c36c53251acde2d6e7288c2d8825b397 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 11 May 2023 23:26:01 +0000 Subject: [PATCH 07/10] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mypy/plugins/attrs.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/mypy/plugins/attrs.py b/mypy/plugins/attrs.py index 418979c011386..682e5eb4569be 100644 --- a/mypy/plugins/attrs.py +++ b/mypy/plugins/attrs.py @@ -1093,12 +1093,10 @@ def fields_function_sig_callback(ctx: mypy.plugin.FunctionSigContext) -> Callabl cls = proper_type.type_object() if cls is not None and MAGIC_ATTR_NAME in cls.names: - # This is a proper attrs class. - ret_type = cls.names[MAGIC_ATTR_NAME].type - if ret_type is not None: - return ctx.default_signature.copy_modified( - arg_types=arg_types, ret_type=ret_type - ) + # This is a proper attrs class. + ret_type = cls.names[MAGIC_ATTR_NAME].type + if ret_type is not None: + return ctx.default_signature.copy_modified(arg_types=arg_types, ret_type=ret_type) ctx.api.fail( f'Argument 1 to "fields" has incompatible type "{format_type_bare(proper_type, ctx.api.options)}"; expected an attrs class', From b5cf588459f0048ba7ae3c427a43bea6e530938f Mon Sep 17 00:00:00 2001 From: Tin Tvrtkovic Date: Fri, 12 May 2023 12:45:48 +0200 Subject: [PATCH 08/10] attrs.fields: early exit --- mypy/plugins/attrs.py | 75 ++++++++++++++++++++++--------------------- 1 file changed, 39 insertions(+), 36 deletions(-) diff --git a/mypy/plugins/attrs.py b/mypy/plugins/attrs.py index 682e5eb4569be..e94a781eebeaa 100644 --- a/mypy/plugins/attrs.py +++ b/mypy/plugins/attrs.py @@ -1065,41 +1065,44 @@ def evolve_function_sig_callback(ctx: mypy.plugin.FunctionSigContext) -> Callabl def fields_function_sig_callback(ctx: mypy.plugin.FunctionSigContext) -> CallableType: """Provide the proper signature for `attrs.fields`.""" - if ctx.args and len(ctx.args) == 1 and ctx.args[0] and ctx.args[0][0]: - # - assert isinstance(ctx.api, TypeChecker) - inst_type = ctx.api.expr_checker.accept(ctx.args[0][0]) - # - proper_type = get_proper_type(inst_type) - - # fields(Any) -> Any, fields(type[Any]) -> Any - if ( - isinstance(proper_type, AnyType) - or isinstance(proper_type, TypeType) - and isinstance(proper_type.item, AnyType) - ): - return ctx.default_signature - - cls = None - arg_types = ctx.default_signature.arg_types - - if isinstance(proper_type, TypeVarType): - inner = get_proper_type(proper_type.upper_bound) - if isinstance(inner, Instance): - # We need to work arg_types to compensate for the attrs stubs. - arg_types = [inst_type] - cls = inner.type - elif isinstance(proper_type, CallableType): - cls = proper_type.type_object() - - if cls is not None and MAGIC_ATTR_NAME in cls.names: - # This is a proper attrs class. - ret_type = cls.names[MAGIC_ATTR_NAME].type - if ret_type is not None: - return ctx.default_signature.copy_modified(arg_types=arg_types, ret_type=ret_type) + if not ctx.args or len(ctx.args) != 1 or not ctx.args[0] or not ctx.args[0][0]: + return ctx.default_signature + + # + assert isinstance(ctx.api, TypeChecker) + inst_type = ctx.api.expr_checker.accept(ctx.args[0][0]) + # + proper_type = get_proper_type(inst_type) + + # fields(Any) -> Any, fields(type[Any]) -> Any + if ( + isinstance(proper_type, AnyType) + or isinstance(proper_type, TypeType) + and isinstance(proper_type.item, AnyType) + ): + return ctx.default_signature + + cls = None + arg_types = ctx.default_signature.arg_types + + if isinstance(proper_type, TypeVarType): + inner = get_proper_type(proper_type.upper_bound) + if isinstance(inner, Instance): + # We need to work arg_types to compensate for the attrs stubs. + arg_types = [inst_type] + cls = inner.type + elif isinstance(proper_type, CallableType): + cls = proper_type.type_object() + + if cls is not None and MAGIC_ATTR_NAME in cls.names: + # This is a proper attrs class. + ret_type = cls.names[MAGIC_ATTR_NAME].type + assert ret_type is not None + return ctx.default_signature.copy_modified(arg_types=arg_types, ret_type=ret_type) + + ctx.api.fail( + f'Argument 1 to "fields" has incompatible type "{format_type_bare(proper_type, ctx.api.options)}"; expected an attrs class', + ctx.context, + ) - ctx.api.fail( - f'Argument 1 to "fields" has incompatible type "{format_type_bare(proper_type, ctx.api.options)}"; expected an attrs class', - ctx.context, - ) return ctx.default_signature From 3e9537f2b1e067cb51b042ec8eecce27b34a51c2 Mon Sep 17 00:00:00 2001 From: Tin Tvrtkovic Date: Sat, 20 May 2023 01:56:35 +0200 Subject: [PATCH 09/10] attrs: tweak docstrings for consistency --- mypy/plugins/attrs.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/mypy/plugins/attrs.py b/mypy/plugins/attrs.py index e94a781eebeaa..4c96003ca326f 100644 --- a/mypy/plugins/attrs.py +++ b/mypy/plugins/attrs.py @@ -936,7 +936,7 @@ def add_method( def _get_attrs_init_type(typ: Instance) -> CallableType | None: """ - If `typ` refers to an attrs class, gets the type of its initializer method. + If `typ` refers to an attrs class, get the type of its initializer method. """ magic_attr = typ.type.get(MAGIC_ATTR_NAME) if magic_attr is None or not magic_attr.plugin_generated: @@ -1010,7 +1010,7 @@ def _get_expanded_attr_types( def _meet_fields(types: list[Mapping[str, Type]]) -> Mapping[str, Type]: """ - "Meets" the fields of a list of attrs classes, i.e. for each field, its new type will be the lower bound. + "Meet" the fields of a list of attrs classes, i.e. for each field, its new type will be the lower bound. """ field_to_types = defaultdict(list) for fields in types: @@ -1027,7 +1027,7 @@ def _meet_fields(types: list[Mapping[str, Type]]) -> Mapping[str, Type]: def evolve_function_sig_callback(ctx: mypy.plugin.FunctionSigContext) -> CallableType: """ - Generates a signature for the 'attr.evolve' function that's specific to the call site + Generate a signature for the 'attr.evolve' function that's specific to the call site and dependent on the type of the first argument. """ if len(ctx.args) != 2: @@ -1064,7 +1064,7 @@ def evolve_function_sig_callback(ctx: mypy.plugin.FunctionSigContext) -> Callabl def fields_function_sig_callback(ctx: mypy.plugin.FunctionSigContext) -> CallableType: - """Provide the proper signature for `attrs.fields`.""" + """Provide the signature for `attrs.fields`.""" if not ctx.args or len(ctx.args) != 1 or not ctx.args[0] or not ctx.args[0][0]: return ctx.default_signature From 0b7335a751ffd066182bae25a990046c622f5863 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 19 May 2023 23:57:10 +0000 Subject: [PATCH 10/10] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- test-data/unit/check-plugin-attrs.test | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-data/unit/check-plugin-attrs.test b/test-data/unit/check-plugin-attrs.test index 8af6f081bf340..e34408454a836 100644 --- a/test-data/unit/check-plugin-attrs.test +++ b/test-data/unit/check-plugin-attrs.test @@ -1582,7 +1582,7 @@ def f(t: TA) -> None: reveal_type(fields(t)[0]) # N: Revealed type is "attr.Attribute[builtins.int]" reveal_type(fields(t).b) # N: Revealed type is "attr.Attribute[builtins.int]" fields(t).x # E: "____main___A_AttrsAttributes__" has no attribute "x" - + [builtins fixtures/plugin_attrs.pyi]