Skip to content

Commit f33f58c

Browse files
committed
Add support for generic attrs converters
1 parent a6b5b1e commit f33f58c

File tree

2 files changed

+59
-3
lines changed

2 files changed

+59
-3
lines changed

mypy/plugins/attrs.py

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import mypy.plugin # To avoid circular imports.
99
from mypy.checker import TypeChecker
1010
from mypy.errorcodes import LITERAL_REQ
11+
from mypy.expandtype import expand_type
1112
from mypy.exprtotype import TypeTranslationError, expr_to_unanalyzed_type
1213
from mypy.messages import format_type_bare
1314
from mypy.nodes import (
@@ -49,7 +50,7 @@
4950
deserialize_and_fixup_type,
5051
)
5152
from mypy.server.trigger import make_wildcard_trigger
52-
from mypy.typeops import make_simplified_union, map_type_from_supertype
53+
from mypy.typeops import get_type_vars, make_simplified_union, map_type_from_supertype
5354
from mypy.types import (
5455
AnyType,
5556
CallableType,
@@ -61,6 +62,7 @@
6162
TupleType,
6263
Type,
6364
TypeOfAny,
65+
TypeVarId,
6466
TypeVarType,
6567
UnionType,
6668
get_proper_type,
@@ -85,8 +87,9 @@
8587
class Converter:
8688
"""Holds information about a `converter=` argument"""
8789

88-
def __init__(self, init_type: Type | None = None) -> None:
90+
def __init__(self, init_type: Type | None = None, ret_type: Type | None = None) -> None:
8991
self.init_type = init_type
92+
self.ret_type = ret_type
9093

9194

9295
class Attribute:
@@ -115,11 +118,20 @@ def __init__(
115118
def argument(self, ctx: mypy.plugin.ClassDefContext) -> Argument:
116119
"""Return this attribute as an argument to __init__."""
117120
assert self.init
118-
119121
init_type: Type | None = None
120122
if self.converter:
121123
if self.converter.init_type:
122124
init_type = self.converter.init_type
125+
if init_type and self.converter.ret_type:
126+
# The converter return type should be the same type as the attribute type.
127+
# Copy type vars from attr type to converter.
128+
converter_vars = get_type_vars(self.converter.ret_type)
129+
init_vars = get_type_vars(self.init_type)
130+
if converter_vars and len(converter_vars) == len(init_vars):
131+
variables = {
132+
binder.id: arg for binder, arg in zip(converter_vars, init_vars)
133+
}
134+
init_type = expand_type(init_type, variables)
123135
else:
124136
ctx.api.fail("Cannot determine __init__ type from converter", self.context)
125137
init_type = AnyType(TypeOfAny.from_error)
@@ -671,6 +683,8 @@ def _parse_converter(
671683
converter_type = get_proper_type(converter_type)
672684
if isinstance(converter_type, CallableType) and converter_type.arg_types:
673685
converter_info.init_type = converter_type.arg_types[0]
686+
if not is_attr_converters_optional:
687+
converter_info.ret_type = converter_type.ret_type
674688
elif isinstance(converter_type, Overloaded):
675689
types: list[Type] = []
676690
for item in converter_type.items:

test-data/unit/check-attr.test

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -469,6 +469,48 @@ A([1], '2') # E: Cannot infer type argument 1 of "A"
469469

470470
[builtins fixtures/list.pyi]
471471

472+
[case testAttrsGenericWithConverter]
473+
from typing import TypeVar, Generic, List, Iterable, Iterator
474+
import attr
475+
T = TypeVar('T')
476+
477+
def int_gen() -> Iterator[int]:
478+
yield 1
479+
480+
def list_converter(x: Iterable[T]) -> List[T]:
481+
return list(x)
482+
483+
@attr.s(auto_attribs=True)
484+
class A(Generic[T]):
485+
x: List[T] = attr.ib(converter=list_converter)
486+
y: T = attr.ib()
487+
def foo(self) -> List[T]:
488+
return [self.y]
489+
def bar(self) -> T:
490+
return self.x[0]
491+
def problem(self) -> T:
492+
return self.x # E: Incompatible return value type (got "List[T]", expected "T")
493+
reveal_type(A) # N: Revealed type is "def [T] (x: typing.Iterable[T`1], y: T`1) -> __main__.A[T`1]"
494+
a1 = A([1], 2)
495+
reveal_type(a1) # N: Revealed type is "__main__.A[builtins.int]"
496+
reveal_type(a1.x) # N: Revealed type is "builtins.list[builtins.int]"
497+
reveal_type(a1.y) # N: Revealed type is "builtins.int"
498+
499+
a2 = A(int_gen(), 2)
500+
reveal_type(a2) # N: Revealed type is "__main__.A[builtins.int]"
501+
reveal_type(a2.x) # N: Revealed type is "builtins.list[builtins.int]"
502+
reveal_type(a2.y) # N: Revealed type is "builtins.int"
503+
504+
# Leaving this as a sanity check
505+
class B(Generic[T]):
506+
def __init__(self, x: Iterable[T], y: T) -> None:
507+
pass
508+
509+
B(['str'], 7)
510+
B([1], '2')
511+
512+
[builtins fixtures/list.pyi]
513+
472514

473515
[case testAttrsUntypedGenericInheritance]
474516
from typing import Generic, TypeVar

0 commit comments

Comments
 (0)