From c13cf6d5ed573927b0c1fb1a4d8825f26d583b68 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Wed, 11 May 2022 14:31:12 +0100 Subject: [PATCH 01/14] Fix crashed related to forward references in attrs classes This is similar to #12762, but for attrs classes. --- mypy/plugins/attrs.py | 21 ++++++++++----------- mypy/plugins/default.py | 32 ++++++++++++++++---------------- test-data/unit/check-attr.test | 14 ++++++++++++++ 3 files changed, 40 insertions(+), 27 deletions(-) diff --git a/mypy/plugins/attrs.py b/mypy/plugins/attrs.py index 38fd2f040be5..0b1f5906fc39 100644 --- a/mypy/plugins/attrs.py +++ b/mypy/plugins/attrs.py @@ -260,7 +260,7 @@ def _get_decorator_optional_bool_argument( def attr_class_maker_callback(ctx: 'mypy.plugin.ClassDefContext', auto_attribs_default: Optional[bool] = False, - frozen_default: bool = False) -> None: + frozen_default: bool = False) -> bool: """Add necessary dunder methods to classes decorated with attr.s. attrs is a package that lets you define classes without writing dull boilerplate code. @@ -286,27 +286,24 @@ def attr_class_maker_callback(ctx: 'mypy.plugin.ClassDefContext', if ctx.api.options.python_version[0] < 3: if auto_attribs: ctx.api.fail("auto_attribs is not supported in Python 2", ctx.reason) - return + return True if not info.defn.base_type_exprs: # Note: This will not catch subclassing old-style classes. ctx.api.fail("attrs only works with new-style classes", info.defn) - return + return True if kw_only: ctx.api.fail(KW_ONLY_PYTHON_2_UNSUPPORTED, ctx.reason) - return + return True attributes = _analyze_class(ctx, auto_attribs, kw_only) # Check if attribute types are ready. for attr in attributes: node = info.get(attr.name) - if node is None: - # This name is likely blocked by a star import. We don't need to defer because - # defer() is already called by mark_incomplete(). - return - if node.type is None and not ctx.api.final_iteration: - ctx.api.defer() - return + if node is None or node.type is None: + # This name is likely blocked by some semantic analysis error that + # should have been reported already. + return True _add_attrs_magic_attribute(ctx, [(attr.name, info[attr.name].type) for attr in attributes]) if slots: @@ -330,6 +327,8 @@ def attr_class_maker_callback(ctx: 'mypy.plugin.ClassDefContext', if frozen: _make_frozen(ctx, attributes) + return True + def _get_frozen(ctx: 'mypy.plugin.ClassDefContext', frozen_default: bool) -> bool: """Return whether this class is frozen.""" diff --git a/mypy/plugins/default.py b/mypy/plugins/default.py index 0ae95eb040db..6552689d6592 100644 --- a/mypy/plugins/default.py +++ b/mypy/plugins/default.py @@ -94,10 +94,24 @@ def get_attribute_hook(self, fullname: str def get_class_decorator_hook(self, fullname: str ) -> Optional[Callable[[ClassDefContext], None]]: - from mypy.plugins import attrs from mypy.plugins import dataclasses - if fullname in attrs.attr_class_makers: + if fullname in dataclasses.dataclass_makers: + return dataclasses.dataclass_tag_callback + + return None + + def get_class_decorator_hook_2(self, fullname: str + ) -> Optional[Callable[[ClassDefContext], bool]]: + from mypy.plugins import dataclasses + from mypy.plugins import functools + from mypy.plugins import attrs + + if fullname in dataclasses.dataclass_makers: + return dataclasses.dataclass_class_maker_callback + elif fullname in functools.functools_total_ordering_makers: + return functools.functools_total_ordering_maker_callback + elif fullname in attrs.attr_class_makers: return attrs.attr_class_maker_callback elif fullname in attrs.attr_dataclass_makers: return partial( @@ -115,20 +129,6 @@ def get_class_decorator_hook(self, fullname: str attrs.attr_class_maker_callback, auto_attribs_default=None, ) - elif fullname in dataclasses.dataclass_makers: - return dataclasses.dataclass_tag_callback - - return None - - def get_class_decorator_hook_2(self, fullname: str - ) -> Optional[Callable[[ClassDefContext], bool]]: - from mypy.plugins import dataclasses - from mypy.plugins import functools - - if fullname in dataclasses.dataclass_makers: - return dataclasses.dataclass_class_maker_callback - elif fullname in functools.functools_total_ordering_makers: - return functools.functools_total_ordering_maker_callback return None diff --git a/test-data/unit/check-attr.test b/test-data/unit/check-attr.test index a69bd473624d..ae80a97e1f06 100644 --- a/test-data/unit/check-attr.test +++ b/test-data/unit/check-attr.test @@ -1591,3 +1591,17 @@ class B: class AB(A, B): pass [builtins fixtures/attr.pyi] + +[case testAttrsForwardReferenceInTypeVarBound] +from typing import TypeVar, Generic +import attr + +T = TypeVar("T", bound="C") + +@attr.define +class D(Generic[T]): + x: int + +class C: + pass +[builtins fixtures/attr.pyi] From ae920152f7f6154c1ece5e564d96da6ed18149c3 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Wed, 11 May 2022 14:51:51 +0100 Subject: [PATCH 02/14] Fix attributes without explicit types --- mypy/plugins/attrs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/plugins/attrs.py b/mypy/plugins/attrs.py index 0b1f5906fc39..d9283ec0051a 100644 --- a/mypy/plugins/attrs.py +++ b/mypy/plugins/attrs.py @@ -300,7 +300,7 @@ def attr_class_maker_callback(ctx: 'mypy.plugin.ClassDefContext', # Check if attribute types are ready. for attr in attributes: node = info.get(attr.name) - if node is None or node.type is None: + if node is None: # This name is likely blocked by some semantic analysis error that # should have been reported already. return True From a3c69e7f0490c5013929571129b3031b7abb0294 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Wed, 11 May 2022 15:34:34 +0100 Subject: [PATCH 03/14] Fix converters --- mypy/plugins/attrs.py | 50 ++++++++++++++++--------------------------- 1 file changed, 18 insertions(+), 32 deletions(-) diff --git a/mypy/plugins/attrs.py b/mypy/plugins/attrs.py index d9283ec0051a..8a816a7e634a 100644 --- a/mypy/plugins/attrs.py +++ b/mypy/plugins/attrs.py @@ -2,7 +2,7 @@ from mypy.backports import OrderedDict -from typing import Optional, Dict, List, cast, Tuple, Iterable +from typing import Optional, Dict, List, cast, Tuple, Iterable, Union from typing_extensions import Final import mypy.plugin # To avoid circular imports. @@ -23,7 +23,7 @@ from mypy.types import ( TupleType, Type, AnyType, TypeOfAny, CallableType, NoneType, TypeVarType, Overloaded, UnionType, FunctionLike, Instance, get_proper_type, - LiteralType, + LiteralType, deserialize_type ) from mypy.typeops import make_simplified_union, map_type_from_supertype from mypy.typevars import fill_typevars @@ -61,9 +61,9 @@ class Converter: """Holds information about a `converter=` argument""" def __init__(self, - name: Optional[str] = None, + type: Optional[Type] = None, is_attr_converters_optional: bool = False) -> None: - self.name = name + self.type = type self.is_attr_converters_optional = is_attr_converters_optional @@ -89,25 +89,10 @@ def argument(self, ctx: 'mypy.plugin.ClassDefContext') -> Argument: init_type = self.init_type or self.info[self.name].type - if self.converter.name: + if self.converter.type: # When a converter is set the init_type is overridden by the first argument # of the converter method. - converter = lookup_fully_qualified(self.converter.name, ctx.api.modules, - raise_on_missing=False) - if not converter: - # The converter may be a local variable. Check there too. - converter = ctx.api.lookup_qualified(self.converter.name, self.info, True) - - # Get the type of the converter. - converter_type: Optional[Type] = None - if converter and isinstance(converter.node, TypeInfo): - from mypy.checkmember import type_object_type # To avoid import cycle. - converter_type = type_object_type(converter.node, ctx.api.named_type) - elif converter and isinstance(converter.node, OverloadedFuncDef): - converter_type = converter.node.type - elif converter and converter.type: - converter_type = converter.type - + converter_type = self.converter.type init_type = None converter_type = get_proper_type(converter_type) if isinstance(converter_type, CallableType) and converter_type.arg_types: @@ -132,13 +117,10 @@ def argument(self, ctx: 'mypy.plugin.ClassDefContext') -> Argument: # the allowed init_type. init_type = UnionType.make_union([init_type, NoneType()]) - if not init_type: + if not init_type and not (isinstance(converter_type, AnyType) and + converter_type.type_of_any == TypeOfAny.from_error): ctx.api.fail("Cannot determine __init__ type from converter", self.context) init_type = AnyType(TypeOfAny.from_error) - elif self.converter.name == '': - # This means we had a converter but it's not of a type we can infer. - # Error was shown in _get_converter_name - init_type = AnyType(TypeOfAny.from_error) if init_type is None: if ctx.api.options.disallow_untyped_defs: @@ -170,7 +152,7 @@ def serialize(self) -> JsonDict: 'has_default': self.has_default, 'init': self.init, 'kw_only': self.kw_only, - 'converter_name': self.converter.name, + 'converter_type': self.converter.type.serialize() if self.converter.type else None, 'converter_is_attr_converters_optional': self.converter.is_attr_converters_optional, 'context_line': self.context.line, 'context_column': self.context.column, @@ -185,12 +167,15 @@ def deserialize(cls, info: TypeInfo, raw_init_type = data['init_type'] init_type = deserialize_and_fixup_type(raw_init_type, api) if raw_init_type else None + converter_type = None + if data['converter_type']: + converter_type = deserialize_and_fixup_type(data['converter_type'], api) return Attribute(data['name'], info, data['has_default'], data['init'], data['kw_only'], - Converter(data['converter_name'], data['converter_is_attr_converters_optional']), + Converter(converter_type, data['converter_is_attr_converters_optional']), Context(line=data['context_line'], column=data['context_column']), init_type) @@ -601,12 +586,13 @@ def _parse_converter(ctx: 'mypy.plugin.ClassDefContext', if (isinstance(converter.node, FuncDef) and converter.node.type and isinstance(converter.node.type, FunctionLike)): - return Converter(converter.node.fullname) + return Converter(converter.node.type) elif (isinstance(converter.node, OverloadedFuncDef) and is_valid_overloaded_converter(converter.node)): - return Converter(converter.node.fullname) + return Converter(converter.node.type) elif isinstance(converter.node, TypeInfo): - return Converter(converter.node.fullname) + from mypy.checkmember import type_object_type # To avoid import cycle. + return Converter(type_object_type(converter.node, ctx.api.named_type)) if (isinstance(converter, CallExpr) and isinstance(converter.callee, RefExpr) @@ -624,7 +610,7 @@ def _parse_converter(ctx: 'mypy.plugin.ClassDefContext', "Unsupported converter, only named functions and types are currently supported", converter ) - return Converter('') + return Converter(AnyType(TypeOfAny.from_error)) return Converter(None) From 89068ec1b8a9570cf5c1c392b9863980b53d1805 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Wed, 11 May 2022 15:35:18 +0100 Subject: [PATCH 04/14] Simplify --- 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 8a816a7e634a..8713f2e64db6 100644 --- a/mypy/plugins/attrs.py +++ b/mypy/plugins/attrs.py @@ -109,8 +109,7 @@ def argument(self, ctx: 'mypy.plugin.ClassDefContext') -> Argument: types.append(item.arg_types[0]) # Make a union of all the valid types. if types: - args = make_simplified_union(types) - init_type = ctx.api.anal_type(args) + init_type = make_simplified_union(types) if self.converter.is_attr_converters_optional and init_type: # If the converter was attr.converter.optional(type) then add None to From 32d00263062300f8d38b62700db7d0e03d7a3694 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Wed, 11 May 2022 15:36:36 +0100 Subject: [PATCH 05/14] Simplify more --- mypy/plugins/attrs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/plugins/attrs.py b/mypy/plugins/attrs.py index 8713f2e64db6..7e4e3cbc3d8a 100644 --- a/mypy/plugins/attrs.py +++ b/mypy/plugins/attrs.py @@ -96,7 +96,7 @@ def argument(self, ctx: 'mypy.plugin.ClassDefContext') -> Argument: init_type = None converter_type = get_proper_type(converter_type) if isinstance(converter_type, CallableType) and converter_type.arg_types: - init_type = ctx.api.anal_type(converter_type.arg_types[0]) + init_type = converter_type.arg_types[0] elif isinstance(converter_type, Overloaded): types: List[Type] = [] for item in converter_type.items: From d3e2b400fe96354910502ef6355ba86557566855 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Wed, 11 May 2022 15:48:42 +0100 Subject: [PATCH 06/14] Add test case --- test-data/unit/check-attr.test | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/test-data/unit/check-attr.test b/test-data/unit/check-attr.test index ae80a97e1f06..2a5dcfead609 100644 --- a/test-data/unit/check-attr.test +++ b/test-data/unit/check-attr.test @@ -1605,3 +1605,34 @@ class D(Generic[T]): class C: pass [builtins fixtures/attr.pyi] + +[case testComplexTypeInAttrIb] +import a + +[file a.py] +import attr +import b +from typing import Callable + +@attr.s +class C: + a = attr.ib(type=Lst[int]) + # Note that for this test, the 'Value of type "int" is not indexable' errors are silly, + # and a consequence of Callable etc. being set to an int in the test stub. + b = attr.ib(type=Callable[[], C]) +[builtins fixtures/bool.pyi] + +[file b.py] +import attr +import a +from typing import List as Lst, Optional + +@attr.s +class D: + a = attr.ib(type=Lst[int]) + b = attr.ib(type=Optional[int]) +[builtins fixtures/list.pyi] +[out] +tmp/b.py:8: error: Value of type "int" is not indexable +tmp/a.py:7: error: Name "Lst" is not defined +tmp/a.py:10: error: Value of type "int" is not indexable From 05e4526dc059b3aa20320f503243e800636a8fee Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Wed, 11 May 2022 15:49:56 +0100 Subject: [PATCH 07/14] Fix lint --- mypy/plugins/attrs.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/mypy/plugins/attrs.py b/mypy/plugins/attrs.py index 7e4e3cbc3d8a..aaebe29565b5 100644 --- a/mypy/plugins/attrs.py +++ b/mypy/plugins/attrs.py @@ -2,12 +2,11 @@ from mypy.backports import OrderedDict -from typing import Optional, Dict, List, cast, Tuple, Iterable, Union +from typing import Optional, Dict, List, cast, Tuple, Iterable from typing_extensions import Final import mypy.plugin # To avoid circular imports. from mypy.exprtotype import expr_to_unanalyzed_type, TypeTranslationError -from mypy.lookup import lookup_fully_qualified from mypy.nodes import ( Context, Argument, Var, ARG_OPT, ARG_POS, TypeInfo, AssignmentStmt, TupleExpr, ListExpr, NameExpr, CallExpr, RefExpr, FuncDef, @@ -23,7 +22,7 @@ from mypy.types import ( TupleType, Type, AnyType, TypeOfAny, CallableType, NoneType, TypeVarType, Overloaded, UnionType, FunctionLike, Instance, get_proper_type, - LiteralType, deserialize_type + LiteralType ) from mypy.typeops import make_simplified_union, map_type_from_supertype from mypy.typevars import fill_typevars From 91131b4d747c532535f506fb1ae039009d812456 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Thu, 12 May 2022 11:17:37 +0100 Subject: [PATCH 08/14] Also fix generic inheritance (now that it's possible) --- mypy/plugins/attrs.py | 3 --- test-data/unit/check-attr.test | 31 +++++++++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/mypy/plugins/attrs.py b/mypy/plugins/attrs.py index aaebe29565b5..8ce283d4b004 100644 --- a/mypy/plugins/attrs.py +++ b/mypy/plugins/attrs.py @@ -180,9 +180,6 @@ def deserialize(cls, info: TypeInfo, def expand_typevar_from_subtype(self, sub_type: TypeInfo) -> None: """Expands type vars in the context of a subtype when an attribute is inherited from a generic super type.""" - if not isinstance(self.init_type, TypeVarType): - return - self.init_type = map_type_from_supertype(self.init_type, sub_type, self.info) diff --git a/test-data/unit/check-attr.test b/test-data/unit/check-attr.test index 2a5dcfead609..e1566ffb9cef 100644 --- a/test-data/unit/check-attr.test +++ b/test-data/unit/check-attr.test @@ -544,6 +544,37 @@ reveal_type(sub.three) # N: Revealed type is "builtins.float" [builtins fixtures/bool.pyi] +[case testAttrsGenericInheritance3] +import attr +from typing import Any, Callable, Generic, TypeVar, List + +T = TypeVar("T") +S = TypeVar("S") + +@attr.s(auto_attribs=True) +class Parent(Generic[T]): + f: Callable[[T], Any] + +@attr.s(auto_attribs=True) +class Child(Parent[T]): ... + +class A: ... +def func(obj: A) -> bool: ... + +reveal_type(Child[A](func).f) # N: Revealed type is "def (__main__.A) -> Any" + +@attr.s(auto_attribs=True) +class Parent2(Generic[T]): + a: List[T] + +@attr.s(auto_attribs=True) +class Child2(Generic[T, S], Parent2[S]): + b: List[T] + +reveal_type(Child2([A()], [1]).a) # N: Revealed type is "builtins.list[__main__.A]" +reveal_type(Child2[int, A]([A()], [1]).b) # N: Revealed type is "builtins.list[builtins.int]" +[builtins fixtures/list.pyi] + [case testAttrsMultiGenericInheritance] from typing import Generic, TypeVar import attr From 4d3ab14d814bb6747e2b5f1fd74c5b3b4c939da5 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Thu, 12 May 2022 11:28:56 +0100 Subject: [PATCH 09/14] Detect base classes that aren't ready yet --- mypy/plugins/attrs.py | 18 +++++++++++++ mypy/plugins/default.py | 10 +++++++ test-data/unit/check-attr.test | 48 ++++++++++++++++++++++++++++++++++ 3 files changed, 76 insertions(+) diff --git a/mypy/plugins/attrs.py b/mypy/plugins/attrs.py index 8ce283d4b004..47b0401ee76b 100644 --- a/mypy/plugins/attrs.py +++ b/mypy/plugins/attrs.py @@ -238,6 +238,16 @@ def _get_decorator_optional_bool_argument( return default +def attr_tag_callback(ctx: 'mypy.plugin.ClassDefContext') -> None: + """Record that we have an attrs class in the main semantic analysis pass. + + The later pass implemented by attr_class_maker_callback will use this + to detect attrs lasses in base classes. + """ + # The value is ignored, only the existence matters. + ctx.cls.info.metadata['attrs_tag'] = {} + + def attr_class_maker_callback(ctx: 'mypy.plugin.ClassDefContext', auto_attribs_default: Optional[bool] = False, frozen_default: bool = False) -> bool: @@ -251,6 +261,9 @@ def attr_class_maker_callback(ctx: 'mypy.plugin.ClassDefContext', into properties. See http://www.attrs.org/en/stable/how-does-it-work.html for information on how attrs works. + + If this returns False, some required metadata was not ready yet and we need another + pass. """ info = ctx.cls.info @@ -275,6 +288,11 @@ def attr_class_maker_callback(ctx: 'mypy.plugin.ClassDefContext', ctx.api.fail(KW_ONLY_PYTHON_2_UNSUPPORTED, ctx.reason) return True + for super_info in ctx.cls.info.mro[1:-1]: + if 'attrs_tag' in super_info.metadata and 'attrs' not in super_info.metadata: + # Super class is not ready yet. Request another pass. + return False + attributes = _analyze_class(ctx, auto_attribs, kw_only) # Check if attribute types are ready. diff --git a/mypy/plugins/default.py b/mypy/plugins/default.py index 6552689d6592..40997803aa7e 100644 --- a/mypy/plugins/default.py +++ b/mypy/plugins/default.py @@ -95,9 +95,19 @@ def get_attribute_hook(self, fullname: str def get_class_decorator_hook(self, fullname: str ) -> Optional[Callable[[ClassDefContext], None]]: from mypy.plugins import dataclasses + from mypy.plugins import attrs + # These dataclass and attrs hooks run in the main semantic analysis pass + # and only tag known dataclasses/attrs classes, so that the second + # hooks (in get_class_decorator_hook_2) can detect dataclasses/attrs classes + # in the MRO. if fullname in dataclasses.dataclass_makers: return dataclasses.dataclass_tag_callback + if (fullname in attrs.attr_class_makers + or fullname in attrs.attr_dataclass_makers + or fullname in attrs.attr_frozen_makers + or fullname in attrs.attr_define_makers): + return attrs.attr_tag_callback return None diff --git a/test-data/unit/check-attr.test b/test-data/unit/check-attr.test index e1566ffb9cef..af422fbb5e1a 100644 --- a/test-data/unit/check-attr.test +++ b/test-data/unit/check-attr.test @@ -1667,3 +1667,51 @@ class D: tmp/b.py:8: error: Value of type "int" is not indexable tmp/a.py:7: error: Name "Lst" is not defined tmp/a.py:10: error: Value of type "int" is not indexable + +[case testAttrsGenericInheritanceSpecialCase1] +import attr +from typing import Generic, TypeVar, List + +T = TypeVar("T") + +@attr.define +class Parent(Generic[T]): + x: List[T] + +@attr.define +class Child1(Parent["Child2"]): ... + +@attr.define +class Child2(Parent["Child1"]): ... + +def f(c: Child2) -> None: + reveal_type(Child1([c]).x) # N: Revealed type is "builtins.list[__main__.Child2]" + +def g(c: Child1) -> None: + reveal_type(Child2([c]).x) # N: Revealed type is "builtins.list[__main__.Child1]" +[builtins fixtures/list.pyi] + +[case testAttrsGenericInheritanceSpecialCase2] +import attr +from typing import Generic, TypeVar + +T = TypeVar("T") + +# A subclass might be analyzed before base in import cycles. They are +# defined here in reversed order to simulate this. + +@attr.define +class Child1(Parent["Child2"]): + x: int + +@attr.define +class Child2(Parent["Child1"]): + y: int + +@attr.define +class Parent(Generic[T]): + key: str + +Child1(x=1, key='') +Child2(y=1, key='') +[builtins fixtures/list.pyi] From 4729b65a16a1e7bebdc0b795cebc056aded5d9d7 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Thu, 12 May 2022 11:30:36 +0100 Subject: [PATCH 10/14] Fix type check --- mypy/plugins/attrs.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/mypy/plugins/attrs.py b/mypy/plugins/attrs.py index 47b0401ee76b..9c8f79c32a89 100644 --- a/mypy/plugins/attrs.py +++ b/mypy/plugins/attrs.py @@ -180,7 +180,10 @@ def deserialize(cls, info: TypeInfo, def expand_typevar_from_subtype(self, sub_type: TypeInfo) -> None: """Expands type vars in the context of a subtype when an attribute is inherited from a generic super type.""" - self.init_type = map_type_from_supertype(self.init_type, sub_type, self.info) + if self.init_type: + self.init_type = map_type_from_supertype(self.init_type, sub_type, self.info) + else: + self.init_type = None def _determine_eq_order(ctx: 'mypy.plugin.ClassDefContext') -> bool: From aba9fadfc8e5dded74f15863424d20c9a0d02bb0 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Thu, 12 May 2022 11:36:27 +0100 Subject: [PATCH 11/14] Fix special case --- mypy/plugins/attrs.py | 14 +++++++++++--- test-data/unit/check-attr.test | 3 +++ 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/mypy/plugins/attrs.py b/mypy/plugins/attrs.py index 9c8f79c32a89..7d5dba4eeac6 100644 --- a/mypy/plugins/attrs.py +++ b/mypy/plugins/attrs.py @@ -279,17 +279,25 @@ def attr_class_maker_callback(ctx: 'mypy.plugin.ClassDefContext', kw_only = _get_decorator_bool_argument(ctx, 'kw_only', False) match_args = _get_decorator_bool_argument(ctx, 'match_args', True) + early_fail = False if ctx.api.options.python_version[0] < 3: if auto_attribs: ctx.api.fail("auto_attribs is not supported in Python 2", ctx.reason) - return True + early_fail = True if not info.defn.base_type_exprs: # Note: This will not catch subclassing old-style classes. ctx.api.fail("attrs only works with new-style classes", info.defn) - return True + early_fail = True if kw_only: ctx.api.fail(KW_ONLY_PYTHON_2_UNSUPPORTED, ctx.reason) - return True + early_fail = True + if early_fail: + # Add empty metadata to mark that we've finished processing this class. + ctx.cls.info.metadata['attrs'] = { + 'attributes': [], + 'frozen': False, + } + return True for super_info in ctx.cls.info.mro[1:-1]: if 'attrs_tag' in super_info.metadata and 'attrs' not in super_info.metadata: diff --git a/test-data/unit/check-attr.test b/test-data/unit/check-attr.test index af422fbb5e1a..52c995ac285b 100644 --- a/test-data/unit/check-attr.test +++ b/test-data/unit/check-attr.test @@ -1041,6 +1041,9 @@ class Good(object): @attr.s class Bad: # E: attrs only works with new-style classes pass +@attr.s +class SubclassOfBad(Bad): + pass [builtins_py2 fixtures/bool.pyi] [case testAttrsAutoAttribsPy2] From 8d25930f736c484b52db33cc38342190781fb410 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Thu, 12 May 2022 11:40:35 +0100 Subject: [PATCH 12/14] Minor tweak --- mypy/plugins/attrs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/plugins/attrs.py b/mypy/plugins/attrs.py index 7d5dba4eeac6..35dde1ee4479 100644 --- a/mypy/plugins/attrs.py +++ b/mypy/plugins/attrs.py @@ -22,7 +22,7 @@ from mypy.types import ( TupleType, Type, AnyType, TypeOfAny, CallableType, NoneType, TypeVarType, Overloaded, UnionType, FunctionLike, Instance, get_proper_type, - LiteralType + LiteralType, ) from mypy.typeops import make_simplified_union, map_type_from_supertype from mypy.typevars import fill_typevars From 15e1f35a15ca15b4099e29481f5c3f61ae7ef2b3 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Thu, 12 May 2022 11:42:44 +0100 Subject: [PATCH 13/14] Another fix --- mypy/plugins/attrs.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/mypy/plugins/attrs.py b/mypy/plugins/attrs.py index 35dde1ee4479..993581bc281e 100644 --- a/mypy/plugins/attrs.py +++ b/mypy/plugins/attrs.py @@ -292,11 +292,7 @@ def attr_class_maker_callback(ctx: 'mypy.plugin.ClassDefContext', ctx.api.fail(KW_ONLY_PYTHON_2_UNSUPPORTED, ctx.reason) early_fail = True if early_fail: - # Add empty metadata to mark that we've finished processing this class. - ctx.cls.info.metadata['attrs'] = { - 'attributes': [], - 'frozen': False, - } + _add_empty_metadata(info) return True for super_info in ctx.cls.info.mro[1:-1]: @@ -312,6 +308,7 @@ def attr_class_maker_callback(ctx: 'mypy.plugin.ClassDefContext', if node is None: # This name is likely blocked by some semantic analysis error that # should have been reported already. + _add_empty_metadata(info) return True _add_attrs_magic_attribute(ctx, [(attr.name, info[attr.name].type) for attr in attributes]) @@ -431,6 +428,14 @@ def _analyze_class(ctx: 'mypy.plugin.ClassDefContext', return attributes +def _add_empty_metadata(info: TypeInfo) -> None: + """Add empty metadata to mark that we've finished processing this class.""" + info.metadata['attrs'] = { + 'attributes': [], + 'frozen': False, + } + + def _detect_auto_attribs(ctx: 'mypy.plugin.ClassDefContext') -> bool: """Return whether auto_attribs should be enabled or disabled. From 2f09a6a5ed4e3b160fa871c1c2d8ad9b05dd446e Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Fri, 13 May 2022 12:10:44 +0100 Subject: [PATCH 14/14] Fix --disallow-untyped-defs --- mypy/plugins/attrs.py | 18 ++++++++++++------ test-data/unit/check-attr.test | 16 ++++++++++++++++ test-data/unit/fixtures/dict.pyi | 2 +- 3 files changed, 29 insertions(+), 7 deletions(-) diff --git a/mypy/plugins/attrs.py b/mypy/plugins/attrs.py index 993581bc281e..dbce8a402141 100644 --- a/mypy/plugins/attrs.py +++ b/mypy/plugins/attrs.py @@ -61,9 +61,11 @@ class Converter: def __init__(self, type: Optional[Type] = None, - is_attr_converters_optional: bool = False) -> None: + is_attr_converters_optional: bool = False, + is_invalid_converter: bool = False) -> None: self.type = type self.is_attr_converters_optional = is_attr_converters_optional + self.is_invalid_converter = is_invalid_converter class Attribute: @@ -88,7 +90,7 @@ def argument(self, ctx: 'mypy.plugin.ClassDefContext') -> Argument: init_type = self.init_type or self.info[self.name].type - if self.converter.type: + if self.converter.type and not self.converter.is_invalid_converter: # When a converter is set the init_type is overridden by the first argument # of the converter method. converter_type = self.converter.type @@ -115,10 +117,12 @@ def argument(self, ctx: 'mypy.plugin.ClassDefContext') -> Argument: # the allowed init_type. init_type = UnionType.make_union([init_type, NoneType()]) - if not init_type and not (isinstance(converter_type, AnyType) and - converter_type.type_of_any == TypeOfAny.from_error): + if not init_type: ctx.api.fail("Cannot determine __init__ type from converter", self.context) init_type = AnyType(TypeOfAny.from_error) + elif self.converter.is_invalid_converter: + # This means we had a converter but it's not of a type we can infer. + init_type = AnyType(TypeOfAny.from_error) if init_type is None: if ctx.api.options.disallow_untyped_defs: @@ -152,6 +156,7 @@ def serialize(self) -> JsonDict: 'kw_only': self.kw_only, 'converter_type': self.converter.type.serialize() if self.converter.type else None, 'converter_is_attr_converters_optional': self.converter.is_attr_converters_optional, + 'converter_is_invalid_converter': self.converter.is_invalid_converter, 'context_line': self.context.line, 'context_column': self.context.column, 'init_type': self.init_type.serialize() if self.init_type else None, @@ -173,7 +178,8 @@ def deserialize(cls, info: TypeInfo, data['has_default'], data['init'], data['kw_only'], - Converter(converter_type, data['converter_is_attr_converters_optional']), + Converter(converter_type, data['converter_is_attr_converters_optional'], + data['converter_is_invalid_converter']), Context(line=data['context_line'], column=data['context_column']), init_type) @@ -639,7 +645,7 @@ def _parse_converter(ctx: 'mypy.plugin.ClassDefContext', "Unsupported converter, only named functions and types are currently supported", converter ) - return Converter(AnyType(TypeOfAny.from_error)) + return Converter(None, is_invalid_converter=True) return Converter(None) diff --git a/test-data/unit/check-attr.test b/test-data/unit/check-attr.test index 52c995ac285b..fdb0da7e0fce 100644 --- a/test-data/unit/check-attr.test +++ b/test-data/unit/check-attr.test @@ -1718,3 +1718,19 @@ class Parent(Generic[T]): Child1(x=1, key='') Child2(y=1, key='') [builtins fixtures/list.pyi] + +[case testAttrsUnsupportedConverterWithDisallowUntypedDefs] +# flags: --disallow-untyped-defs +import attr +from typing import Mapping, Any, Union + +def default_if_none(factory: Any) -> Any: pass + +@attr.s(slots=True, frozen=True) +class C: + name: Union[str, None] = attr.ib(default=None) + options: Mapping[str, Mapping[str, Any]] = attr.ib( + default=None, converter=default_if_none(factory=dict) \ + # E: Unsupported converter, only named functions and types are currently supported + ) +[builtins fixtures/dict.pyi] diff --git a/test-data/unit/fixtures/dict.pyi b/test-data/unit/fixtures/dict.pyi index f8a5e3481d13..9e7cb6f8c70d 100644 --- a/test-data/unit/fixtures/dict.pyi +++ b/test-data/unit/fixtures/dict.pyi @@ -34,7 +34,7 @@ class dict(Mapping[KT, VT]): class int: # for convenience def __add__(self, x: Union[int, complex]) -> int: pass def __sub__(self, x: Union[int, complex]) -> int: pass - def __neg__(self): pass + def __neg__(self) -> int: pass real: int imag: int