Skip to content

Commit 1514996

Browse files
committed
Support dataclasses.InitVar
1 parent 163a108 commit 1514996

File tree

8 files changed

+287
-80
lines changed

8 files changed

+287
-80
lines changed
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
# `dataclasses.InitVar`
2+
3+
From the Python documentation on [`dataclasses.InitVar`]:
4+
5+
If a field is an `InitVar`, it is considered a pseudo-field called an init-only field. As it is not
6+
a true field, it is not returned by the module-level `fields()` function. Init-only fields are added
7+
as parameters to the generated `__init__()` method, and are passed to the optional `__post_init__()`
8+
method. They are not otherwise used by dataclasses.
9+
10+
## Basic
11+
12+
Consider the following dataclass example where the `db` attribute is annotated with `InitVar`:
13+
14+
```py
15+
from dataclasses import InitVar, dataclass
16+
17+
class Database: ...
18+
19+
@dataclass(order=True)
20+
class Person:
21+
db: InitVar[Database]
22+
23+
name: str
24+
age: int
25+
```
26+
27+
We can see in the signature if `__init__`, that `db` is included as an argument:
28+
29+
```py
30+
reveal_type(Person.__init__) # revealed: (self: Person, db: Database, name: str, age: int) -> None
31+
```
32+
33+
However, when we create an instance of this dataclass, the `db` attribute is not accessible:
34+
35+
```py
36+
db = Database()
37+
alice = Person(db, "Alice", 30)
38+
39+
alice.db # error: [unresolved-attribute]
40+
```
41+
42+
The `db` attribute is also not accessible on the class itself:
43+
44+
```py
45+
Person.db # error: [unresolved-attribute]
46+
```
47+
48+
Other fields can still be accessed normally:
49+
50+
```py
51+
reveal_type(alice.name) # revealed: str
52+
reveal_type(alice.age) # revealed: int
53+
```
54+
55+
## `InitVar` wit default value
56+
57+
An `InitVar` can also have a default value
58+
59+
```py
60+
from dataclasses import InitVar, dataclass
61+
62+
@dataclass
63+
class Person:
64+
name: str
65+
age: int
66+
67+
metadata: InitVar[str] = "default"
68+
69+
reveal_type(Person.__init__) # revealed: (self: Person, name: str, age: int, metadata: str = Literal["default"]) -> None
70+
71+
alice = Person("Alice", 30)
72+
bob = Person("Bob", 25, "custom metadata")
73+
```
74+
75+
## Error cases
76+
77+
### Syntax
78+
79+
`InitVar` can only be used with a single argument:
80+
81+
```py
82+
from dataclasses import InitVar, dataclass
83+
84+
@dataclass
85+
class Wrong:
86+
x: InitVar[int, str] # error: [invalid-type-form] "Type qualifier `InitVar` expected exactly 1 argument, got 2"
87+
```
88+
89+
A bare `InitVar` is not allowed according to the [type annotation grammar]:
90+
91+
```py
92+
@dataclass
93+
class AlsoWrong:
94+
x: InitVar # error: [invalid-type-form] "`InitVar` may not be used without a type argument"
95+
```
96+
97+
### Outside of dataclasses
98+
99+
`InitVar` annotations are not allowed outside of dataclass attribute annotations:
100+
101+
```py
102+
from dataclasses import InitVar, dataclass
103+
104+
# error: [invalid-type-form] "`InitVar` annotations are only allowed in class-body scopes"
105+
x: InitVar[int] = 1
106+
107+
def f(x: InitVar[int]) -> None: # error: [invalid-type-form] "`InitVar` is not allowed in function parameter annotations"
108+
pass
109+
110+
def g() -> InitVar[int]: # error: [invalid-type-form] "`InitVar` is not allowed in function return type annotations"
111+
return 1
112+
113+
class C:
114+
# TODO: this would ideally be an error
115+
x: InitVar[int]
116+
117+
@dataclass
118+
class D:
119+
def __init__(self) -> None:
120+
self.x: InitVar[int] = 1 # error: [invalid-type-form] "`InitVar` annotations are not allowed for non-name targets"
121+
```
122+
123+
[type annotation grammar]: https://typing.python.org/en/latest/spec/annotations.html#type-and-annotation-expressions
124+
[`dataclasses.initvar`]: https://docs.python.org/3/library/dataclasses.html#dataclasses.InitVar

