Skip to content
Open
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
19 changes: 8 additions & 11 deletions pyrefly/lib/alt/class/class_field.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1817,22 +1817,20 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> {

// Identify whether this is a descriptor
let mut descriptor = None;
// Descriptor semantics apply when:
// 1. The field is initialized in the class body (class-level attribute), or
// 2. The field is annotated with ClassVar (explicitly class-level, even without initialization), or
// 3. The field's type is special-case to always be treated like a descriptor.
let is_classvar = direct_annotation
.as_ref()
.is_some_and(|annot| annot.has_qualifier(&Qualifier::ClassVar));
// Descriptor semantics apply when the field is modeled as class-level:
// either by a class-body definition, or by `Magic` for stub/interface
// declarations where the runtime initializer is omitted. Some types are
// also always treated like descriptors.
let is_special_descriptor_type = direct_annotation.as_ref().is_some_and(|annot| {
annot
.ty
.as_ref()
.is_some_and(|ty| self.is_special_descriptor_type(ty))
});
if matches!(initialization, ClassFieldInitialization::ClassBody(_))
|| is_classvar
|| is_special_descriptor_type
if matches!(
initialization,
ClassFieldInitialization::ClassBody(_) | ClassFieldInitialization::Magic
) || is_special_descriptor_type
{
match &ty {
// TODO(stroxler): This works for simple descriptors. There are known gaps:
Expand Down Expand Up @@ -1910,7 +1908,6 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> {
} else if ty.is_property_getter() || ty.is_property_setter_with_getter().is_some() {
ClassField(ClassFieldInner::Property { ty, is_abstract }, is_inherited)
} else if let Some(descriptor) = descriptor {
// Descriptors are always initialized in class body (or wouldn't trigger descriptor protocol)
ClassField(
ClassFieldInner::Descriptor {
ty,
Expand Down
44 changes: 44 additions & 0 deletions pyrefly/lib/test/descriptors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -650,6 +650,33 @@ from .decl_api import DeclarativeBase as DeclarativeBase
env
}

fn stub_descriptor_env() -> TestEnv {
let mut env = TestEnv::new();
env.add_with_path(
"pkg.styleable",
"pkg/styleable.pyi",
r#"
class Descriptor:
def __get__(self, obj: object, owner: object) -> str: ...
def __set__(self, obj: object, value: str) -> None: ...

class StyleableObject:
style: Descriptor
"#,
);
env.add_with_path(
"pkg.cell",
"pkg/cell.pyi",
r#"
from .styleable import StyleableObject

class Cell(StyleableObject): ...
"#,
);
env.add_with_path("pkg", "pkg/__init__.pyi", "");
env
}

testcase!(
test_sqlalchemy_mapped_is_always_descriptor,
sqlalchemy_mapped_env(),
Expand All @@ -664,6 +691,23 @@ class User(Base):
"#,
);

testcase!(
test_stub_annotation_only_descriptor_has_descriptor_semantics,
stub_descriptor_env(),
r#"
from typing import assert_type

from pkg.cell import Cell
from pkg.styleable import StyleableObject

c: Cell = None # type: ignore
s: StyleableObject = None # type: ignore

assert_type(c.style, str)
assert_type(s.style, str)
"#,
);

testcase!(
test_overloaded_descriptor_get_with_bounded_typevar,
r#"
Expand Down
Loading