-
-
Notifications
You must be signed in to change notification settings - Fork 3k
Plugin to typecheck attrs-generated classes #4397
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
005547a
ecd0164
8f4dbd7
2b55334
83b008c
3bc5c11
aa55fe5
5f897b7
8867278
cd2aded
104f9f4
6856500
0e359a8
ebb9834
001a16a
453c8ae
d26bdb6
e78c040
fd1a24b
1680e8f
521d215
071c0fd
6986547
5d60d99
2284797
89f3537
353319c
72cc0a7
e48091f
2d0f5dd
f7f6033
2ff3a19
d06bd74
6fbe81f
ccfd6c0
4d9c182
2fc6f5f
d11c138
dd947d2
ce2cac6
d05c364
695b422
76fa908
405204d
536117f
0fa72a9
6506d7f
5d16792
9fb4e30
256b446
d566806
5b7b290
b2f075e
f6f2f41
6f60854
7492278
28c7b8d
aeb01be
4fcb5dd
1ddb89e
Add more tests.
euresti Jan 19, 2018
5155480
ff00d73
bc9db4b
a4f34a8
d7a8e2e
6b86aa2
8eae273
93f80d6
8cdcbfa
70e25dd
fc2b22b
95e3ac0
a076957
3fa9e4f
cad44bb
0240092
c37147d
cb795cd
b2b538c
d1e3e0b
6ef4a63
feab654
641230d
bf49a87
File filter
Filter by extension
Conversations
Jump to
Diff view
8000Diff view
- Loading branch information
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,12 +2,14 @@ | |
|
||
from collections import OrderedDict | ||
from abc import abstractmethod | ||
from typing import Callable, List, Tuple, Optional, NamedTuple, TypeVar | ||
from typing import Callable, List, Tuple, Optional, NamedTuple, TypeVar, Set, \ | ||
cast | ||
|
||
from mypy import messages | ||
from mypy.nodes import Expression, StrExpr, IntExpr, UnaryExpr, Context, \ | ||
DictExpr, ClassDef, Argument, Var, TypeInfo, FuncDef, Block, \ | ||
SymbolTableNode, MDEF, CallExpr, RefExpr, AssignmentStmt, TempNode, \ | ||
ARG_POS, ARG_OPT, EllipsisExpr | ||
ARG_POS, ARG_OPT, EllipsisExpr, NameExpr | ||
from mypy.types import ( | ||
Type, Instance, CallableType, TypedDictType, UnionType, NoneTyp, FunctionLike, TypeVarType, | ||
AnyType, TypeList, UnboundType, TypeOfAny | ||
|
@@ -269,6 +271,7 @@ def get_class_decorator_hook(self, fullname: str | |
) -> Optional[Callable[[ClassDefContext], None]]: | ||
if fullname == 'attr.s': | ||
return attr_s_callback | ||
return None | ||
|
||
|
||
def open_callback(ctx: FunctionContext) -> Type: | ||
|
@@ -405,7 +408,7 @@ def add_method( | |
arg_names = [arg.variable.name() for arg in args] | ||
arg_kinds = [arg.kind for arg in args] | ||
assert None not in arg_types | ||
signature = CallableType(arg_types, arg_kinds, arg_names, | ||
signature = CallableType(cast(List[Type], arg_types), arg_kinds, arg_names, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is it possible to avoid the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The named tuple code does the same thing. arg_types is technically a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think the cast is not required with that assert. Have you actually tried removing it? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah I get:
When running |
||
ret_type, function_type) | ||
func = FuncDef(method_name, args, Block([])) | ||
func.info = info | ||
|
@@ -419,24 +422,25 @@ def attr_s_callback(ctx: ClassDefContext) -> None: | |
"""Add an __init__ method to classes decorated with attr.s.""" | ||
# TODO: Add __cmp__ methods. | ||
|
||
def get_bool_argument(call: CallExpr, name: str, default: bool): | ||
def get_bool_argument(call: CallExpr, name: str, default: Optional[bool]) -> Optional[bool]: | ||
for arg_name, arg_value in zip(call.arg_names, call.args): | ||
if arg_name == name: | ||
# TODO: Handle None being returned here. | ||
return ctx.api.parse_bool(arg_value) | ||
return default | ||
|
||
def get_argument(call: CallExpr, name: Optional[str], num: Optional[int]): | ||
def get_argument(call: CallExpr, name: Optional[str], num: Optional[int]) -> Optional[Expression]: | ||
for i, (attr_name, attr_value) in enumerate(zip(call.arg_names, call.args)): | ||
if num is not None and i == num: | ||
if num is not None and not attr_name and i == num: | ||
return attr_value | ||
if name and attr_name == name: | ||
return attr_value | ||
return None | ||
|
||
def called_function(expr: Expression): | ||
def called_function(expr: Expression) -> Optional[str]: | ||
if isinstance(expr, CallExpr) and isinstance(expr.callee, RefExpr): | ||
return expr.callee.fullname | ||
return None | ||
|
||
decorator = ctx.reason | ||
if isinstance(decorator, CallExpr): | ||
|
@@ -449,7 +453,6 @@ def called_function(expr: Expression): | |
auto_attribs = False | ||
|
||
if not init: | ||
print("Nothing to do", init) | ||
return | ||
|
||
print(f"{ctx.cls.info.fullname()} init={init} auto={auto_attribs}") | ||
|
@@ -460,48 +463,56 @@ def called_function(expr: Expression): | |
names = [] # type: List[str] | ||
types = [] # type: List[Type] | ||
has_default = set() # type: Set[str] | ||
|
||
def add_init_argument(name: str, typ:Optional[Type], default: bool, context:Context) -> None: | ||
if not default and has_default: | ||
ctx.api.fail( | ||
"Non-default attributes not allowed after default attributes.", | ||
context) | ||
if not typ: | ||
ctx.api.fail(messages.NEED_ANNOTATION_FOR_VAR, context) | ||
typ = AnyType(TypeOfAny.unannotated) | ||
|
||
names.append(name) | ||
assert typ is not None | ||
types.append(typ) | ||
if default: | ||
has_default.add(name) | ||
|
||
def is_class_var(expr: NameExpr) -> bool: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Move this function to the outer scope (or maybe actually move it to |
||
# import pdb; pdb.set_trace() | ||
if isinstance(expr.node, Var): | ||
return expr.node.is_classvar | ||
return False | ||
|
||
for stmt in ctx.cls.defs.body: | ||
if isinstance(stmt, AssignmentStmt): | ||
name = stmt.lvalues[0].name.lstrip("_") | ||
typ = (AnyType(TypeOfAny.unannotated) if stmt.type is None | ||
else ctx.api.anal_type(stmt.type)) | ||
|
||
if isinstance(stmt.rvalue, TempNode): | ||
print(f"{name}: {typ}") | ||
# `x: int` (without equal sign) assigns rvalue to TempNode(AnyType()) | ||
if has_default: | ||
print("DEFAULT ISSUE") | ||
elif called_function(stmt.rvalue) == 'attr.ib': | ||
if isinstance(stmt, AssignmentStmt) and isinstance(stmt.lvalues[0], NameExpr): | ||
lhs = stmt.lvalues[0] | ||
name = lhs.name.lstrip("_") | ||
typ = stmt.type | ||
print(name, typ, is_class_var(lhs)) | ||
|
||
if called_function(stmt.rvalue) == 'attr.ib': | ||
# Look for a default value in the call. | ||
if get_argument(stmt.rvalue, "default", 0): | ||
has_default.add(name) | ||
print(f"{name} = attr.ib(default=...)") | ||
else: | ||
if has_default: | ||
ctx.api.fail("Non-default attributes not allowed after default attributes.", stmt.rvalue) | ||
print(f"{name} = attr.ib()") | ||
|
||
names.append(name) | ||
types.append(typ) | ||
assert isinstance(stmt.rvalue, CallExpr) | ||
add_init_argument(name, typ, bool(get_argument(stmt.rvalue, "default", 0)), stmt) | ||
else: | ||
print(f"{name} = {stmt.rvalue}") | ||
# rhs[name] = stmt.rvalue | ||
|
||
any_type = AnyType(TypeOfAny.unannotated) | ||
|
||
print(names, types, has_default) | ||
if auto_attribs and not is_class_var(lhs): | ||
# `x: int` (without equal sign) assigns rvalue to TempNode(AnyType()) | ||
has_rhs = not isinstance(stmt.rvalue, TempNode) | ||
add_init_argument(name, typ, has_rhs, stmt) | ||
|
||
args = [] | ||
for (name, typ) in zip(names, types): | ||
var = Var(name, typ) | ||
kind = ARG_OPT if name in has_default else ARG_POS | ||
args.append(Argument(var, var.type, EllipsisExpr(), kind)) | ||
init_args = [ | ||
Argument(Var(name, typ), typ, EllipsisExpr(), | ||
ARG_OPT if name in has_default else ARG_POS) | ||
for (name, typ) in zip(names, types) | ||
] | ||
|
||
add_method( | ||
info=info, | ||
method_name='__init__', | ||
args=args, | ||
args=init_args, | ||
ret_type=NoneTyp(), | ||
self_type=ctx.api.named_type(info.name()), | ||
function_type=ctx.api.named_type('__builtins__.function'), | ||
) | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Our style is to use
(...)
for multiline imports instead of\
, see for example right belowfrom mypy.types import
etc.