diff --git a/crates/ty_python_semantic/resources/mdtest/attributes.md b/crates/ty_python_semantic/resources/mdtest/attributes.md index 19a05d27a5..7346db6933 100644 --- a/crates/ty_python_semantic/resources/mdtest/attributes.md +++ b/crates/ty_python_semantic/resources/mdtest/attributes.md @@ -689,16 +689,14 @@ class C: reveal_type(C.pure_class_variable1) # revealed: str -# TODO: Should be `Unknown | Literal[1]`. -reveal_type(C.pure_class_variable2) # revealed: Unknown +reveal_type(C.pure_class_variable2) # revealed: Unknown | Literal[1] c_instance = C() # It is okay to access a pure class variable on an instance. reveal_type(c_instance.pure_class_variable1) # revealed: str -# TODO: Should be `Unknown | Literal[1]`. -reveal_type(c_instance.pure_class_variable2) # revealed: Unknown +reveal_type(c_instance.pure_class_variable2) # revealed: Unknown | Literal[1] # 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" @@ -714,6 +712,24 @@ class Subclass(C): reveal_type(Subclass.pure_class_variable1) # revealed: str ``` +If a class variable is additionally qualified as `Final`, we do not union with `Unknown` for bare +`ClassVar`s: + +```py +from typing import Final + +class D: + final1: Final[ClassVar] = 1 + final2: ClassVar[Final] = 1 + final3: ClassVar[Final[int]] = 1 + final4: Final[ClassVar[int]] = 1 + +reveal_type(D.final1) # revealed: Literal[1] +reveal_type(D.final2) # revealed: Literal[1] +reveal_type(D.final3) # revealed: int +reveal_type(D.final4) # revealed: int +``` + #### Variable only mentioned in a class method We also consider a class variable to be a pure class variable if it is only mentioned in a class diff --git a/crates/ty_python_semantic/resources/mdtest/type_qualifiers/classvar.md b/crates/ty_python_semantic/resources/mdtest/type_qualifiers/classvar.md index 52b117ab5b..4e2135bd3c 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_qualifiers/classvar.md +++ b/crates/ty_python_semantic/resources/mdtest/type_qualifiers/classvar.md @@ -21,8 +21,7 @@ class C: 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.d) # revealed: Unknown | Literal[1] reveal_type(C.e) # revealed: int c = C() diff --git a/crates/ty_python_semantic/src/place.rs b/crates/ty_python_semantic/src/place.rs index dcb6300221..0c1901b591 100644 --- a/crates/ty_python_semantic/src/place.rs +++ b/crates/ty_python_semantic/src/place.rs @@ -9,8 +9,8 @@ use crate::semantic_index::{ }; use crate::semantic_index::{DeclarationWithConstraint, global_scope, use_def_map}; use crate::types::{ - KnownClass, Truthiness, Type, TypeAndQualifiers, TypeQualifiers, UnionBuilder, UnionType, - binding_type, declaration_type, todo_type, + DynamicType, KnownClass, Truthiness, Type, TypeAndQualifiers, TypeQualifiers, UnionBuilder, + UnionType, binding_type, declaration_type, todo_type, }; use crate::{Db, FxOrderSet, KnownModule, Program, resolve_module}; @@ -672,6 +672,30 @@ fn place_by_id<'db>( .with_qualifiers(qualifiers); } + // Handle bare `ClassVar` annotations by falling back to the union of `Unknown` and the + // inferred type. + match declared { + Ok(PlaceAndQualifiers { + place: Place::Type(Type::Dynamic(DynamicType::Unknown), declaredness), + qualifiers, + }) if qualifiers.contains(TypeQualifiers::CLASS_VAR) => { + let bindings = all_considered_bindings(); + match place_from_bindings_impl(db, bindings, requires_explicit_reexport) { + Place::Type(inferred, boundness) => { + return Place::Type( + UnionType::from_elements(db, [Type::unknown(), inferred]), + boundness, + ) + .with_qualifiers(qualifiers); + } + Place::Unbound => { + return Place::Type(Type::unknown(), declaredness).with_qualifiers(qualifiers); + } + } + } + _ => {} + } + match declared { // Place is declared, trust the declared type Ok(