Skip to content

Commit f384df8

Browse files
eurestigvanrossum
authored andcommitted
Better support for converter in attrs plugin. (#4607)
Fixes #4583 (and more). My first pass at this only supported converters that were reachable from the root. With this code the converter can be a local function too. Also now when we can't detect the type of the converter the init type becomes Any.
1 parent 429fb4d commit f384df8

File tree

4 files changed

+224
-12
lines changed

4 files changed

+224
-12
lines changed

mypy/plugin.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@
22

33
from abc import abstractmethod
44
from functools import partial
5-
from typing import Callable, List, Tuple, Optional, NamedTuple, TypeVar
5+
from typing import Callable, List, Tuple, Optional, NamedTuple, TypeVar, Dict
66

77
import mypy.plugins.attrs
88
from mypy.nodes import (
99
Expression, StrExpr, IntExpr, UnaryExpr, Context, DictExpr, ClassDef,
10-
TypeInfo, SymbolTableNode
10+
TypeInfo, SymbolTableNode, MypyFile
1111
)
1212
from mypy.tvar_scope import TypeVarScope
1313
from mypy.types import (
@@ -61,6 +61,7 @@ def named_generic_type(self, name: str, args: List[Type]) -> Instance:
6161
class SemanticAnalyzerPluginInterface:
6262
"""Interface for accessing semantic analyzer functionality in plugins."""
6363

64+
modules = None # type: Dict[str, MypyFile]
6465
options = None # type: Options
6566
msg = None # type: MessageBuilder
6667

@@ -93,6 +94,15 @@ def class_type(self, info: TypeInfo) -> Type:
9394
def lookup_fully_qualified(self, name: str) -> SymbolTableNode:
9495
raise NotImplementedError
9596

97+
@abstractmethod
98+
def lookup_fully_qualified_or_none(self, name: str) -> Optional[SymbolTableNode]:
99+
raise NotImplementedError
100+
101+
@abstractmethod
102+
def lookup_qualified(self, name: str, ctx: Context,
103+
suppress_errors: bool = False) -> Optional[SymbolTableNode]:
104+
raise NotImplementedError
105+
96106

97107
# A context for a function hook that infers the return type of a function with
98108
# a special signature.

mypy/plugins/attrs.py

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import mypy.plugin # To avoid circular imports.
66
from mypy.exprtotype import expr_to_unanalyzed_type, TypeTranslationError
7+
from mypy.fixup import lookup_qualified_stnode
78
from mypy.nodes import (
89
Context, Argument, Var, ARG_OPT, ARG_POS, TypeInfo, AssignmentStmt,
910
TupleExpr, ListExpr, NameExpr, CallExpr, RefExpr, FuncBase,
@@ -54,14 +55,21 @@ def argument(self, ctx: 'mypy.plugin.ClassDefContext') -> Argument:
5455
if self.converter_name:
5556
# When a converter is set the init_type is overriden by the first argument
5657
# of the converter method.
57-
converter = ctx.api.lookup_fully_qualified(self.converter_name)
58+
converter = lookup_qualified_stnode(ctx.api.modules, self.converter_name, True)
59+
if not converter:
60+
# The converter may be a local variable. Check there too.
61+
converter = ctx.api.lookup_qualified(self.converter_name, self.info, True)
62+
5863
if (converter
5964
and converter.type
6065
and isinstance(converter.type, CallableType)
6166
and converter.type.arg_types):
6267
init_type = converter.type.arg_types[0]
6368
else:
64-
init_type = None
69+
init_type = AnyType(TypeOfAny.from_error)
70+
elif self.converter_name == '':
71+
# This means we had a converter but it's not of a type we can infer.
72+
init_type = AnyType(TypeOfAny.from_error)
6573

6674
if init_type is None:
6775
if ctx.api.options.disallow_untyped_defs:
@@ -317,14 +325,16 @@ def _attribute_from_attrib_maker(ctx: 'mypy.plugin.ClassDefContext',
317325
def _get_converter_name(converter: Optional[Expression]) -> Optional[str]:
318326
"""Return the full name of the converter if it exists and is a simple function."""
319327
# TODO: Support complex converters, e.g. lambdas, calls, etc.
320-
if (converter
321-
and isinstance(converter, RefExpr)
322-
and converter.node
323-
and isinstance(converter.node, FuncBase)
324-
and converter.node.type
325-
and isinstance(converter.node.type, CallableType)
326-
and converter.node.type.arg_types):
327-
return converter.node.fullname()
328+
if converter:
329+
if (isinstance(converter, RefExpr)
330+
and converter.node
331+
and isinstance(converter.node, FuncBase)
332+
and converter.node.type
333+
and isinstance(converter.node.type, CallableType)
334+
and converter.node.type.arg_types):
335+
return converter.node.fullname()
336+
# Signal that we have an unsupported converter.
337+
return ''
328338
return None
329339

330340

test-data/unit/check-attr.test

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -532,6 +532,21 @@ class C:
532532

533533
[builtins fixtures/list.pyi]
534534

535+
[case testAttrsUsingUnsupportedConverter]
536+
import attr
537+
class Thing:
538+
def do_it(self, int) -> str:
539+
...
540+
thing = Thing()
541+
def factory(default: int):
542+
...
543+
@attr.s
544+
class C:
545+
x: str = attr.ib(converter=thing.do_it)
546+
y: str = attr.ib(converter=lambda x: x)
547+
z: str = attr.ib(converter=factory(8))
548+
reveal_type(C) # E: Revealed type is 'def (x: Any, y: Any, z: Any) -> __main__.C'
549+
[builtins fixtures/list.pyi]
535550

536551
[case testAttrsUsingConverterAndSubclass]
537552
import attr

test-data/unit/check-incremental.test

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3745,6 +3745,183 @@ class C(A, B):
37453745
[out1]
37463746
[out2]
37473747

3748+
[case testAttrsIncrementalConverterInSubmodule]
3749+
from a.a import A
3750+
reveal_type(A)
3751+
[file a/__init__.py]
3752+
[file a/a.py]
3753+
from typing import Optional
3754+
def converter(s:Optional[int]) -> int:
3755+
...
3756+
3757+
import attr
3758+
@attr.s
3759+
class A:
3760+
x: int = attr.ib(converter=converter)
3761+
3762+
[builtins fixtures/list.pyi]
3763+
[out1]
3764+
main:2: error: Revealed type is 'def (x: Union[builtins.int, builtins.None]) -> a.a.A'
3765+
[out2]
3766+
main:2: error: Revealed type is 'def (x: Union[builtins.int, builtins.None]) -> a.a.A'
3767+
3768+
[case testAttrsIncrementalConverterManyStyles]
3769+
from base import Base
3770+
reveal_type(Base)
3771+
from subclass import A, B
3772+
reveal_type(A)
3773+
reveal_type(B)
3774+
from submodule.base import SubBase
3775+
reveal_type(SubBase)
3776+
from submodule.subclass import AA, BB
3777+
reveal_type(AA)
3778+
reveal_type(BB)
3779+
from submodule.subsubclass import SubAA, SubBB
3780+
reveal_type(SubAA)
3781+
reveal_type(SubBB)
3782+
3783+
[file foo.py]
3784+
from typing import Optional
3785+
def maybe_int(x: Optional[int]) -> int:
3786+
...
3787+
[file bar.py]
3788+
from typing import Optional
3789+
def maybe_bool(x: Optional[bool]) -> bool:
3790+
...
3791+
[file base.py]
3792+
from typing import Optional
3793+
import attr
3794+
import bar
3795+
from foo import maybe_int
3796+
def maybe_str(x: Optional[str]) -> str:
3797+
...
3798+
@attr.s
3799+
class Base:
3800+
x: int = attr.ib(converter=maybe_int)
3801+
y: str = attr.ib(converter=maybe_str)
3802+
z: bool = attr.ib(converter=bar.maybe_bool)
3803+
[file subclass.py]
3804+
from typing import Optional
3805+
import attr
3806+
from base import Base
3807+
@attr.s
3808+
class A(Base): pass
3809+
3810+
import bar
3811+
from foo import maybe_int
3812+
def maybe_str(x: Optional[str]) -> str:
3813+
...
3814+
@attr.s
3815+
class B(Base):
3816+
xx: int = attr.ib(converter=maybe_int)
3817+
yy: str = attr.ib(converter=maybe_str)
3818+
zz: bool = attr.ib(converter=bar.maybe_bool)
3819+
3820+
[file submodule/__init__.py]
3821+
[file submodule/base.py]
3822+
from typing import Optional
3823+
import attr
3824+
import bar
3825+
from foo import maybe_int
3826+
def maybe_str(x: Optional[str]) -> str:
3827+
...
3828+
@attr.s
3829+
class SubBase:
3830+
x: int = attr.ib(converter=maybe_int)
3831+
y: str = attr.ib(converter=maybe_str)
3832+
z: bool = attr.ib(converter=bar.maybe_bool)
3833+
3834+
[file submodule/subclass.py]
3835+
from typing import Optional
3836+
import attr
3837+
from base import Base
3838+
@attr.s
3839+
class AA(Base): pass
3840+
3841+
import bar
3842+
from foo import maybe_int
3843+
def maybe_str(x: Optional[str]) -> str:
3844+
...
3845+
@attr.s
3846+
class BB(Base):
3847+
xx: int = attr.ib(converter=maybe_int)
3848+
yy: str = attr.ib(converter=maybe_str)
3849+
zz: bool = attr.ib(converter=bar.maybe_bool)
3850+
3851+
[file submodule/subsubclass.py]
3852+
from typing import Optional
3853+
import attr
3854+
from .base import SubBase
3855+
@attr.s
3856+
class SubAA(SubBase): pass
3857+
3858+
import bar
3859+
from foo import maybe_int
3860+
def maybe_str(x: Optional[str]) -> str:
3861+
...
3862+
@attr.s
3863+
class SubBB(SubBase):
3864+
xx: int = attr.ib(converter=maybe_int)
3865+
yy: str = attr.ib(converter=maybe_str)
3866+
zz: bool = attr.ib(converter=bar.maybe_bool)
3867+
[builtins fixtures/list.pyi]
3868+
[out1]
3869+
main:2: error: Revealed type is 'def (x: Union[builtins.int, builtins.None], y: Union[builtins.str, builtins.None], z: Union[builtins.bool, builtins.None]) -> base.Base'
3870+
main:4: error: Revealed type is 'def (x: Union[builtins.int, builtins.None], y: Union[builtins.str, builtins.None], z: Union[builtins.bool, builtins.None]) -> subclass.A'
3871+
main:5: error: Revealed type is 'def (x: Union[builtins.int, builtins.None], y: Union[builtins.str, builtins.None], z: Union[builtins.bool, builtins.None], xx: Union[builtins.int, builtins.None], yy: Union[builtins.str, builtins.None], zz: Union[builtins.bool, builtins.None]) -> subclass.B'
3872+
main:7: error: Revealed type is 'def (x: Union[builtins.int, builtins.None], y: Union[builtins.str, builtins.None], z: Union[builtins.bool, builtins.None]) -> submodule.base.SubBase'
3873+
main:9: error: Revealed type is 'def (x: Union[builtins.int, builtins.None], y: Union[builtins.str, builtins.None], z: Union[builtins.bool, builtins.None]) -> submodule.subclass.AA'
3874+
main:10: error: Revealed type is 'def (x: Union[builtins.int, builtins.None], y: Union[builtins.str, builtins.None], z: Union[builtins.bool, builtins.None], xx: Union[builtins.int, builtins.None], yy: Union[builtins.str, builtins.None], zz: Union[builtins.bool, builtins.None]) -> submodule.subclass.BB'
3875+
main:12: error: Revealed type is 'def (x: Union[builtins.int, builtins.None], y: Union[builtins.str, builtins.None], z: Union[builtins.bool, builtins.None]) -> submodule.subsubclass.SubAA'
3876+
main:13: error: Revealed type is 'def (x: Union[builtins.int, builtins.None], y: Union[builtins.str, builtins.None], z: Union[builtins.bool, builtins.None], xx: Union[builtins.int, builtins.None], yy: Union[builtins.str, builtins.None], zz: Union[builtins.bool, builtins.None]) -> submodule.subsubclass.SubBB'
3877+
[out2]
3878+
main:2: error: Revealed type is 'def (x: Union[builtins.int, builtins.None], y: Union[builtins.str, builtins.None], z: Union[builtins.bool, builtins.None]) -> base.Base'
3879+
main:4: error: Revealed type is 'def (x: Union[builtins.int, builtins.None], y: Union[builtins.str, builtins.None], z: Union[builtins.bool, builtins.None]) -> subclass.A'
3880+
main:5: error: Revealed type is 'def (x: Union[builtins.int, builtins.None], y: Union[builtins.str, builtins.None], z: Union[builtins.bool, builtins.None], xx: Union[builtins.int, builtins.None], yy: Union[builtins.str, builtins.None], zz: Union[builtins.bool, builtins.None]) -> subclass.B'
3881+
main:7: error: Revealed type is 'def (x: Union[builtins.int, builtins.None], y: Union[builtins.str, builtins.None], z: Union[builtins.bool, builtins.None]) -> submodule.base.SubBase'
3882+
main:9: error: Revealed type is 'def (x: Union[builtins.int, builtins.None], y: Union[builtins.str, builtins.None], z: Union[builtins.bool, builtins.None]) -> submodule.subclass.AA'
3883+
main:10: error: Revealed type is 'def (x: Union[builtins.int, builtins.None], y: Union[builtins.str, builtins.None], z: Union[builtins.bool, builtins.None], xx: Union[builtins.int, builtins.None], yy: Union[builtins.str, builtins.None], zz: Union[builtins.bool, builtins.None]) -> submodule.subclass.BB'
3884+
main:12: error: Revealed type is 'def (x: Union[builtins.int, builtins.None], y: Union[builtins.str, builtins.None], z: Union[builtins.bool, builtins.None]) -> submodule.subsubclass.SubAA'
3885+
main:13: error: Revealed type is 'def (x: Union[builtins.int, builtins.None], y: Union[builtins.str, builtins.None], z: Union[builtins.bool, builtins.None], xx: Union[builtins.int, builtins.None], yy: Union[builtins.str, builtins.None], zz: Union[builtins.bool, builtins.None]) -> submodule.subsubclass.SubBB'
3886+
3887+
[case testAttrsIncrementalConverterInFunction]
3888+
import attr
3889+
def foo() -> None:
3890+
def foo(x: str) -> int:
3891+
...
3892+
@attr.s
3893+
class A:
3894+
x: int = attr.ib(converter=foo)
3895+
reveal_type(A)
3896+
[builtins fixtures/list.pyi]
3897+
[out1]
3898+
main:8: error: Revealed type is 'def (x: str?) -> __main__.A@5'
3899+
[out2]
3900+
main:8: error: Revealed type is 'def (x: str?) -> __main__.A@5'
3901+
3902+
[case testAttrsIncrementalConverterInSubmoduleForwardRef]
3903+
from a.a import A
3904+
reveal_type(A)
3905+
[file a/__init__.py]
3906+
[file a/a.py]
3907+
from typing import List
3908+
def converter(s:F) -> int:
3909+
...
3910+
3911+
import attr
3912+
@attr.s
3913+
class A:
3914+
x: int = attr.ib(converter=converter)
3915+
3916+
F = List[int]
3917+
3918+
[builtins fixtures/list.pyi]
3919+
[out1]
3920+
main:2: error: Revealed type is 'def (x: builtins.list[builtins.int]) -> a.a.A'
3921+
[out2]
3922+
main:2: error: Revealed type is 'def (x: builtins.list[builtins.int]) -> a.a.A'
3923+
3924+
37483925
[case testAttrsIncrementalThreeRuns]
37493926
from a import A
37503927
A(5)

0 commit comments

Comments
 (0)