-
Notifications
You must be signed in to change notification settings - Fork 1.6k
[red-knot] typing.dataclass_transform
#17445
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
Conversation
|
0ff175b to
a2c00b0
Compare
3acb56b to
5aca67b
Compare
|
Ecosystem analysis: There are a few new false positives. We do not understand unannotated dataclass fields: @attr.define
class Foo:
foo = attr.field()
Foo(foo=1) # Argument `foo` does not match any known parameterThe false positives in |
5aca67b to
36ad484
Compare
36ad484 to
63995af
Compare
| # Salsa generates functions with parameters for each field of a `salsa::interned` struct. | ||
| # If we don't allow this, we get warnings for structs with too many fields. | ||
| too_many_arguments = "allow" |
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 didn't find a way to only set this for red_knot_python_semantic, as it inherits this configuration, and it looks like I can't overwrite workspace settings for lints?
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'll merge this for now, but I will open a salsa ticket to improve this, and add a reminder for myself to remove this again.
|
598bfb3 to
4abc9c6
Compare
| # TODO: these should not be errors | ||
| D1("a") # error: [too-many-positional-arguments] | ||
| D2("a") # error: [too-many-positional-arguments] | ||
|
|
||
| # TODO: these should be invalid-argument-type errors | ||
| D1(1.2) # error: [too-many-positional-arguments] | ||
| D2(1.2) # error: [too-many-positional-arguments] |
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 is similar to the (working) test above, but now it's not the implementation that is decorated with @dataclass_transform(), but one of the overloads. This is explicitly allowed in the spec.
I haven't looked into this deeply, but I am assume this isn't working since we never see the decorator on the actual function definition. I am not sure how to handle this, as I'm not yet familiar with how overloads work. And how we would treat decorators on overloads. @dhruvmanila This is not urgent, but if you see an easy solution to this, I'd be glad to hear it.
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.
The logic for collecting overloads is in FunctionType::signature method. I think you'd either need to have that method also return "dataclass transform params, if found" (which is a little weird), or extract the overload-walking logic into a separate method that iterates overloads as FunctionLiteral types, and use that both for your purposes and in ::signature.
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.
extract the overload-walking logic into a separate method that iterates overloads as
FunctionLiteraltypes, and use that both for your purposes and in::signature.
Yeah, I think this is what I'd do as well.
Initially, I thought we could have a method that returns an iterator over the overloaded FunctionType but that wouldn't work reliably because we need to follow through all of the symbols with the same name to find the boundary where the overload ends. This means the method would need to collect all of them, so maybe something like the following method:
struct OverloadedFunction<'db> {
/// The overloads of this function.
overloads: Vec<FunctionType<'db>>,
/// The implementation of this overloaded function, if any.
implementation: Option<FunctionType<'db>>,
}
#[salsa::tracked]
impl<'db> FunctionType<'db> {
/// Returns `self` as [`OverloadedFunction`] if it is overloaded, [`None`] otherwise.
fn to_overloaded(self, db: &'db dyn Db) -> Option<OverloadedFunction<'db>> {
todo!()
}
}This could be used to get the DataclassTransformerParams that's added to a FunctionType which is decorated with @dataclass_transform. Another way would be to add a method to return this and to keep the above method internal-only:
#[salsa::tracked]
impl<'db> FunctionType<'db> {
/// Returns `self` as [`OverloadedFunction`] if it is overloaded, [`None`] otherwise.
fn to_overloaded(self, db: &'db dyn Db) -> Option<OverloadedFunction<'db>> {
todo!()
}
// TODO: What should be the name of this method? Or, should we change the field name?
pub(crate) fn dataclass_transformer_params(self, ...) -> Option<DataclassTransformerParams> {
if let Some(overloaded) = self.to_overloaded(db) {
// get the `DataclassTransformerParams` from `OverloadedFunction`
} else {
self.dataclass_transformer_params(db)
}
}
}We might then need to check whether "the dataclass_transform decorator is applied to any one, but not more than one, of the overloads" and raise diagnostic (although Pyright and mypy doesn't do that) within this function and then it might just be better to actually create the DataclassTransformerParams in this method instead of during inference.
I also thought about this for a bit and I would also need to somehow iterate over all the overloaded function definitions during the inference (at the end of infer_region_scope) to validate the overload usages. The information that I require would all exists on the Signature (would need to copy the FunctionDecorators) which can be fetched by the signature method so that might be ok.
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.
Thank you Carl and Dhruv. I left this open for now, but opened a new (post-alpha?) ticket.
4abc9c6 to
4983da5
Compare
carljm
left a comment
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.
Looks good!
| # TODO: these should not be errors | ||
| D1("a") # error: [too-many-positional-arguments] | ||
| D2("a") # error: [too-many-positional-arguments] | ||
|
|
||
| # TODO: these should be invalid-argument-type errors | ||
| D1(1.2) # error: [too-many-positional-arguments] | ||
| D2(1.2) # error: [too-many-positional-arguments] |
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.
The logic for collecting overloads is in FunctionType::signature method. I think you'd either need to have that method also return "dataclass transform params, if found" (which is a little weird), or extract the overload-walking logic into a separate method that iterates overloads as FunctionLiteral types, and use that both for your purposes and in ::signature.
| # TODO: these should not be errors | ||
| D1("a") # error: [too-many-positional-arguments] | ||
| D2("a") # error: [too-many-positional-arguments] | ||
|
|
||
| # TODO: these should be invalid-argument-type errors | ||
| D1(1.2) # error: [too-many-positional-arguments] | ||
| D2(1.2) # error: [too-many-positional-arguments] |
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.
extract the overload-walking logic into a separate method that iterates overloads as
FunctionLiteraltypes, and use that both for your purposes and in::signature.
Yeah, I think this is what I'd do as well.
Initially, I thought we could have a method that returns an iterator over the overloaded FunctionType but that wouldn't work reliably because we need to follow through all of the symbols with the same name to find the boundary where the overload ends. This means the method would need to collect all of them, so maybe something like the following method:
struct OverloadedFunction<'db> {
/// The overloads of this function.
overloads: Vec<FunctionType<'db>>,
/// The implementation of this overloaded function, if any.
implementation: Option<FunctionType<'db>>,
}
#[salsa::tracked]
impl<'db> FunctionType<'db> {
/// Returns `self` as [`OverloadedFunction`] if it is overloaded, [`None`] otherwise.
fn to_overloaded(self, db: &'db dyn Db) -> Option<OverloadedFunction<'db>> {
todo!()
}
}This could be used to get the DataclassTransformerParams that's added to a FunctionType which is decorated with @dataclass_transform. Another way would be to add a method to return this and to keep the above method internal-only:
#[salsa::tracked]
impl<'db> FunctionType<'db> {
/// Returns `self` as [`OverloadedFunction`] if it is overloaded, [`None`] otherwise.
fn to_overloaded(self, db: &'db dyn Db) -> Option<OverloadedFunction<'db>> {
todo!()
}
// TODO: What should be the name of this method? Or, should we change the field name?
pub(crate) fn dataclass_transformer_params(self, ...) -> Option<DataclassTransformerParams> {
if let Some(overloaded) = self.to_overloaded(db) {
// get the `DataclassTransformerParams` from `OverloadedFunction`
} else {
self.dataclass_transformer_params(db)
}
}
}We might then need to check whether "the dataclass_transform decorator is applied to any one, but not more than one, of the overloads" and raise diagnostic (although Pyright and mypy doesn't do that) within this function and then it might just be better to actually create the DataclassTransformerParams in this method instead of during inference.
I also thought about this for a bit and I would also need to somehow iterate over all the overloaded function definitions during the inference (at the end of infer_region_scope) to validate the overload usages. The information that I require would all exists on the Signature (would need to copy the FunctionDecorators) which can be fetched by the signature method so that might be ok.
4983da5 to
8db0e6e
Compare
* main: (37 commits) [red-knot] Add list of failing/slow ecosystem projects (#17474) [red-knot] mypy_primer: extend ecosystem checks (#17544) [red-knot] Move `InstanceType` to its own submodule (#17525) [red-knot] mypy_primer: capture backtraces (#17543) [red-knot] mypy_primer: Use upstream repo (#17500) [red-knot] `typing.dataclass_transform` (#17445) Update dependency react-resizable-panels to v2.1.8 (#17513) Update dependency smol-toml to v1.3.3 (#17505) Update dependency uuid to v11.1.0 (#17517) Update actions/setup-node action to v4.4.0 (#17514) [red-knot] Fix variable name (#17532) [red-knot] Add basic subtyping between class literal and callable (#17469) [`pyupgrade`] Add fix safety section to docs (`UP030`) (#17443) [`perflint`] Allow list function calls to be replaced with a comprehension (`PERF401`) (#17519) Update pre-commit dependencies (#17506) [red-knot] Simplify visibility constraint handling for `*`-import definitions (#17486) [red-knot] Detect (some) invalid protocols (#17488) [red-knot] Correctly identify protocol classes (#17487) Update dependency ruff to v0.11.6 (#17516) Update Rust crate shellexpand to v3.1.1 (#17512) ...
| @overload | ||
| @dataclass_transform() | ||
| def versioned_class( | ||
| cls: T, | ||
| *, | ||
| version: int = 1, | ||
| ) -> T: ... | ||
| @overload | ||
| def versioned_class( | ||
| *, | ||
| version: int = 1, | ||
| ) -> Callable[[T], T]: ... | ||
| def versioned_class( | ||
| cls: T | None = None, | ||
| *, | ||
| version: int = 1, | ||
| ) -> T | Callable[[T], T]: | ||
| raise NotImplementedError | ||
|
|
||
| @versioned_class | ||
| class D1: | ||
| x: str |
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.
In this example, versioned_class takes in a parameter (version).
In that case, versioned_class, when called, should return a decorator which will accept a class, right?
If the default version value has to be picked up, even then, the invocation should be like this, isn't it?
@versioned_class() # notice ()
class D1:
x: str@sharkdp I did not understand how @versioned_class (without the call) will work.
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.
In this example,
versioned_classtakes in a parameter (version).
Well... it takes a keyword-only argument version. But it can also take a positional cls argument (first overload).
In that case,
versioned_class, when called, should return a decorator which will accept a class, right?
If it's called with versioned_class(version=2), it should return a decorator, yes.
But if it's called with versioned_class(some_class), where some_class is of type T, it should return a T (first overload). This lets versioned_class itself also act as a decorator.
If the default
versionvalue has to be picked up, even then, the invocation should be like this, isn't it?
@versioned_class() # notice ()
Using @versioned_class() should also work (it calls the second overload), but the purpose of this test was to check that it also works without the parens (in which case it is called with one argument, the D1 class, and picks the first overload).
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.
Thanks for the explanation.
If it's called with versioned_class(version=2), it should return a decorator, yes.
For cases where versioned_class itself is the decorator (first overload), there is no way to pass a custom version value right? If so, what is the point of having the version kwarg in the first overload's signature? (ie, why not just versioned_class(cls: T) -> T?)
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 guess you could still use it with non-decorator syntax
class C: …
C = versioned_class(C, version=2)But yeah, I agree, it's probably not that useful. I added this test because a lot of libraries like attrs, pydantic, or strawberry use this exact pattern. They provide dataclass-like decorators which you can use with or without arguments. And using these two overloads is exactly how they are implemented.
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.
Got it, thanks for the context!
Summary
typing.dataclass_transform@dataclass_transform(…)(used byattrs,strawberry)@dataclass_transform(…)(used bypydantic, but doesn't work yet, because we don't seem to model__new__calls correctly?)@dataclass_transform(…). I haven't figured out how this even supposed to work. And haven't seen it being used.strawberryas an ecosystem project, as it makes heavy use of@dataclass_transformTest Plan
New Markdown tests