Skip to content
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

add support for dataclasses #5010

Merged
merged 23 commits into from Jun 5, 2018
Merged
Changes from 14 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
File filter...
Filter file types
Jump to…
Jump to file or symbol
Failed to load files and symbols.
+1,148 −66
Diff settings

Always

Just for now

Copy path View file
@@ -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
@@ -302,13 +301,19 @@ 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

This comment has been minimized.

Copy link
@ilevkivskyi

ilevkivskyi May 14, 2018

Collaborator

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.

This comment has been minimized.

Copy link
@Bogdanp

Bogdanp May 20, 2018

Author Contributor

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!

This comment has been minimized.

Copy link
@ilevkivskyi

ilevkivskyi May 24, 2018

Collaborator

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
)
# TODO: Drop the or clause once dataclasses lands in typeshed.

This comment has been minimized.

Copy link
@ilevkivskyi

ilevkivskyi May 24, 2018

Collaborator

I think you can actually overtake the typeshed PR python/typeshed#1944, the original author seems to not have time to finish it.

This comment has been minimized.

Copy link
@Bogdanp

Bogdanp May 27, 2018

Author Contributor

Sorry, but I don't think I'll have the time to carry that through at the moment.

This comment has been minimized.

Copy link
@ilevkivskyi

ilevkivskyi Jun 2, 2018

Collaborator

The typeshed PR is landed. Is this still needed?

This comment has been minimized.

Copy link
@Bogdanp

Bogdanp Jun 4, 2018

Author Contributor

Looks like it still is, not sure why, but after updating the typeshed submodule to the current master, if I remove the or clause and run mypy against my project, the issue still happens.

elif fullname in dataclasses.dataclass_makers or fullname.endswith('.dataclass'):
return dataclasses.dataclass_class_maker_callback
return None


Copy path View file
@@ -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
@@ -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.
Copy path View file
@@ -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(

This comment has been minimized.

Copy link
@ilevkivskyi

ilevkivskyi May 14, 2018

Collaborator

Do I need to review these three functions or you just copied these from attrs?

This comment has been minimized.

Copy link
@Bogdanp

Bogdanp May 20, 2018

Author Contributor

Yes, these are the same functions from the attrs module, unchanged.

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)
Oops, something went wrong.
ProTip! Use n and p to navigate between commits in a pull request.
You can’t perform that action at this time.