From b7430ceaebb736d6ecb7bc8e9fa45e186d5ee233 Mon Sep 17 00:00:00 2001 From: Markus Schmaus Date: Tue, 12 May 2020 14:57:21 +0200 Subject: [PATCH 1/5] Allow attrs kw_only arguments at any position --- mypy/plugins/attrs.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/mypy/plugins/attrs.py b/mypy/plugins/attrs.py index d1744f6a37ca..12675042aa57 100644 --- a/mypy/plugins/attrs.py +++ b/mypy/plugins/attrs.py @@ -358,7 +358,6 @@ def _analyze_class(ctx: 'mypy.plugin.ClassDefContext', # Check the init args for correct default-ness. Note: This has to be done after all the # attributes for all classes have been read, because subclasses can override parents. last_default = False - last_kw_only = False for i, attribute in enumerate(attributes): if not attribute.init: @@ -366,7 +365,6 @@ def _analyze_class(ctx: 'mypy.plugin.ClassDefContext', if attribute.kw_only: # Keyword-only attributes don't care whether they are default or not. - last_kw_only = True continue # If the issue comes from merging different classes, report it @@ -377,11 +375,6 @@ def _analyze_class(ctx: 'mypy.plugin.ClassDefContext', ctx.api.fail( "Non-default attributes not allowed after default attributes.", context) - if last_kw_only: - ctx.api.fail( - "Non keyword-only attributes are not allowed after a keyword-only attribute.", - context - ) last_default |= attribute.has_default return attributes @@ -626,7 +619,18 @@ def _make_frozen(ctx: 'mypy.plugin.ClassDefContext', attributes: List[Attribute] def _add_init(ctx: 'mypy.plugin.ClassDefContext', attributes: List[Attribute], adder: 'MethodAdder') -> None: """Generate an __init__ method for the attributes and add it to the class.""" - args = [attribute.argument(ctx) for attribute in attributes if attribute.init] + # Convert attributes to arguments with kw_only arguments at the end of + # the argument list + pos_args = [] + kw_only_args = [] + for attribute in attributes: + if not attribute.init: + continue + if attribute.kw_only: + kw_only_args.append(attribute.argument(ctx)) + else: + pos_args.append(attribute.argument(ctx)) + args = pos_args + kw_only_args if all( # We use getattr rather than instance checks because the variable.type # might be wrapped into a Union or some other type, but even non-Any From 2b82163e233be2eb9d791340cd74bbaf490de4fd Mon Sep 17 00:00:00 2001 From: Markus Schmaus Date: Sat, 23 May 2020 15:44:18 +0200 Subject: [PATCH 2/5] Partially fixes python#8625 --- mypy/plugins/attrs.py | 42 +++++++++++++++++++++++++++++-- test-data/unit/check-attr.test | 37 +++++++++++++++++++++++++++ test-data/unit/fixtures/tuple.pyi | 4 +++ 3 files changed, 81 insertions(+), 2 deletions(-) diff --git a/mypy/plugins/attrs.py b/mypy/plugins/attrs.py index 12675042aa57..040ecb5b369a 100644 --- a/mypy/plugins/attrs.py +++ b/mypy/plugins/attrs.py @@ -6,6 +6,8 @@ from typing_extensions import Final import mypy.plugin # To avoid circular imports. +from mypy.constraints import infer_constraints, SUBTYPE_OF +from mypy.expandtype import expand_type from mypy.exprtotype import expr_to_unanalyzed_type, TypeTranslationError from mypy.fixup import lookup_qualified_stnode from mypy.nodes import ( @@ -18,6 +20,7 @@ from mypy.plugins.common import ( _get_argument, _get_bool_argument, _get_decorator_bool_argument, add_method ) +from mypy.solve import solve_constraints from mypy.types import ( Type, AnyType, TypeOfAny, CallableType, NoneType, TypeVarDef, TypeVarType, Overloaded, UnionType, FunctionLike, get_proper_type @@ -57,6 +60,40 @@ def __init__(self, self.is_attr_converters_optional = is_attr_converters_optional +def expand_arg_type( + callable_type: CallableType, + target_type: Type, +): + # The result is based on the type of the first argument of the callable + arg_type = get_proper_type(callable_type.arg_types[0]) + ret_type = get_proper_type(callable_type.ret_type) + target_type = get_proper_type(target_type) + + if ret_type == target_type or isinstance(ret_type, AnyType): + # If the callable has the exact same return type as the target + # we can directly return the type of the first argument. + # This is also the case if the return type is `Any`. + return arg_type + + # Find the constraints on the type vars given that the result must be a + # subtype of the target type. + constraints = infer_constraints(ret_type, target_type, SUBTYPE_OF) + # When this code gets run, callable_type.variables has not yet been + # properly initialized, so we instead simply construct a set of all unique + # type var ids in the constraints + type_var_ids = list({const.type_var for const in constraints}) + # Now we get the best solutions for these constraints + solutions = solve_constraints(type_var_ids, constraints) + type_map = { + tid: sol + for tid, sol in zip(type_var_ids, solutions) + } + + # Now we can use these solutions to expand the generic arg type into a + # concrete type + return expand_type(arg_type, type_map) + + class Attribute: """The value of an attr.ib() call.""" @@ -94,10 +131,11 @@ def argument(self, ctx: 'mypy.plugin.ClassDefContext') -> Argument: elif converter and converter.type: converter_type = converter.type + orig_type = init_type 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 = ctx.api.anal_type(expand_arg_type(converter_type, orig_type)) elif isinstance(converter_type, Overloaded): types = [] # type: List[Type] for item in converter_type.items(): @@ -107,7 +145,7 @@ def argument(self, ctx: 'mypy.plugin.ClassDefContext') -> Argument: continue if num_arg_types > 1 and any(kind == ARG_POS for kind in item.arg_kinds[1:]): continue - types.append(item.arg_types[0]) + types.append(expand_arg_type(item, orig_type)) # Make a union of all the valid types. if types: args = make_simplified_union(types) diff --git a/test-data/unit/check-attr.test b/test-data/unit/check-attr.test index 28613454d2ff..36e3162ba835 100644 --- a/test-data/unit/check-attr.test +++ b/test-data/unit/check-attr.test @@ -714,6 +714,43 @@ reveal_type(A) # N: Revealed type is 'def (x: builtins.int) -> __main__.A' reveal_type(A(15).x) # N: Revealed type is 'builtins.str' [builtins fixtures/list.pyi] +[case testAttrsUsingTupleConverter] +from typing import Tuple +import attr + +@attr.s +class C: + t: Tuple[int, ...] = attr.ib(converter=tuple) + +o = C([1, 2, 3]) +o = C(['a']) # E: List item 0 has incompatible type "str"; expected "int" +[builtins fixtures/tuple.pyi] + +[case testAttrsUsingListConverter] +from typing import List +import attr + +@attr.s +class C: + t: List[int] = attr.ib(converter=list) + +o = C([1, 2, 3]) +o = C(['a']) # E: List item 0 has incompatible type "str"; expected "int" +[builtins fixtures/list.pyi] + +[case testAttrsUsingDictConverter] +from typing import Dict +import attr + +@attr.s +class C(object): + values = attr.ib(type=Dict[str, int], converter=dict) + + +C(values=[('a', 1), ('b', 2)]) +C(values=[(1, 'a')]) # E: List item 0 has incompatible type "Tuple[int, str]"; expected "Tuple[str, int]" +[builtins fixtures/dict.pyi] + [case testAttrsUsingConverterWithTypes] from typing import overload import attr diff --git a/test-data/unit/fixtures/tuple.pyi b/test-data/unit/fixtures/tuple.pyi index a101595c6f30..0fe728c93d63 100644 --- a/test-data/unit/fixtures/tuple.pyi +++ b/test-data/unit/fixtures/tuple.pyi @@ -11,6 +11,10 @@ class type: def __init__(self, *a: object) -> None: pass def __call__(self, *a: object) -> object: pass class tuple(Sequence[Tco], Generic[Tco]): + @overload + def __init__(self) -> None: pass + @overload + def __init__(self, x: Iterable[Tco]) -> None: pass def __iter__(self) -> Iterator[Tco]: pass def __contains__(self, item: object) -> bool: pass def __getitem__(self, x: int) -> Tco: pass From d8f0602c8d8ab087f8e8c9a213dd6a282467b783 Mon Sep 17 00:00:00 2001 From: Markus Schmaus Date: Sat, 23 May 2020 16:30:53 +0200 Subject: [PATCH 3/5] revert changes to tuple fixture to avoid issues with other test --- test-data/unit/check-attr.test | 2 +- test-data/unit/fixtures/attr.pyi | 22 +++++++++++++++++++++- test-data/unit/fixtures/tuple.pyi | 4 ---- 3 files changed, 22 insertions(+), 6 deletions(-) diff --git a/test-data/unit/check-attr.test b/test-data/unit/check-attr.test index 36e3162ba835..e680b1fb2201 100644 --- a/test-data/unit/check-attr.test +++ b/test-data/unit/check-attr.test @@ -724,7 +724,7 @@ class C: o = C([1, 2, 3]) o = C(['a']) # E: List item 0 has incompatible type "str"; expected "int" -[builtins fixtures/tuple.pyi] +[builtins fixtures/attr.pyi] [case testAttrsUsingListConverter] from typing import List diff --git a/test-data/unit/fixtures/attr.pyi b/test-data/unit/fixtures/attr.pyi index deb1906d931e..00d871e0c4e0 100644 --- a/test-data/unit/fixtures/attr.pyi +++ b/test-data/unit/fixtures/attr.pyi @@ -1,5 +1,7 @@ # Builtins stub used to support @attr.s tests. -from typing import Union, overload +from typing import Union, overload, Sequence, Generic, TypeVar, Iterable, \ + Tuple, Iterator + class object: def __init__(self) -> None: pass @@ -22,6 +24,24 @@ class complex: @overload def __init__(self, real: str = ...) -> None: ... +Tco = TypeVar('Tco', covariant=True) + +class tuple(Sequence[Tco], Generic[Tco]): + @overload + def __init__(self) -> None: pass + @overload + def __init__(self, x: Iterable[Tco]) -> None: pass + def __iter__(self) -> Iterator[Tco]: pass + def __contains__(self, item: object) -> bool: pass + def __getitem__(self, x: int) -> Tco: pass + def __rmul__(self, n: int) -> Tuple[Tco, ...]: pass + def __add__(self, x: Tuple[Tco, ...]) -> Tuple[Tco, ...]: pass + def count(self, obj: object) -> int: pass + +T = TypeVar('T') + +class list(Sequence[T], Generic[T]): pass + class str: pass class unicode: pass class ellipsis: pass diff --git a/test-data/unit/fixtures/tuple.pyi b/test-data/unit/fixtures/tuple.pyi index 0fe728c93d63..a101595c6f30 100644 --- a/test-data/unit/fixtures/tuple.pyi +++ b/test-data/unit/fixtures/tuple.pyi @@ -11,10 +11,6 @@ class type: def __init__(self, *a: object) -> None: pass def __call__(self, *a: object) -> object: pass class tuple(Sequence[Tco], Generic[Tco]): - @overload - def __init__(self) -> None: pass - @overload - def __init__(self, x: Iterable[Tco]) -> None: pass def __iter__(self) -> Iterator[Tco]: pass def __contains__(self, item: object) -> bool: pass def __getitem__(self, x: int) -> Tco: pass From e109df2fef120692e1633b5779e8234fe7535eec Mon Sep 17 00:00:00 2001 From: Markus Schmaus Date: Sat, 23 May 2020 16:56:24 +0200 Subject: [PATCH 4/5] Fix typing errors --- mypy/plugins/attrs.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mypy/plugins/attrs.py b/mypy/plugins/attrs.py index 040ecb5b369a..2617f0114a64 100644 --- a/mypy/plugins/attrs.py +++ b/mypy/plugins/attrs.py @@ -63,7 +63,7 @@ def __init__(self, def expand_arg_type( callable_type: CallableType, target_type: Type, -): +) -> Type: # The result is based on the type of the first argument of the callable arg_type = get_proper_type(callable_type.arg_types[0]) ret_type = get_proper_type(callable_type.ret_type) @@ -87,6 +87,7 @@ def expand_arg_type( type_map = { tid: sol for tid, sol in zip(type_var_ids, solutions) + if sol is not None } # Now we can use these solutions to expand the generic arg type into a From bc90c160b3f3627b03ca53fa98c67a610e134012 Mon Sep 17 00:00:00 2001 From: Markus Schmaus Date: Sat, 23 May 2020 18:13:15 +0200 Subject: [PATCH 5/5] Replace missing target type with Any --- mypy/plugins/attrs.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/mypy/plugins/attrs.py b/mypy/plugins/attrs.py index 2617f0114a64..adc9a27c23ed 100644 --- a/mypy/plugins/attrs.py +++ b/mypy/plugins/attrs.py @@ -62,12 +62,14 @@ def __init__(self, def expand_arg_type( callable_type: CallableType, - target_type: Type, + target_type: Optional[Type], ) -> Type: # The result is based on the type of the first argument of the callable arg_type = get_proper_type(callable_type.arg_types[0]) ret_type = get_proper_type(callable_type.ret_type) target_type = get_proper_type(target_type) + if target_type is None: + target_type = AnyType(TypeOfAny.unannotated) if ret_type == target_type or isinstance(ret_type, AnyType): # If the callable has the exact same return type as the target