Skip to content

Commit 91f2d36

Browse files
eurestiilevkivskyi
authored andcommitted
Plugin to typecheck attrs-generated classes (#4397)
See http://www.attrs.org/en/stable/how-does-it-work.html for information on how attrs works. The plugin walks the class declaration (including superclasses) looking for "attributes" then depending on how the decorator was called, makes modification to the classes as follows: * init=True adds an __init__ method. * cmp=True adds all of the necessary __cmp__ methods. * frozen=True turns all attributes into properties to make the class read only. * Remove any @x.default and @y.validator decorators which are only part of class creation. Fixes #2088
1 parent b993693 commit 91f2d36

File tree

12 files changed

+1611
-3
lines changed

12 files changed

+1611
-3
lines changed

mypy/nodes.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1993,6 +1993,10 @@ class is generic then it will be a type constructor of higher kind.
19931993
# needed during the semantic passes.)
19941994
replaced = None # type: TypeInfo
19951995

1996+
# This is a dictionary that will be serialized and un-serialized as is.
1997+
# It is useful for plugins to add their data to save in the cache.
1998+
metadata = None # type: Dict[str, JsonDict]
1999+
19962000
FLAGS = [
19972001
'is_abstract', 'is_enum', 'fallback_to_any', 'is_named_tuple',
19982002
'is_newtype', 'is_protocol', 'runtime_protocol'
@@ -2016,6 +2020,7 @@ def __init__(self, names: 'SymbolTable', defn: ClassDef, module_name: str) -> No
20162020
self._cache = set()
20172021
self._cache_proper = set()
20182022
self.add_type_vars()
2023+
self.metadata = {}
20192024

20202025
def add_type_vars(self) -> None:
20212026
if self.defn.type_vars:
@@ -2218,6 +2223,7 @@ def serialize(self) -> JsonDict:
22182223
'typeddict_type':
22192224
None if self.typeddict_type is None else self.typeddict_type.serialize(),
22202225
'flags': get_flags(self, TypeInfo.FLAGS),
2226+
'metadata': self.metadata,
22212227
}
22222228
return data
22232229

@@ -2244,6 +2250,7 @@ def deserialize(cls, data: JsonDict) -> 'TypeInfo':
22442250
else mypy.types.TupleType.deserialize(data['tuple_type']))
22452251
ti.typeddict_type = (None if data['typeddict_type'] is None
22462252
else mypy.types.TypedDictType.deserialize(data['typeddict_type']))
2253+
ti.metadata = data['metadata']
22472254
set_flags(ti, data['flags'])
22482255
return ti
22492256

@@ -2612,3 +2619,10 @@ def check_arg_names(names: Sequence[Optional[str]], nodes: List[T], fail: Callab
26122619
fail("Duplicate argument '{}' in {}".format(name, description), node)
26132620
break
26142621
seen_names.add(name)
2622+
2623+
2624+
def is_class_var(expr: NameExpr) -> bool:
2625+
"""Return whether the expression is ClassVar[...]"""
2626+
if isinstance(expr.node, Var):
2627+
return expr.node.is_classvar
2628+
return False

mypy/plugin.py

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
11
"""Plugin system for extending mypy."""
22

3-
from collections import OrderedDict
43
from abc import abstractmethod
4+
from functools import partial
55
from typing import Callable, List, Tuple, Optional, NamedTuple, TypeVar
66

7-
from mypy.nodes import Expression, StrExpr, IntExpr, UnaryExpr, Context, DictExpr, ClassDef
7+
import mypy.plugins.attrs
8+
from mypy.nodes import (
9+
Expression, StrExpr, IntExpr, UnaryExpr, Context, DictExpr, ClassDef,
10+
TypeInfo, SymbolTableNode
11+
)
12+
from mypy.tvar_scope import TypeVarScope
813
from mypy.types import (
9-
Type, Instance, CallableType, TypedDictType, UnionType, NoneTyp, FunctionLike, TypeVarType,
14+
Type, Instance, CallableType, TypedDictType, UnionType, NoneTyp, TypeVarType,
1015
AnyType, TypeList, UnboundType, TypeOfAny
1116
)
1217
from mypy.messages import MessageBuilder
@@ -56,6 +61,9 @@ def named_generic_type(self, name: str, args: List[Type]) -> Instance:
5661
class SemanticAnalyzerPluginInterface:
5762
"""Interface for accessing semantic analyzer functionality in plugins."""
5863

64+
options = None # type: Options
65+
msg = None # type: MessageBuilder
66+
5967
@abstractmethod
6068
def named_type(self, qualified_name: str, args: Optional[List[Type]] = None) -> Instance:
6169
raise NotImplementedError
@@ -69,6 +77,22 @@ def fail(self, msg: str, ctx: Context, serious: bool = False, *,
6977
blocker: bool = False) -> None:
7078
raise NotImplementedError
7179

80+
@abstractmethod
81+
def anal_type(self, t: Type, *,
82+
tvar_scope: Optional[TypeVarScope] = None,
83+
allow_tuple_literal: bool = False,
84+
aliasing: bool = False,
85+
third_pass: bool = False) -> Type:
86+
raise NotImplementedError
87+
88+
@abstractmethod
89+
def class_type(self, info: TypeInfo) -> Type:
90+
raise NotImplementedError
91+
92+
@abstractmethod
93+
def lookup_fully_qualified(self, name: str) -> SymbolTableNode:
94+
raise NotImplementedError
95+
7296

7397
# A context for a function hook that infers the return type of a function with
7498
# a special signature.
@@ -262,6 +286,17 @@ def get_method_hook(self, fullname: str
262286
return int_pow_callback
263287
return None
264288

289+
def get_class_decorator_hook(self, fullname: str
290+
) -> Optional[Callable[[ClassDefContext], None]]:
291+
if fullname in mypy.plugins.attrs.attr_class_makers:
292+
return mypy.plugins.attrs.attr_class_maker_callback
293+
elif fullname in mypy.plugins.attrs.attr_dataclass_makers:
294+
return partial(
295+
mypy.plugins.attrs.attr_class_maker_callback,
296+
auto_attribs_default=True
297+
)
298+
return None
299+
265300

266301
def open_callback(ctx: FunctionContext) -> Type:
267302
"""Infer a better return type for 'open'.

mypy/plugins/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)