-
-
Notifications
You must be signed in to change notification settings - Fork 3k
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
Changes from 14 commits
92b5347
98becd6
3979713
4ab42a1
58ae577
7f7415e
addde6e
3004fac
f5f52a8
74436ea
2918ebd
7c857cb
37eaae8
afbeb78
8d2dd6d
9d08b7a
8b20ceb
5b78ad7
3b9c215
55dfc7e
988c6e8
f007a78
747473e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
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. | ||
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 you can actually overtake the typeshed PR python/typeshed#1944, the original author seems to not have time to finish 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. Sorry, but I don't think I'll have the time to carry that through at the moment. 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 typeshed PR is landed. Is this still needed? 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. 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 | ||
|
||
|
||
|
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( | ||
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. Do I need to review these three functions or you just copied these from attrs? 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. 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) |
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.
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.
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.
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 ensureimport mypy.plugins.common
could be imported failed. I think the circular dependency can be fixed by extractingClassDefContext
into a separate module (likemypy.plugin_context
). Let me know if you'd like me to do that!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.
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.