Skip to content

add support for dataclasses #5010

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

Merged
merged 23 commits into from
Jun 5, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 9 additions & 5 deletions mypy/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
from functools import partial
from typing import Callable, List, Tuple, Optional, NamedTuple, TypeVar, Dict

import mypy.plugins.attrs
from mypy.nodes import (
Expression, StrExpr, IntExpr, UnaryExpr, Context, DictExpr, ClassDef,
TypeInfo, SymbolTableNode, MypyFile
Expand Down Expand Up @@ -302,13 +301,18 @@ def get_method_hook(self, fullname: str

def get_class_decorator_hook(self, fullname: str
) -> Optional[Callable[[ClassDefContext], None]]:
if fullname in mypy.plugins.attrs.attr_class_makers:
return mypy.plugins.attrs.attr_class_maker_callback
elif fullname in mypy.plugins.attrs.attr_dataclass_makers:
from mypy.plugins import attrs
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do you need the local import? If there is an import cycle, you should try breaking it. At mypy we try hard to not introduce import cycles, because they complicate the (already complex) code logic.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There had already been a circular dependency that was being resolved by this line. When I extracted mypy.plugins.common the same thing continued to work fine for the test suite, but the dynamically-generated test to ensure import mypy.plugins.common could be imported failed. I think the circular dependency can be fixed by extracting ClassDefContext into a separate module (like mypy.plugin_context). Let me know if you'd like me to do that!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the circular dependency can be fixed by extracting ClassDefContext into a separate module (like mypy.plugin_context). Let me know if you'd like me to do that!

I think moving out all the interfaces to a separate file mypy.api is a more permanent solution. But it is a big refactoring, so it is better to do this in a separate PR.

from mypy.plugins import dataclasses

if fullname in attrs.attr_class_makers:
return attrs.attr_class_maker_callback
elif fullname in attrs.attr_dataclass_makers:
return partial(
mypy.plugins.attrs.attr_class_maker_callback,
attrs.attr_class_maker_callback,
auto_attribs_default=True
)
elif fullname in dataclasses.dataclass_makers:
return dataclasses.dataclass_class_maker_callback
return None


Expand Down
64 changes: 3 additions & 61 deletions mypy/plugins/attrs.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@
is_class_var, TempNode, Decorator, MemberExpr, Expression, FuncDef, Block,
PassStmt, SymbolTableNode, MDEF, JsonDict, OverloadedFuncDef
)
from mypy.plugins.common import (
_get_argument, _get_bool_argument, _get_decorator_bool_argument
)
from mypy.types import (
Type, AnyType, TypeOfAny, CallableType, NoneTyp, TypeVarDef, TypeVarType,
Overloaded, Instance, UnionType, FunctionLike
Expand Down Expand Up @@ -468,67 +471,6 @@ def _add_init(ctx: 'mypy.plugin.ClassDefContext', attributes: List[Attribute],
func_type.arg_types[0] = ctx.api.class_type(ctx.cls.info)


def _get_decorator_bool_argument(
ctx: 'mypy.plugin.ClassDefContext',
name: str,
default: bool) -> bool:
"""Return the bool argument for the decorator.

This handles both @attr.s(...) and @attr.s
"""
if isinstance(ctx.reason, CallExpr):
return _get_bool_argument(ctx, ctx.reason, name, default)
else:
return default


def _get_bool_argument(ctx: 'mypy.plugin.ClassDefContext', expr: CallExpr,
name: str, default: bool) -> bool:
"""Return the boolean value for an argument to a call or the default if it's not found."""
attr_value = _get_argument(expr, name)
if attr_value:
ret = ctx.api.parse_bool(attr_value)
if ret is None:
ctx.api.fail('"{}" argument must be True or False.'.format(name), expr)
return default
return ret
return default


def _get_argument(call: CallExpr, name: str) -> Optional[Expression]:
"""Return the expression for the specific argument."""
# To do this we use the CallableType of the callee to find the FormalArgument,
# then walk the actual CallExpr looking for the appropriate argument.
#
# Note: I'm not hard-coding the index so that in the future we can support other
# attrib and class makers.
callee_type = None
if (isinstance(call.callee, RefExpr)
and isinstance(call.callee.node, (Var, FuncBase))
and call.callee.node.type):
callee_node_type = call.callee.node.type
if isinstance(callee_node_type, Overloaded):
# We take the last overload.
callee_type = callee_node_type.items()[-1]
elif isinstance(callee_node_type, CallableType):
callee_type = callee_node_type

if not callee_type:
return None

argument = callee_type.argument_by_name(name)
if not argument:
return None
assert argument.name

for i, (attr_name, attr_value) in enumerate(zip(call.arg_names, call.args)):
if argument.pos is not None and not attr_name and i == argument.pos:
return attr_value
if attr_name == argument.name:
return attr_value
return None


class MethodAdder:
"""Helper to add methods to a TypeInfo.

Expand Down
110 changes: 110 additions & 0 deletions mypy/plugins/common.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
from typing import List, Optional

from mypy.nodes import (
ARG_OPT, ARG_POS, MDEF, Argument, Block, CallExpr, Expression, FuncBase,
FuncDef, PassStmt, RefExpr, SymbolTableNode, Var
)
from mypy.plugin import ClassDefContext
from mypy.semanal import set_callable_name
from mypy.types import CallableType, Overloaded, Type, TypeVarDef
from mypy.typevars import fill_typevars


def _get_decorator_bool_argument(
ctx: ClassDefContext,
name: str,
default: bool,
) -> bool:
"""Return the bool argument for the decorator.

This handles both @decorator(...) and @decorator.
"""
if isinstance(ctx.reason, CallExpr):
return _get_bool_argument(ctx, ctx.reason, name, default)
else:
return default


def _get_bool_argument(ctx: ClassDefContext, expr: CallExpr,
name: str, default: bool) -> bool:
"""Return the boolean value for an argument to a call or the
default if it's not found.
"""
attr_value = _get_argument(expr, name)
if attr_value:
ret = ctx.api.parse_bool(attr_value)
if ret is None:
ctx.api.fail('"{}" argument must be True or False.'.format(name), expr)
return default
return ret
return default


def _get_argument(call: CallExpr, name: str) -> Optional[Expression]:
"""Return the expression for the specific argument."""
# To do this we use the CallableType of the callee to find the FormalArgument,
# then walk the actual CallExpr looking for the appropriate argument.
#
# Note: I'm not hard-coding the index so that in the future we can support other
# attrib and class makers.
callee_type = None
if (isinstance(call.callee, RefExpr)
and isinstance(call.callee.node, (Var, FuncBase))
and call.callee.node.type):
callee_node_type = call.callee.node.type
if isinstance(callee_node_type, Overloaded):
# We take the last overload.
callee_type = callee_node_type.items()[-1]
elif isinstance(callee_node_type, CallableType):
callee_type = callee_node_type

if not callee_type:
return None

argument = callee_type.argument_by_name(name)
if not argument:
return None
assert argument.name

for i, (attr_name, attr_value) in enumerate(zip(call.arg_names, call.args)):
if argument.pos is not None and not attr_name and i == argument.pos:
return attr_value
if attr_name == argument.name:
return attr_value
return None


def _add_method(
ctx: ClassDefContext,
name: str,
args: List[Argument],
return_type: Type,
self_type: Optional[Type] = None,
tvar_def: Optional[TypeVarDef] = None,
) -> None:
"""Adds a new method to a class.
"""
info = ctx.cls.info
self_type = self_type or fill_typevars(info)
function_type = ctx.api.named_type('__builtins__.function')

args = [Argument(Var('self'), self_type, None, ARG_POS)] + args
arg_types, arg_names, arg_kinds = [], [], []
for arg in args:
assert arg.type_annotation, 'All arguments must be fully typed.'
arg_types.append(arg.type_annotation)
arg_names.append(arg.variable.name())
arg_kinds.append(arg.kind)

signature = CallableType(arg_types, arg_kinds, arg_names, return_type, function_type)
if tvar_def:
signature.variables = [tvar_def]

func = FuncDef(name, args, Block([PassStmt()]))
func.info = info
func.type = set_callable_name(signature, func)
func._fullname = info.fullname() + '.' + name
func.line = info.line

info.names[name] = SymbolTableNode(MDEF, func)
info.defn.defs.body.append(func)
Loading