Skip to content

Commit

Permalink
attrs namedtuple (#11794)
Browse files Browse the repository at this point in the history
  • Loading branch information
Tinche authored Jan 7, 2022
1 parent f5ac47f commit 254d41e
Show file tree
Hide file tree
Showing 5 changed files with 86 additions and 10 deletions.
6 changes: 5 additions & 1 deletion mypy/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ class C: pass
from mypy_extensions import trait, mypyc_attr

from mypy.nodes import (
Expression, Context, ClassDef, SymbolTableNode, MypyFile, CallExpr, ArgKind
Expression, Context, ClassDef, SymbolTableNode, MypyFile, CallExpr, ArgKind, TypeInfo
)
from mypy.tvar_scope import TypeVarLikeScope
from mypy.types import (
Expand Down Expand Up @@ -274,6 +274,10 @@ def named_type_or_none(self, fullname: str,
"""
raise NotImplementedError

@abstractmethod
def basic_new_typeinfo(self, name: str, basetype_or_fallback: Instance, line: int) -> TypeInfo:
raise NotImplementedError

@abstractmethod
def parse_bool(self, expr: Expression) -> Optional[bool]:
"""Parse True/False literals."""
Expand Down
31 changes: 23 additions & 8 deletions mypy/plugins/attrs.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
)
from mypy.types import (
TupleType, Type, AnyType, TypeOfAny, CallableType, NoneType, TypeVarType,
Overloaded, UnionType, FunctionLike, get_proper_type,
Overloaded, UnionType, FunctionLike, Instance, get_proper_type,
)
from mypy.typeops import make_simplified_union, map_type_from_supertype
from mypy.typevars import fill_typevars
Expand Down Expand Up @@ -50,6 +50,8 @@
}

SELF_TVAR_NAME: Final = "_AT"
MAGIC_ATTR_NAME: Final = "__attrs_attrs__"
MAGIC_ATTR_CLS_NAME: Final = "_AttrsAttributes" # The namedtuple subclass name.


class Converter:
Expand Down Expand Up @@ -302,7 +304,7 @@ def attr_class_maker_callback(ctx: 'mypy.plugin.ClassDefContext',
ctx.api.defer()
return

_add_attrs_magic_attribute(ctx, raw_attr_types=[info[attr.name].type for attr in attributes])
_add_attrs_magic_attribute(ctx, [(attr.name, info[attr.name].type) for attr in attributes])
if slots:
_add_slots(ctx, attributes)

Expand Down Expand Up @@ -710,23 +712,36 @@ def _add_init(ctx: 'mypy.plugin.ClassDefContext', attributes: List[Attribute],


def _add_attrs_magic_attribute(ctx: 'mypy.plugin.ClassDefContext',
raw_attr_types: 'List[Optional[Type]]') -> None:
attr_name = '__attrs_attrs__'
attrs: 'List[Tuple[str, Optional[Type]]]') -> None:
any_type = AnyType(TypeOfAny.explicit)
attributes_types: 'List[Type]' = [
ctx.api.named_type_or_none('attr.Attribute', [attr_type or any_type]) or any_type
for attr_type in raw_attr_types
for _, attr_type in attrs
]
fallback_type = ctx.api.named_type('builtins.tuple', [
ctx.api.named_type_or_none('attr.Attribute', [any_type]) or any_type,
])
var = Var(name=attr_name, type=TupleType(attributes_types, fallback=fallback_type))

ti = ctx.api.basic_new_typeinfo(MAGIC_ATTR_CLS_NAME, fallback_type, 0)
ti.is_named_tuple = True
for (name, _), attr_type in zip(attrs, attributes_types):
var = Var(name, attr_type)
var.is_property = True
proper_type = get_proper_type(attr_type)
if isinstance(proper_type, Instance):
var.info = proper_type.type
ti.names[name] = SymbolTableNode(MDEF, var, plugin_generated=True)
attributes_type = Instance(ti, [])

var = Var(name=MAGIC_ATTR_NAME, type=TupleType(attributes_types, fallback=attributes_type))
var.info = ctx.cls.info
var._fullname = ctx.cls.info.fullname + '.' + attr_name
ctx.cls.info.names[attr_name] = SymbolTableNode(
var.is_classvar = True
var._fullname = f"{ctx.cls.fullname}.{MAGIC_ATTR_CLS_NAME}"
ctx.cls.info.names[MAGIC_ATTR_NAME] = SymbolTableNode(
kind=MDEF,
node=var,
plugin_generated=True,
no_serialize=True,
)


Expand Down
1 change: 1 addition & 0 deletions mypy/test/testfinegrained.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ class FineGrainedSuite(DataSuite):
'fine-grained-modules.test',
'fine-grained-follow-imports.test',
'fine-grained-suggest.test',
'fine-grained-attr.test',
]

# Whether to use the fine-grained cache in the testing. This is overridden
Expand Down
35 changes: 34 additions & 1 deletion test-data/unit/check-attr.test
Original file line number Diff line number Diff line change
Expand Up @@ -1399,7 +1399,40 @@ class A:
b: int = attr.ib()
c: str = attr.ib()

reveal_type(A.__attrs_attrs__) # N: Revealed type is "Tuple[attr.Attribute[builtins.int], attr.Attribute[builtins.str]]"
reveal_type(A.__attrs_attrs__) # N: Revealed type is "Tuple[attr.Attribute[builtins.int], attr.Attribute[builtins.str], fallback=__main__.A._AttrsAttributes]"
reveal_type(A.__attrs_attrs__[0]) # N: Revealed type is "attr.Attribute[builtins.int]"
reveal_type(A.__attrs_attrs__.b) # N: Revealed type is "attr.Attribute[builtins.int]"
A.__attrs_attrs__.x # E: "_AttrsAttributes" has no attribute "x"

[builtins fixtures/attr.pyi]

[case testAttrsBareClassHasAttributeWithAttributes]
import attr

@attr.s
class A:
b = attr.ib()
c = attr.ib()

reveal_type(A.__attrs_attrs__) # N: Revealed type is "Tuple[attr.Attribute[Any], attr.Attribute[Any], fallback=__main__.A._AttrsAttributes]"
reveal_type(A.__attrs_attrs__[0]) # N: Revealed type is "attr.Attribute[Any]"
reveal_type(A.__attrs_attrs__.b) # N: Revealed type is "attr.Attribute[Any]"
A.__attrs_attrs__.x # E: "_AttrsAttributes" has no attribute "x"

[builtins fixtures/attr.pyi]

[case testAttrsNGClassHasAttributeWithAttributes]
import attr

@attr.define
class A:
b: int
c: str

reveal_type(A.__attrs_attrs__) # N: Revealed type is "Tuple[attr.Attribute[builtins.int], attr.Attribute[builtins.str], fallback=__main__.A._AttrsAttributes]"
reveal_type(A.__attrs_attrs__[0]) # N: Revealed type is "attr.Attribute[builtins.int]"
reveal_type(A.__attrs_attrs__.b) # N: Revealed type is "attr.Attribute[builtins.int]"
A.__attrs_attrs__.x # E: "_AttrsAttributes" has no attribute "x"

[builtins fixtures/attr.pyi]

Expand Down
23 changes: 23 additions & 0 deletions test-data/unit/fine-grained-attr.test
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
[case updateMagicField]
from attr import Attribute
import m

def g() -> Attribute[int]:
return m.A.__attrs_attrs__[0]

[file m.py]
from attr import define

@define
class A:
a: int
[file m.py.2]
from attr import define

@define
class A:
a: float
[builtins fixtures/attr.pyi]
[out]
==
main:5: error: Incompatible return value type (got "Attribute[float]", expected "Attribute[int]")

0 comments on commit 254d41e

Please sign in to comment.