Skip to content

Commit c5b8d55

Browse files
authored
[ty] Suppress false positives when dataclasses.dataclass(...)(cls) is called imperatively (#21729)
Fixes astral-sh/ty#1705
1 parent f68080b commit c5b8d55

File tree

2 files changed

+72
-4
lines changed

2 files changed

+72
-4
lines changed

crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1462,3 +1462,57 @@ def test_c():
14621462
c = C(1)
14631463
c.__lt__ = Mock()
14641464
```
1465+
1466+
## Imperatively calling `dataclasses.dataclass`
1467+
1468+
While we do not currently recognize the special behaviour of `dataclasses.dataclass` if it is called
1469+
imperatively, we recognize that it can be called imperatively and do not emit any false-positive
1470+
diagnostics on such calls:
1471+
1472+
```py
1473+
from dataclasses import dataclass
1474+
from typing_extensions import TypeVar, dataclass_transform
1475+
1476+
U = TypeVar("U")
1477+
1478+
@dataclass_transform(kw_only_default=True)
1479+
def sequence(cls: type[U]) -> type[U]:
1480+
d = dataclass(
1481+
repr=False,
1482+
eq=False,
1483+
match_args=False,
1484+
kw_only=True,
1485+
)(cls)
1486+
reveal_type(d) # revealed: type[U@sequence] & Any
1487+
return d
1488+
1489+
@dataclass_transform(kw_only_default=True)
1490+
def sequence2(cls: type) -> type:
1491+
d = dataclass(
1492+
repr=False,
1493+
eq=False,
1494+
match_args=False,
1495+
kw_only=True,
1496+
)(cls)
1497+
reveal_type(d) # revealed: type & Any
1498+
return d
1499+
1500+
@dataclass_transform(kw_only_default=True)
1501+
def sequence3(cls: type[U]) -> type[U]:
1502+
# TODO: should reveal `type[U@sequence3]`
1503+
return reveal_type(dataclass(cls)) # revealed: Unknown
1504+
1505+
@dataclass_transform(kw_only_default=True)
1506+
def sequence4(cls: type) -> type:
1507+
# TODO: should reveal `type`
1508+
return reveal_type(dataclass(cls)) # revealed: Unknown
1509+
1510+
class Foo: ...
1511+
1512+
ordered_foo = dataclass(order=True)(Foo)
1513+
reveal_type(ordered_foo) # revealed: type[Foo] & Any
1514+
# TODO: should be `Foo & Any`
1515+
reveal_type(ordered_foo()) # revealed: @Todo(Type::Intersection.call)
1516+
# TODO: should be `Any`
1517+
reveal_type(ordered_foo() < ordered_foo()) # revealed: @Todo(Type::Intersection.call)
1518+
```

crates/ty_python_semantic/src/types.rs

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6226,12 +6226,26 @@ impl<'db> Type<'db> {
62266226
),
62276227

62286228
Type::Intersection(_) => {
6229-
Binding::single(self, Signature::todo("Type::Intersection.call()")).into()
6229+
Binding::single(self, Signature::todo("Type::Intersection.call")).into()
6230+
}
6231+
6232+
Type::DataclassDecorator(_) => {
6233+
let typevar = BoundTypeVarInstance::synthetic(db, "T", TypeVarVariance::Invariant);
6234+
let typevar_meta = SubclassOfType::from(db, typevar);
6235+
let context = GenericContext::from_typevar_instances(db, [typevar]);
6236+
let parameters = [Parameter::positional_only(Some(Name::new_static("cls")))
6237+
.with_annotated_type(typevar_meta)];
6238+
// Intersect with `Any` for the return type to reflect the fact that the `dataclass()`
6239+
// decorator adds methods to the class
6240+
let returns = IntersectionType::from_elements(db, [typevar_meta, Type::any()]);
6241+
let signature = Signature::new_generic(
6242+
Some(context),
6243+
Parameters::new(db, parameters),
6244+
Some(returns),
6245+
);
6246+
Binding::single(self, signature).into()
62306247
}
62316248

6232-
// TODO: this is actually callable
6233-
Type::DataclassDecorator(_) => CallableBinding::not_callable(self).into(),
6234-
62356249
// TODO: some `SpecialForm`s are callable (e.g. TypedDicts)
62366250
Type::SpecialForm(_) => CallableBinding::not_callable(self).into(),
62376251

0 commit comments

Comments
 (0)