crates/ty_python_semantic/src/module_resolver/module.rs

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -344,10 +344,6 @@ impl KnownModule {
344344
pub const fn is_importlib(self) -> bool {
345345
matches!(self, Self::ImportLib)
346346
}
347-
348-
pub const fn is_dataclasses(self) -> bool {
349-
matches!(self, Self::Dataclasses)
350-
}
351347
}
352348

353349
impl std::fmt::Display for KnownModule {

crates/ty_python_semantic/src/place.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -524,6 +524,11 @@ impl<'db> PlaceAndQualifiers<'db> {
524524
self.qualifiers.contains(TypeQualifiers::CLASS_VAR)
525525
}
526526

527+
/// Returns `true` if the place has a `InitVar` type qualifier.
528+
pub(crate) fn is_init_var(&self) -> bool {
529+
self.qualifiers.contains(TypeQualifiers::INIT_VAR)
530+
}
531+
527532
/// Returns `Some(…)` if the place is qualified with `typing.Final` without a specified type.
528533
pub(crate) fn is_bare_final(&self) -> Option<TypeQualifiers> {
529534
match self {

crates/ty_python_semantic/src/types.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5214,7 +5214,7 @@ impl<'db> Type<'db> {
52145214
})
52155215
}
52165216

5217-
SpecialFormType::ClassVar | SpecialFormType::Final | SpecialFormType::InitVar => {
5217+
SpecialFormType::ClassVar | SpecialFormType::Final => {
52185218
Err(InvalidTypeExpressionError {
52195219
invalid_expressions: smallvec::smallvec_inline![
52205220
InvalidTypeExpression::TypeQualifier(*special_form)

crates/ty_python_semantic/src/types/class.rs

Lines changed: 47 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -881,6 +881,13 @@ impl MethodDecorator {
881881
}
882882
}
883883

884+
#[derive(Debug, Clone, PartialEq, Eq)]
885+
pub(crate) struct DataclassField<'db> {
886+
pub(crate) field_ty: Type<'db>,
887+
pub(crate) default_ty: Option<Type<'db>>,
888+
pub(crate) init_only: bool,
889+
}
890+
884891
/// Representation of a class definition statement in the AST: either a non-generic class, or a
885892
/// generic class that has not been specialized.
886893
///
@@ -1473,6 +1480,10 @@ impl<'db> ClassLiteral<'db> {
14731480
}
14741481
}
14751482

1483+
if lookup_result.is_ok_and(|r| r.qualifiers.contains(TypeQualifiers::INIT_VAR)) {
1484+
return Place::Unbound.into();
1485+
}
1486+
14761487
match (
14771488
PlaceAndQualifiers::from(lookup_result),
14781489
dynamic_type_to_intersect_with,
@@ -1580,10 +1591,16 @@ impl<'db> ClassLiteral<'db> {
15801591

15811592
let signature_from_fields = |mut parameters: Vec<_>| {
15821593
let mut kw_only_field_seen = false;
1583-
for (name, (mut attr_ty, mut default_ty)) in
1584-
self.fields(db, specialization, field_policy)
1594+
for (
1595+
field_name,
1596+
DataclassField {
1597+
mut field_ty,
1598+
mut default_ty,
1599+
init_only: _,
1600+
},
1601+
) in self.fields(db, specialization, field_policy)
15851602
{
1586-
if attr_ty
1603+
if field_ty
15871604
.into_nominal_instance()
15881605
.is_some_and(|instance| instance.class.is_known(db, KnownClass::KwOnly))
15891606
{
@@ -1594,7 +1611,7 @@ impl<'db> ClassLiteral<'db> {
15941611
continue;
15951612
}
15961613

1597-
let dunder_set = attr_ty.class_member(db, "__set__".into());
1614+
let dunder_set = field_ty.class_member(db, "__set__".into());
15981615
if let Place::Type(dunder_set, Boundness::Bound) = dunder_set.place {
15991616
// The descriptor handling below is guarded by this not-dynamic check, because
16001617
// dynamic types like `Any` are valid (data) descriptors: since they have all
@@ -1623,7 +1640,7 @@ impl<'db> ClassLiteral<'db> {
16231640
}
16241641
}
16251642
}
1626-
attr_ty = value_types.build();
1643+
field_ty = value_types.build();
16271644

16281645
// The default value of the attribute is *not* determined by the right hand side
16291646
// of the class-body assignment. Instead, the runtime invokes `__get__` on the
@@ -1640,11 +1657,11 @@ impl<'db> ClassLiteral<'db> {
16401657
}
16411658

16421659
let mut parameter = if kw_only_field_seen {
1643-
Parameter::keyword_only(name)
1660+
Parameter::keyword_only(field_name)
16441661
} else {
1645-
Parameter::positional_or_keyword(name)
1662+
Parameter::positional_or_keyword(field_name)
16461663
}
1647-
.with_annotated_type(attr_ty);
1664+
.with_annotated_type(field_ty);
16481665

16491666
if let Some(default_ty) = default_ty {
16501667
parameter = parameter.with_default_type(default_ty);
@@ -1746,7 +1763,7 @@ impl<'db> ClassLiteral<'db> {
17461763
db: &'db dyn Db,
17471764
specialization: Option<Specialization<'db>>,
17481765
field_policy: CodeGeneratorKind,
1749-
) -> FxOrderMap<Name, (Type<'db>, Option<Type<'db>>)> {
1766+
) -> FxOrderMap<Name, DataclassField<'db>> {
17501767
if field_policy == CodeGeneratorKind::NamedTuple {
17511768
// NamedTuples do not allow multiple inheritance, so it is sufficient to enumerate the
17521769
// fields of this class only.
@@ -1793,7 +1810,7 @@ impl<'db> ClassLiteral<'db> {
17931810
self,
17941811
db: &'db dyn Db,
17951812
specialization: Option<Specialization<'db>>,
1796-
) -> FxOrderMap<Name, (Type<'db>, Option<Type<'db>>)> {
1813+
) -> FxOrderMap<Name, DataclassField<'db>> {
17971814
let mut attributes = FxOrderMap::default();
17981815

17991816
let class_body_scope = self.body_scope(db);
@@ -1835,11 +1852,12 @@ impl<'db> ClassLiteral<'db> {
18351852

18361853
attributes.insert(
18371854
place_expr.expect_name().clone(),
1838-
(
1839-
attr_ty.apply_optional_specialization(db, specialization),
1840-
default_ty
1855+
DataclassField {
1856+
field_ty: attr_ty.apply_optional_specialization(db, specialization),
1857+
default_ty: default_ty
18411858
.map(|ty| ty.apply_optional_specialization(db, specialization)),
1842-
),
1859+
init_only: attr.is_init_var(),
1860+
},
18431861
);
18441862
}
18451863
}
@@ -1876,6 +1894,10 @@ impl<'db> ClassLiteral<'db> {
18761894
qualifiers,
18771895
} = class.own_instance_member(db, name)
18781896
{
1897+
if qualifiers.contains(TypeQualifiers::INIT_VAR) {
1898+
continue;
1899+
}
1900+
18791901
// TODO: We could raise a diagnostic here if there are conflicting type qualifiers
18801902
union_qualifiers |= qualifiers;
18811903

@@ -2592,6 +2614,7 @@ pub enum KnownClass {
25922614
// dataclasses
25932615
Field,
25942616
KwOnly,
2617+
InitVar,
25952618
// _typeshed._type_checker_internals
25962619
NamedTupleFallback,
25972620
}
@@ -2686,6 +2709,7 @@ impl KnownClass {
26862709
| Self::Deprecated
26872710
| Self::Field
26882711
| Self::KwOnly
2712+
| Self::InitVar
26892713
| Self::NamedTupleFallback => Truthiness::Ambiguous,
26902714
}
26912715
}
@@ -2744,6 +2768,7 @@ impl KnownClass {
27442768
| Self::EllipsisType
27452769
| Self::NotImplementedType
27462770
| Self::KwOnly
2771+
| Self::InitVar
27472772
| Self::VersionInfo
27482773
| Self::Bool
27492774
| Self::NoneType => false,
@@ -2843,6 +2868,7 @@ impl KnownClass {
28432868
| KnownClass::NotImplementedType
28442869
| KnownClass::Field
28452870
| KnownClass::KwOnly
2871+
| KnownClass::InitVar
28462872
| KnownClass::NamedTupleFallback => false,
28472873
}
28482874
}
@@ -2925,6 +2951,7 @@ impl KnownClass {
29252951
| Self::UnionType
29262952
| Self::Field
29272953
| Self::KwOnly
2954+
| Self::InitVar
29282955
| Self::NamedTupleFallback => false,
29292956
}
29302957
}
@@ -3016,6 +3043,7 @@ impl KnownClass {
30163043
Self::NotImplementedType => "_NotImplementedType",
30173044
Self::Field => "Field",
30183045
Self::KwOnly => "KW_ONLY",
3046+
Self::InitVar => "InitVar",
30193047
Self::NamedTupleFallback => "NamedTupleFallback",
30203048
}
30213049
}
@@ -3269,8 +3297,7 @@ impl KnownClass {
32693297
| Self::DefaultDict
32703298
| Self::Deque
32713299
| Self::OrderedDict => KnownModule::Collections,
3272-
Self::Field => KnownModule::Dataclasses,
3273-
Self::KwOnly => KnownModule::Dataclasses,
3300+
Self::Field | Self::KwOnly | Self::InitVar => KnownModule::Dataclasses,
32743301
Self::NamedTupleFallback => KnownModule::TypeCheckerInternals,
32753302
}
32763303
}
@@ -3342,6 +3369,7 @@ impl KnownClass {
33423369
| Self::NewType
33433370
| Self::Field
33443371
| Self::KwOnly
3372+
| Self::InitVar
33453373
| Self::Iterable
33463374
| Self::Iterator
33473375
| Self::NamedTupleFallback => false,
@@ -3417,6 +3445,7 @@ impl KnownClass {
34173445
| Self::NewType
34183446
| Self::Field
34193447
| Self::KwOnly
3448+
| Self::InitVar
34203449
| Self::Iterable
34213450
| Self::Iterator
34223451
| Self::NamedTupleFallback => false,
@@ -3504,6 +3533,7 @@ impl KnownClass {
35043533
"_NotImplementedType" => Self::NotImplementedType,
35053534
"Field" => Self::Field,
35063535
"KW_ONLY" => Self::KwOnly,
3536+
"InitVar" => Self::InitVar,
35073537
"NamedTupleFallback" => Self::NamedTupleFallback,
35083538
_ => return None,
35093539
};
@@ -3566,6 +3596,7 @@ impl KnownClass {
35663596
| Self::WrapperDescriptorType
35673597
| Self::Field
35683598
| Self::KwOnly
3599+
| Self::InitVar
35693600
| Self::NamedTupleFallback => module == self.canonical_module(db),
35703601
Self::NoneType => matches!(module, KnownModule::Typeshed | KnownModule::Types),
35713602
Self::SpecialForm

crates/ty_python_semantic/src/types/class_base.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -192,8 +192,7 @@ impl<'db> ClassBase<'db> {
192192
| SpecialFormType::TypeOf
193193
| SpecialFormType::CallableTypeOf
194194
| SpecialFormType::AlwaysTruthy
195-
| SpecialFormType::AlwaysFalsy
196-
| SpecialFormType::InitVar => None,
195+
| SpecialFormType::AlwaysFalsy => None,
197196

198197
SpecialFormType::Unknown => Some(Self::unknown()),
199198

0 commit comments

Comments
 (0)