-
-
Notifications
You must be signed in to change notification settings - Fork 3.2k
Add signature for dataclasses.replace #14849
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 3 commits
6dbaf9c
4dcbe44
7ed3741
89257b5
32b1d47
1f08816
c456a5f
8118d29
789cb2b
9f0974c
a37e406
367c0e9
0e84c4f
9cfc081
7b907cf
985db60
15dbb7b
3227fde
2dbf249
b32881c
26056a4
40315b7
c005895
d71bc21
d914b94
2cb6dee
5735726
d38897e
04f0ee3
f402b86
306c3f3
9b491f5
283fe3d
29780e9
9c43ab6
94024ba
70240c4
99bf973
8fe75a7
bea50e8
cd35951
d71a7c0
957744e
6abf6ff
4c4fc94
be4a290
ee0ae21
3f656f8
65d6e89
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 |
|---|---|---|
|
|
@@ -7,6 +7,7 @@ | |
|
|
||
| from mypy import errorcodes, message_registry | ||
| from mypy.expandtype import expand_type | ||
| from mypy.messages import format_type_bare | ||
| from mypy.nodes import ( | ||
| ARG_NAMED, | ||
| ARG_NAMED_OPT, | ||
|
|
@@ -35,7 +36,7 @@ | |
| TypeVarExpr, | ||
| Var, | ||
| ) | ||
| from mypy.plugin import ClassDefContext, SemanticAnalyzerPluginInterface | ||
| from mypy.plugin import ClassDefContext, FunctionSigContext, SemanticAnalyzerPluginInterface | ||
| from mypy.plugins.common import ( | ||
| _get_decorator_bool_argument, | ||
| add_attribute_to_class, | ||
|
|
@@ -769,3 +770,63 @@ def _is_dataclasses_decorator(node: Node) -> bool: | |
| if isinstance(node, RefExpr): | ||
| return node.fullname in dataclass_makers | ||
| return False | ||
|
|
||
|
|
||
| def replace_function_sig_callback(ctx: FunctionSigContext) -> CallableType: | ||
| """ | ||
| Generates a signature for the 'dataclasses.replace' function that's specific to the call site | ||
| and dependent on the type of the first argument. | ||
| """ | ||
| if len(ctx.args) != 2: | ||
| # Ideally the name and context should be callee's, but we don't have it in FunctionSigContext. | ||
| ctx.api.fail(f'"{ctx.default_signature.name}" has unexpected type annotation', ctx.context) | ||
| return ctx.default_signature | ||
|
|
||
| if len(ctx.args[0]) != 1: | ||
| return ctx.default_signature # leave it to the type checker to complain | ||
|
|
||
| inst_arg = ctx.args[0][0] | ||
|
|
||
| # <hack> | ||
| from mypy.checker import TypeChecker | ||
|
|
||
| assert isinstance(ctx.api, TypeChecker) | ||
| obj_type = ctx.api.expr_checker.accept(inst_arg) | ||
| # </hack> | ||
|
|
||
| obj_type = get_proper_type(obj_type) | ||
| if not isinstance(obj_type, Instance): | ||
| return ctx.default_signature | ||
| inst_type_str = format_type_bare(obj_type) | ||
|
|
||
| metadata = obj_type.type.metadata | ||
| dataclass = metadata.get("dataclass") | ||
| if not dataclass: | ||
| ctx.api.fail( | ||
| f'Argument 1 to "replace" has incompatible type "{inst_type_str}"; expected a dataclass', | ||
| ctx.context, | ||
| ) | ||
| return ctx.default_signature | ||
|
|
||
| arg_names = [None] | ||
| arg_kinds = [ARG_POS] | ||
| arg_types = [obj_type] | ||
| for attr in dataclass["attributes"]: | ||
|
Contributor
Author
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 wish I could've deserialized metadata, or even accessed a list of Or maybe the right way to go is to replace
Contributor
Author
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
Contributor
Author
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. OK, solved it by creating a "secret" symbol with a signature at semantic analysis time, then using this signature in the function sig callback. |
||
| if not attr["is_in_init"]: | ||
| continue | ||
| arg_names.append(attr["name"]) | ||
| arg_kinds.append( | ||
| ARG_NAMED if not attr["has_default"] and attr["is_init_var"] else ARG_NAMED_OPT | ||
| ) | ||
| arg_types.append(ctx.api.named_type(attr["type"])) | ||
|
|
||
| return ctx.default_signature.copy_modified( | ||
| arg_names=arg_names, | ||
| arg_kinds=arg_kinds, | ||
| arg_types=arg_types, | ||
| ret_type=obj_type, | ||
| name=f"{ctx.default_signature.name} of {inst_type_str}", | ||
| # prevent 'dataclasses.pyi:...: note: "replace" of "A" defined here' notes | ||
| # since they are misleading: the definition is dynamic, not from a definition | ||
| definition=None, | ||
| ) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -32,3 +32,5 @@ def field(*, | |
|
|
||
|
|
||
| class Field(Generic[_T]): pass | ||
|
|
||
| def replace(obj: _T, **changes: Any) -> _T: ... | ||
|
Member
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. This is pretty different to typeshed's stub for
Contributor
Author
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 stub has: I can rename if sys.version_info >= (3, 9):
def replace(obj: _DataclassT, /, **changes: Any) -> _DataclassT: ...
else:
def replace(obj: _DataclassT, **changes: Any) -> _DataclassT: ...Can we just use the Python 3.9 syntax even for Python 3.8 targets? Passing Do you think it matters much? For the plugin to function,
The
Member
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.
No need: prepending the parameter name with I don't think you need to change anything in your existing fixtures/tests for this PR, I'm just suggesting adding a single additional test in the
Contributor
Author
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. |
||
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.
FWIW this kind of hack is quite common (I myself used it several times, for old sqlalchemy plugin, and internally). I would propose to instead add another plugin hook, e.g.
get_function_signature_hook_2(similar to what we have for classes), that would be called afterinfer_arg_types_in_context()with a context that includes inferred argument types.cc @JukkaL what do you think?
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.
This proposal still stands. Do we have an issue for this?
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's #14845 and #10216, though not one spelling out the "get_function_signature_hook_2" solution.