Repository navigation
Add signature for dataclasses.replace - #14849
Conversation
This comment has been minimized.
This comment has been minimized.
| arg_names = [None] | ||
| arg_kinds = [ARG_POS] | ||
| arg_types = [obj_type] | ||
| for attr in dataclass["attributes"]: |
There was a problem hiding this comment.
I wish I could've deserialized metadata, or even accessed a list of DataclassAttributes that's already been deserialized...
Or maybe the right way to go is to replace dataclasses.replace with an OverloadedFuncDef that the transform (which has full context) will keep adding to.
There was a problem hiding this comment.
The OverloadedFuncDef appears to be a non-starter since by the time I can do anything to replace dataclasses.replace with a synthesized OverloadedFuncDef, the call expression has been resolved with the pointer to the generic FunctionDef.
There was a problem hiding this comment.
OK, solved it by creating a "secret" symbol with a signature at semantic analysis time, then using this signature in the function sig callback.
f6c3a8f to
1f08816
Compare
This comment has been minimized.
This comment has been minimized.
|
@ilevkivskyi care to take a look? |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
AlexWaygood
left a comment
There was a problem hiding this comment.
I'm just a triager over at mypy, so I'm afraid I don't have merge privileges! Here's a quick comment though:
|
|
||
| class Field(Generic[_T]): pass | ||
|
|
||
| def replace(obj: _T, **changes: Any) -> _T: ... |
There was a problem hiding this comment.
This is pretty different to typeshed's stub for replace. Would it be worth adding a pythoneval test as well, to check that this machinery all works with the full typeshed stubs for the stdlib, outside the context of mypy's test suite?
There was a problem hiding this comment.
The stub has:
def replace(__obj: _DataclassT, **changes: Any) -> _DataclassT: ...
I can rename obj to __obj in this fixture. We can also change the stub to better reflect reality:
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 obj as kw is deprecated in Python 3.8 so being a checker/linter we might as well just disallow it, no?
Do you think it matters much? For the plugin to function, replace has to:
- exist
- have a first argument
The **changes are optional, the retval is overwritten, and FWIW it doesn't look for a keyword arg obj since it's deprecated since Python 3.8 and was never a good idea to begin with, so we might as well just not support it, right?
There was a problem hiding this comment.
We can also change the stub to better reflect reality:
No need: prepending the parameter name with __ is the py37-compatible way of declaring that a parameter is positional-only, as is described in PEP 484: https://peps.python.org/pep-0484/#positional-only-arguments. So the obj parameter is already marked as positional-only on all Python versions in typeshed's stubs.
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 pythoneval.test file. Unlike mypy's other tests, the tests in that file don't use fixtures — they run with typeshed's full stdlib stubs. So it's a good sanity check to make sure that the machinery you've added works with typeshed's stubs for this function as well as with mypy's fixtures.
…konst/mypy into 2023-03-06-dataclasses-replace
This comment has been minimized.
This comment has been minimized.
Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
This comment has been minimized.
This comment has been minimized.
|
According to mypy_primer, this change doesn't affect type check results on a corpus of open source code. ✅ |
|
@ilevkivskyi can take another look? |
|
@ikonst Please note there is now a merge conflict. You can also address couple remaining comments before I merge this. |
|
Diff from mypy_primer, showing the effect of this PR on open source code: graphql-core (https://github.com/graphql-python/graphql-core): typechecking got 1.06x slower (281.6s -> 297.4s)
(Performance measurements are based on a single noisy sample)
|
…5503) Now we use a similar approach to #14849 First, we generate a private name to store in a metadata (with `-`, so - no conflicts, ever). Next, we check override to be compatible: we take the currect signature and compare it to the ideal one we have. Simple and it works :) Closes #15498 Closes #9254 --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Ivan Levkivskyi <levkivskyi@gmail.com>
Supersedes the previous commit's double solve. A review of it found that keeping a precise solution only when it refines the old one guards the call, not what the caller does with the result: `[copy.replace(t), A()]` joined `T` with `A` to `object`, so `items[0].x` failed. With the join fixed underneath (join(T, S) is join(upper_bound(T), S), previous merge), a single solve suffices: - Mixed bounds. When a protocol is matched against a type variable `T`, lower-bound constraints (SUPERTYPE_OF) come from binding self to `T`, and upper-bound constraints (SUBTYPE_OF) from binding self to the upper bound, as before. Binding to `T` only ever makes a lower bound smaller and an upper bound stricter, so this takes the weaker of each. Upper bounds and their meets are what the old inference produced, and lower bounds join to the old result unless every lower bound comes from `T`: `add(a: TV, b: V)` still infers `V` (join(TV, V) is V), `use2(t, d)` with contravariant `Self` still infers `D`, and `copy.replace(t)` alone infers `T`. - Removed: the second solve, refines(), the binding flag and counter on type_state, and their use in infer_function_type_arguments() and unify_generic_callable(). bind_self() and generic-callable subtype checks now get the same mixed bounds, so `t.m()` with `self: SupportsRep[X]` returns `T` and `t.n(c)` with a contravariant `Self` still accepts `C`. - A type variable is bound to only if it isn't one of the type variables of a generic callable being matched. The checker-scope test is gone: it made is_subtype() depend on where it ran while subtype caches are keyed only on types, so the same check could pass in one function and fail in another depending on order (testTypeVarSelfTypeProtoSubtypingIndependentOfScope). - A member binds self non-trivially unless it's a method with FuncDef.is_trivial_self, a decorator over one with no other decorators, or an attribute whose type has no type variables. Dataclass fields with concrete types no longer trigger the self-to-type-variable path. Consequences pinned in tests: a call's `T` result joined with a value of the bound gives the bound, as before (testTypeVarSelfTypeProtoResultJoinsWithBound); a variable first assigned a `T` result is typed `T`, so assigning a value of the bound later is an error, exactly as for `w = t; w = c` (testTypeVarSelfTypeProtoResultTypesVariable). The synthesized `__replace__` on a dataclass whose subclass narrows a field now behaves like `dataclasses.replace(t, ...)`, which has returned `T` since python#14849 (testDunderReplaceFieldNarrowingMatchesDataclassesReplace). Co-Authored-By: claude-opus-5-5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Mdn8NkvmAM1PfoEsARpBk6
Validate
dataclassses.replaceactual arguments to match the fields:__init__, the arguments are always named.InitVars without a default value.The tricks:
__mypy-replace(obviously not part of PEP-557 but contains a hyphen so should not conflict with any future valid identifier). Stashing the signature into the symbol table allows it to be passed across phases and cached across invocations. The stashed signature lacks the first argument, which we prepend at function signature hook time, since it depends on the type thatreplaceis called on.Based on #14526 but actually simpler.
Partially addresses #5152.
Remaining tasks