-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[red-knot] Add support for
typing.ClassVar
(#15550)
## 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
Showing
5 changed files
with
499 additions
and
114 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
93 changes: 93 additions & 0 deletions
93
crates/red_knot_python_semantic/resources/mdtest/type_qualifiers/classvar.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
Oops, something went wrong.