Skip to content

Commit

Permalink
[red-knot] Add support for typing.ClassVar (#15550)
Browse files Browse the repository at this point in the history
## Summary

Add support for `typing.ClassVar`, i.e. emit a diagnostic in this
scenario:
```py
from typing import ClassVar

class C:
    x: ClassVar[int] = 1

c = C()
c.x = 3  # error: "Cannot assign to pure class variable `x` from an instance of type `C`"
```

## Test Plan

- New tests for the `typing.ClassVar` qualifier
- Fixed one TODO in `attributes.md`
  • Loading branch information
sharkdp authored Jan 18, 2025
1 parent 9730ff3 commit fb15da5
Show file tree
Hide file tree
Showing 5 changed files with 499 additions and 114 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,10 @@ class C:
pure_class_variable1: ClassVar[str] = "value in class body"
pure_class_variable2: ClassVar = 1

def method(self):
# TODO: this should be an error
self.pure_class_variable1 = "value set through instance"

reveal_type(C.pure_class_variable1) # revealed: str

# TODO: this should be `Literal[1]`, or `Unknown | Literal[1]`.
Expand All @@ -182,7 +186,7 @@ reveal_type(c_instance.pure_class_variable1) # revealed: str
# TODO: Should be `Unknown | Literal[1]`.
reveal_type(c_instance.pure_class_variable2) # revealed: Unknown

# TODO: should raise an error. It is not allowed to reassign a pure class variable on an instance.
# error: [invalid-attribute-access] "Cannot assign to ClassVar `pure_class_variable1` from an instance of type `C`"
c_instance.pure_class_variable1 = "value set on instance"

C.pure_class_variable1 = "overwritten on class"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# `typing.ClassVar`

[`typing.ClassVar`] is a type qualifier that is used to indicate that a class variable may not be
written to from instances of that class.

This test makes sure that we discover the type qualifier while inferring types from an annotation.
For more details on the semantics of pure class variables, see [this test](../attributes.md).

## Basic

```py
from typing import ClassVar, Annotated

class C:
a: ClassVar[int] = 1
b: Annotated[ClassVar[int], "the annotation for b"] = 1
c: ClassVar[Annotated[int, "the annotation for c"]] = 1
d: ClassVar = 1
e: "ClassVar[int]" = 1

reveal_type(C.a) # revealed: int
reveal_type(C.b) # revealed: int
reveal_type(C.c) # revealed: int
# TODO: should be Unknown | Literal[1]
reveal_type(C.d) # revealed: Unknown
reveal_type(C.e) # revealed: int

c = C()

# error: [invalid-attribute-access]
c.a = 2
# error: [invalid-attribute-access]
c.b = 2
# error: [invalid-attribute-access]
c.c = 2
# error: [invalid-attribute-access]
c.d = 2
# error: [invalid-attribute-access]
c.e = 2
```

## Conflicting type qualifiers

We currently ignore conflicting qualifiers and simply union them, which is more conservative than
intersecting them. This means that we consider `a` to be a `ClassVar` here:

```py
from typing import ClassVar

def flag() -> bool:
return True

class C:
if flag():
a: ClassVar[int] = 1
else:
a: str

reveal_type(C.a) # revealed: int | str

c = C()

# error: [invalid-attribute-access]
c.a = 2
```

## Too many arguments

```py
class C:
# error: [invalid-type-form] "Type qualifier `typing.ClassVar` expects exactly one type parameter"
x: ClassVar[int, str] = 1
```

## Illegal `ClassVar` in type expression

```py
class C:
# error: [invalid-type-form] "Type qualifier `typing.ClassVar` is not allowed in type expressions (only in annotation expressions)"
x: ClassVar | int

# error: [invalid-type-form] "Type qualifier `typing.ClassVar` is not allowed in type expressions (only in annotation expressions)"
y: int | ClassVar[str]
```

## Used outside of a class

```py
# TODO: this should be an error
x: ClassVar[int] = 1
```

[`typing.classvar`]: https://docs.python.org/3/library/typing.html#typing.ClassVar
Loading

0 comments on commit fb15da5

Please sign in to comment.