Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
# `dataclasses.InitVar`

From the Python documentation on [`dataclasses.InitVar`]:

If a field is an `InitVar`, it is considered a pseudo-field called an init-only field. As it is not
a true field, it is not returned by the module-level `fields()` function. Init-only fields are added
as parameters to the generated `__init__()` method, and are passed to the optional `__post_init__()`
method. They are not otherwise used by dataclasses.

## Basic

Consider the following dataclass example where the `db` attribute is annotated with `InitVar`:

```py
from dataclasses import InitVar, dataclass

class Database: ...

@dataclass(order=True)
class Person:
db: InitVar[Database]

name: str
age: int
```

We can see in the signature of `__init__` that `db` is included as an argument:

```py
reveal_type(Person.__init__) # revealed: (self: Person, db: Database, name: str, age: int) -> None
```

However, when we create an instance of this dataclass, the `db` attribute is not accessible:

```py
db = Database()
alice = Person(db, "Alice", 30)

alice.db # error: [unresolved-attribute]
```

The `db` attribute is also not accessible on the class itself:

```py
Person.db # error: [unresolved-attribute]
```

Other fields can still be accessed normally:

```py
reveal_type(alice.name) # revealed: str
reveal_type(alice.age) # revealed: int
```

## `InitVar` with default value

An `InitVar` can also have a default value. In this case, the attribute *is* accessible on the class
and on instances:

```py
from dataclasses import InitVar, dataclass

@dataclass
class Person:
name: str
age: int

metadata: InitVar[str] = "default"

reveal_type(Person.__init__) # revealed: (self: Person, name: str, age: int, metadata: str = Literal["default"]) -> None

alice = Person("Alice", 30)
bob = Person("Bob", 25, "custom metadata")

reveal_type(bob.metadata) # revealed: str

reveal_type(Person.metadata) # revealed: str
```

## Overwritten `InitVar`

We do not emit an error if an `InitVar` attribute is later overwritten on the instance. In that
case, we also allow the attribute to be accessed:

```py
from dataclasses import InitVar, dataclass

@dataclass
class Person:
name: str
metadata: InitVar[str]

def __post_init__(self, metadata: str) -> None:
self.metadata = f"Person with name {self.name}"

alice = Person("Alice", "metadata that will be overwritten")

reveal_type(alice.metadata) # revealed: str
```

## Error cases

### Syntax

`InitVar` can only be used with a single argument:

```py
from dataclasses import InitVar, dataclass

@dataclass
class Wrong:
x: InitVar[int, str] # error: [invalid-type-form] "Type qualifier `InitVar` expected exactly 1 argument, got 2"
```

A bare `InitVar` is not allowed according to the [type annotation grammar]:

```py
@dataclass
class AlsoWrong:
x: InitVar # error: [invalid-type-form] "`InitVar` may not be used without a type argument"
```

### Outside of dataclasses

`InitVar` annotations are not allowed outside of dataclass attribute annotations:

```py
from dataclasses import InitVar, dataclass

# error: [invalid-type-form] "`InitVar` annotations are only allowed in class-body scopes"
x: InitVar[int] = 1

def f(x: InitVar[int]) -> None: # error: [invalid-type-form] "`InitVar` is not allowed in function parameter annotations"
pass

def g() -> InitVar[int]: # error: [invalid-type-form] "`InitVar` is not allowed in function return type annotations"
return 1

class C:
# TODO: this would ideally be an error
x: InitVar[int]

@dataclass
class D:
def __init__(self) -> None:
self.x: InitVar[int] = 1 # error: [invalid-type-form] "`InitVar` annotations are not allowed for non-name targets"
```

[type annotation grammar]: https://typing.python.org/en/latest/spec/annotations.html#type-and-annotation-expressions
[`dataclasses.initvar`]: https://docs.python.org/3/library/dataclasses.html#dataclasses.InitVar
7 changes: 6 additions & 1 deletion crates/ty_python_semantic/src/place.rs
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,7 @@ pub(crate) fn class_symbol<'db>(
ConsideredDefinitions::EndOfScope,
);

if !place_and_quals.place.is_unbound() {
if !place_and_quals.place.is_unbound() && !place_and_quals.is_init_var() {
// Trust the declared type if we see a class-level declaration
return place_and_quals;
}
Expand Down Expand Up @@ -524,6 +524,11 @@ impl<'db> PlaceAndQualifiers<'db> {
self.qualifiers.contains(TypeQualifiers::CLASS_VAR)
}

/// Returns `true` if the place has a `InitVar` type qualifier.
pub(crate) fn is_init_var(&self) -> bool {
self.qualifiers.contains(TypeQualifiers::INIT_VAR)
}

/// Returns `Some(…)` if the place is qualified with `typing.Final` without a specified type.
pub(crate) fn is_bare_final(&self) -> Option<TypeQualifiers> {
match self {
Expand Down
18 changes: 18 additions & 0 deletions crates/ty_python_semantic/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6137,11 +6137,29 @@ bitflags! {
const CLASS_VAR = 1 << 0;
/// `typing.Final`
const FINAL = 1 << 1;
/// `dataclasses.InitVar`
const INIT_VAR = 1 << 2;
}
}

impl get_size2::GetSize for TypeQualifiers {}

impl TypeQualifiers {
/// Get the name of a qualifier. Note that this only works
///
/// Panics if more than a single bit is set.
fn name(self) -> &'static str {
match self {
Self::CLASS_VAR => "ClassVar",
Self::FINAL => "Final",
Self::INIT_VAR => "InitVar",
_ => {
unreachable!("Only a single bit should be set when calling `TypeQualifiers::name`")
}
}
}
}

/// When inferring the type of an annotation expression, we can also encounter type qualifiers
/// such as `ClassVar` or `Final`. These do not affect the inferred type itself, but rather
/// control how a particular place can be accessed or modified. This struct holds a type and
Expand Down
Loading
Loading