Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[red-knot] Add support for typing.ClassVar #15550

Merged
merged 27 commits into from
Jan 18, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
3aef9bc
[red-knot] Understand 'typing.ClassVar'
sharkdp Jan 17, 2025
c2c9069
Return QualifiedType from declaration_ty
sharkdp Jan 17, 2025
93735ec
Add diagnostic message
sharkdp Jan 17, 2025
672de46
Refactoring
sharkdp Jan 17, 2025
6b684ef
More simplifications
sharkdp Jan 17, 2025
5fab161
Support bare ClassVar
sharkdp Jan 17, 2025
e25f38c
Better error message
sharkdp Jan 17, 2025
308c33e
Test for ClassVar[Annotated[T, …]]
sharkdp Jan 17, 2025
3f2eace
Wording
sharkdp Jan 17, 2025
5ace6c6
Move TypeAndQualifiers
sharkdp Jan 17, 2025
7bf9ebb
Tuple struct
sharkdp Jan 17, 2025
8acb264
Conflicting type qualifiers
sharkdp Jan 17, 2025
202cd43
Add TODO
sharkdp Jan 17, 2025
712fdaf
Move diagnostic emitting code to proper place
sharkdp Jan 17, 2025
f056279
Use SymbolAndQualifiers in more places
sharkdp Jan 17, 2025
22715aa
Some simplifications around SymbolAndQualifiers<'db>
sharkdp Jan 17, 2025
3c1a670
Add doc comment
sharkdp Jan 17, 2025
cc2f5f8
Special form => Type qualifier
sharkdp Jan 17, 2025
b8a8590
Move is_class_var method
sharkdp Jan 17, 2025
d99015b
Only return qualifiers from infer_annotation_expression
sharkdp Jan 17, 2025
37e9eaf
Minor changes
sharkdp Jan 17, 2025
cbf384f
Add partial support for Final
sharkdp Jan 17, 2025
827abf6
Update doc comment
sharkdp Jan 18, 2025
e15ab5e
report_invalid_arguments_to_annotated as a free function
sharkdp Jan 18, 2025
02f4959
pure class variable => ClassVar
sharkdp Jan 18, 2025
cf49085
inner_type => inner_ty
sharkdp Jan 18, 2025
c915773
Remove superfluous match on ExprContext
sharkdp Jan 18, 2025
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
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"
sharkdp marked this conversation as resolved.
Show resolved Hide resolved

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
sharkdp marked this conversation as resolved.
Show resolved Hide resolved
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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe this should be a diagnostic? But it isn't in pyright (which has the same behavior you implemented here).

It is in mypy, but for different reasons than it would be for us, since mypy just generally prohibits re-declaration.

Not sure, don't feel clear enough about it to even suggest a TODO comment. We can always add this diagnostic if we decide we need it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The "currently" was my way to try and describe that this is probably an open design decision. It feels to me like a not very common edge case, so in spirit of "every lint rule generates maintenance work", I'll leave this as-is for now, but it should be straightforward to add a lint for this later if we want to.

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
Loading