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 signature for attr.evolve #14526

Merged
merged 21 commits into from
Mar 6, 2023

Conversation

ikonst
Copy link
Contributor

@ikonst ikonst commented Jan 26, 2023

Validate attr.evolve calls to specify correct arguments and types.

The implementation makes it so that at every point where attr.evolve is called, the signature is modified to expect the attrs class' initializer's arguments (but so that they're all kw-only and optional).

Notes:

Fixes #14525.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

) -> Callable[[FunctionSigContext], FunctionLike] | None:
from mypy.plugins import attrs

if fullname in ("attr.evolve", "attr.assoc"):
Copy link
Contributor

Choose a reason for hiding this comment

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

There’s also attrs.evolve() - maybe that’s just an alias and will work, but it should be tested. Perhaps these should be moved to a constant with the others at the start of the attrs plug-in module also?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yep, yep. All still WIP.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ready now!

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@ikonst
Copy link
Contributor Author

ikonst commented Jan 27, 2023

mypy-primer identified an error in home-assistant:

new = self.areas[area_id] = attr.evolve(old, **new_values)

Typing new_values as a typed dict with the appropriate fields can address that. The misleading error is mentioned in #14073.

@github-actions

This comment has been minimized.

@ikonst ikonst marked this pull request as ready for review January 27, 2023 17:41
@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

Comment on lines 1812 to 1813
2: <b>, <b[wildcard]>, __main__
3: <b>, <b[wildcard]>, __main__, a
Copy link
Contributor Author

@ikonst ikonst Jan 27, 2023

Choose a reason for hiding this comment

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

So.... the addition of dict in fixtures/list.pyi now causes __annotations__ to exist:

mypy/mypy/semanal.py

Lines 636 to 639 in bac9e77

elif name == "__annotations__":
sym = self.lookup_qualified("__builtins__.dict", Context(), suppress_errors=True)
if not sym:
continue

which then makes __annotations__ one of the names in the diff (reasonably...):

mypy/mypy/server/update.py

Lines 781 to 795 in a08388c

if item.count(".") <= package_nesting_level + 1 and item.split(".")[-1] not in (
"__builtins__",
"__file__",
"__name__",
"__package__",
"__doc__",
):
# Activate catch-all wildcard trigger for top-level module changes (used for
# "from m import *"). This also gets triggered by changes to module-private
# entries, but as these unneeded dependencies only result in extra processing,
# it's a minor problem.
#
# TODO: Some __* names cause mistriggers. Fix the underlying issue instead of
# special casing them here.
diff.add(id + WILDCARD_TAG)

I've created #14547 to discuss this more.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Addressed in #14550 and #14575.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@ikonst
Copy link
Contributor Author

ikonst commented Mar 2, 2023

@AlexWaygood can label with attrs? :)

@github-actions

This comment has been minimized.

@ikonst
Copy link
Contributor Author

ikonst commented Mar 2, 2023

@ikonst
Copy link
Contributor Author

ikonst commented Mar 3, 2023

@JelleZijlstra would love to get some eyes on this (I was waiting for #14575 to merge to reduce the diff)

@github-actions

This comment has been minimized.

@github-actions
Copy link
Contributor

github-actions bot commented Mar 3, 2023

According to mypy_primer, this change has no effect on the checked open source code. 🤖🎉

@JelleZijlstra
Copy link
Member

I can take a look in the next few days.

This function is basically equivalent to dataclasses.replace, right? Would be nice if we can support that function in a similar way (in a separate PR).

@JelleZijlstra JelleZijlstra self-requested a review March 3, 2023 18:13
Copy link
Member

@JelleZijlstra JelleZijlstra left a comment

Choose a reason for hiding this comment

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

Looks good, just one minor point.

@@ -1775,7 +1775,7 @@ def copy_modified(
self: CT,
arg_types: Bogus[Sequence[Type]] = _dummy,
arg_kinds: Bogus[list[ArgKind]] = _dummy,
arg_names: Bogus[list[str | None]] = _dummy,
arg_names: Bogus[Sequence[str | None]] = _dummy,
Copy link
Member

Choose a reason for hiding this comment

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

What is the reason for this change? I'd prefer to avoid touching the mypy core in this PR.

Copy link
Contributor Author

@ikonst ikonst Mar 6, 2023

Choose a reason for hiding this comment

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

Because of list's invariance — otherwise, this fails since it passes list[str]:

arg_names=["inst"] + attrs_init_type.arg_names[1:],

CallableType.__init__ accepts arg_names: Sequence[str | None] but I guess CallableType.copy_modified was not updated in tandem.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Broke out into #14840 — can merge then update this branch.

@JelleZijlstra JelleZijlstra merged commit bbc9cce into python:master Mar 6, 2023
Comment on lines +927 to +930
# <hack>
assert isinstance(ctx.api, TypeChecker)
inst_type = ctx.api.expr_checker.accept(inst_arg)
# </hack>
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@JelleZijlstra now that we got this merged 😅 what would be the right place to discuss whether this should be promoted to be formal plugin API?

Copy link
Member

Choose a reason for hiding this comment

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

Open an issue, I suppose.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

👉 #14845

@LukasKrocek
Copy link

LukasKrocek commented Apr 6, 2023

hey, I have tried to run this in my project, doen't work well when evolving generic classes

class A(Generic[T]):
    field: T

a = A("1")

evolve(a, field="2") ```

# 2 errors are raised:
#     Argument 1 to "evolve" of "A[str]" has incompatible type "A[str]"; expected "A[T]"  [arg-type]
#     Argument "field" to "evolve" of "A[str]" has incompatible type "int"; expected "T"  [arg-type]

@ikonst
Copy link
Contributor Author

ikonst commented Apr 6, 2023

@LukasKrocek this was just mentioned in the review for #14849 — addressed in c005895, but here it would actually be simpler

@ikonst
Copy link
Contributor Author

ikonst commented Apr 7, 2023

Generics should be addressed in #15016.

ilevkivskyi pushed a commit that referenced this pull request Jun 17, 2023
Validate `dataclassses.replace` actual arguments to match the fields:

- Unlike `__init__`, the arguments are always named.
- All arguments are optional except for `InitVar`s without a default
value.

The tricks:
- We're looking up type of the first positional argument ("obj") through
private API. See #10216, #14845.
- We're preparing the signature of "replace" (for that specific
dataclass) during the dataclass transformation and storing it in a
"private" class attribute `__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 that `replace` is
called on.

Based on #14526 but actually simpler.
Partially addresses #5152.

# Remaining tasks

- [x] handle generic dataclasses
- [x] avoid data class transforms
- [x] fine-grained mode tests

---------

Co-authored-by: Alex Waygood <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

attrs plugin should support attrs.evolve
5 participants