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 function signature hooks for dataclasses functions: replace, asdict, astuple #5152

Open
ilevkivskyi opened this issue Jun 5, 2018 · 6 comments

Comments

@ilevkivskyi
Copy link
Member

Currently, the plugin only supports dataclass creation (i.e. generation of dunder methods with correct types, including __init__). However, some functions in the dataclass modules have quite broad types in typeshed, for example:

def replace(obj: _T, **changes: Any) -> _T: ...

so that arbitrary arguments can be give for changes, etc. The plugin should be able to give more precise type for these functions. In particular, asdict could return a TypedDict.

@ilevkivskyi ilevkivskyi added feature priority-1-normal topic-plugins The plugin API and ideas for new plugins labels Jun 5, 2018
syastrov added a commit to syastrov/mypy that referenced this issue Mar 26, 2020
syastrov added a commit to syastrov/mypy that referenced this issue Mar 26, 2020
syastrov added a commit to syastrov/mypy that referenced this issue Mar 26, 2020
syastrov added a commit to syastrov/mypy that referenced this issue Aug 18, 2020
syastrov added a commit to syastrov/mypy that referenced this issue Aug 18, 2020
@danr
Copy link

danr commented Mar 25, 2021

How would one go about to fix the type signature for replace? In TypeScript it is possible to give a replace function this type:

function replace<X extends Record<string, any>>(base: X, changes: Partial<X>): X {
  return {...base, ...changes}
}

It's not a 100% correspondence, I'm using TS' Record type instead of dataclass and just one single changes argument instead of a **changes keyword argument. But in essence it's the type I want to give replace from the dataclasses module.

@edaqa-uncountable
Copy link

This hole is allowing errors to leak through in some of my production code. Lacking a 'typeof' operator as well, it's difficult to ensure the types are correct.

@OlegAlexander
Copy link

Hello and thank you for creating mypy!!

If I may, I'd like to make a case for this issue. dataclasses.replace() is equivalent to the with keyword in F#. For anyone wishing to do typed functional programming in Python, having replace() checked at compile time is critical. This issue was opened 5 years ago and even the mypy documentation says:

Some functions in the dataclasses module, such as replace() and asdict(), have imprecise (too permissive) types. This will be fixed in future releases.

I can only assume that this isn't a trivial issue to fix.

It seems I haven't made a strong case for this issue after all--I've only managed to express my frustration 😢. It's just that the next time someone asks me if it's possible to do typed functional programming in Python, I'd love to say "Yes!" instead of "Almost!"

@ikonst
Copy link
Contributor

ikonst commented Mar 6, 2023

With asdict returning an anonymous TypedDict (as planned in #8583), would this bring us any closer to closing this issue?

If the TypedDict was not anonymous, and better yet if it was part of dataclasses (e.g. if a dataclass would have __TYPED_DICT__ and in particular __PARTIAL_TYPED_DICT__ that's total=False) then this could perhaps be done in the type definition a'la

def replace(obj: _T, **changes: _T.__PARTIAL_TYPED_DICT__) -> _T: ...

Otherwise we could try something like #14526 where we determine the signature in a function sig hook. Curiously while in attrs.evolve we can take the __init__ signature and mutate it to be all-ARG_NAMED_OPT, for replace we must make init=True arguments ARG_NAMED since they must be explicitly re-specified during replace.

@ilevkivskyi
Copy link
Member Author

FWIW I remember I thought #8583 was an OK solution. But we also need to support other methods: astuple() and replace(). They should be quite straightforward to implement, it is just a matter of spending time on carefully writing the code.

ilevkivskyi pushed a commit that referenced this issue 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]>
@OlegAlexander
Copy link

dataclasses.replace works properly since mypy 1.5.0. Thank you for fixing this!!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

6 participants