[ty] Suppress false positives when `dataclasses.dataclass(...)(cls)` is called imperatively (#21729)

Fixes https://github.com/astral-sh/ty/issues/1705
This commit is contained in:
Alex Waygood 2025-12-03 08:05:25 +00:00 committed by GitHub
parent f68080b55e
commit c5b8d551df
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 71 additions and 3 deletions

View File

@ -1462,3 +1462,57 @@ def test_c():
c = C(1) c = C(1)
c.__lt__ = Mock() c.__lt__ = Mock()
``` ```
## Imperatively calling `dataclasses.dataclass`
While we do not currently recognize the special behaviour of `dataclasses.dataclass` if it is called
imperatively, we recognize that it can be called imperatively and do not emit any false-positive
diagnostics on such calls:
```py
from dataclasses import dataclass
from typing_extensions import TypeVar, dataclass_transform
U = TypeVar("U")
@dataclass_transform(kw_only_default=True)
def sequence(cls: type[U]) -> type[U]:
d = dataclass(
repr=False,
eq=False,
match_args=False,
kw_only=True,
)(cls)
reveal_type(d) # revealed: type[U@sequence] & Any
return d
@dataclass_transform(kw_only_default=True)
def sequence2(cls: type) -> type:
d = dataclass(
repr=False,
eq=False,
match_args=False,
kw_only=True,
)(cls)
reveal_type(d) # revealed: type & Any
return d
@dataclass_transform(kw_only_default=True)
def sequence3(cls: type[U]) -> type[U]:
# TODO: should reveal `type[U@sequence3]`
return reveal_type(dataclass(cls)) # revealed: Unknown
@dataclass_transform(kw_only_default=True)
def sequence4(cls: type) -> type:
# TODO: should reveal `type`
return reveal_type(dataclass(cls)) # revealed: Unknown
class Foo: ...
ordered_foo = dataclass(order=True)(Foo)
reveal_type(ordered_foo) # revealed: type[Foo] & Any
# TODO: should be `Foo & Any`
reveal_type(ordered_foo()) # revealed: @Todo(Type::Intersection.call)
# TODO: should be `Any`
reveal_type(ordered_foo() < ordered_foo()) # revealed: @Todo(Type::Intersection.call)
```

View File

@ -6226,11 +6226,25 @@ impl<'db> Type<'db> {
), ),
Type::Intersection(_) => { Type::Intersection(_) => {
Binding::single(self, Signature::todo("Type::Intersection.call()")).into() Binding::single(self, Signature::todo("Type::Intersection.call")).into()
} }
// TODO: this is actually callable Type::DataclassDecorator(_) => {
Type::DataclassDecorator(_) => CallableBinding::not_callable(self).into(), let typevar = BoundTypeVarInstance::synthetic(db, "T", TypeVarVariance::Invariant);
let typevar_meta = SubclassOfType::from(db, typevar);
let context = GenericContext::from_typevar_instances(db, [typevar]);
let parameters = [Parameter::positional_only(Some(Name::new_static("cls")))
.with_annotated_type(typevar_meta)];
// Intersect with `Any` for the return type to reflect the fact that the `dataclass()`
// decorator adds methods to the class
let returns = IntersectionType::from_elements(db, [typevar_meta, Type::any()]);
let signature = Signature::new_generic(
Some(context),
Parameters::new(db, parameters),
Some(returns),
);
Binding::single(self, signature).into()
}
// TODO: some `SpecialForm`s are callable (e.g. TypedDicts) // TODO: some `SpecialForm`s are callable (e.g. TypedDicts)
Type::SpecialForm(_) => CallableBinding::not_callable(self).into(), Type::SpecialForm(_) => CallableBinding::not_callable(self).into(),