mirror of https://github.com/astral-sh/ruff
[ty] Unwrap `enum.nonmember` values (#22025)
## Summary This PR unwraps the `enum.nonmember` type to match runtime behavior. Closes https://github.com/astral-sh/ty/issues/1974.
This commit is contained in:
parent
5a2d3cda3d
commit
76854fdb15
|
|
@ -451,6 +451,10 @@ class Answer(Enum):
|
||||||
|
|
||||||
# revealed: tuple[Literal["YES"], Literal["NO"]]
|
# revealed: tuple[Literal["YES"], Literal["NO"]]
|
||||||
reveal_type(enum_members(Answer))
|
reveal_type(enum_members(Answer))
|
||||||
|
|
||||||
|
# `nonmember` attributes are unwrapped to the inner value type when accessed.
|
||||||
|
# revealed: int
|
||||||
|
reveal_type(Answer.OTHER)
|
||||||
```
|
```
|
||||||
|
|
||||||
`member` can also be used as a decorator:
|
`member` can also be used as a decorator:
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,9 @@ use crate::types::bound_super::BoundSuperError;
|
||||||
use crate::types::constraints::{ConstraintSet, IteratorConstraintsExtension};
|
use crate::types::constraints::{ConstraintSet, IteratorConstraintsExtension};
|
||||||
use crate::types::context::InferContext;
|
use crate::types::context::InferContext;
|
||||||
use crate::types::diagnostic::{INVALID_TYPE_ALIAS_TYPE, SUPER_CALL_IN_NAMED_TUPLE_METHOD};
|
use crate::types::diagnostic::{INVALID_TYPE_ALIAS_TYPE, SUPER_CALL_IN_NAMED_TUPLE_METHOD};
|
||||||
use crate::types::enums::enum_metadata;
|
use crate::types::enums::{
|
||||||
|
enum_metadata, is_enum_class_by_inheritance, try_unwrap_nonmember_value,
|
||||||
|
};
|
||||||
use crate::types::function::{
|
use crate::types::function::{
|
||||||
DataclassTransformerFlags, DataclassTransformerParams, KnownFunction,
|
DataclassTransformerFlags, DataclassTransformerParams, KnownFunction,
|
||||||
};
|
};
|
||||||
|
|
@ -2355,6 +2357,18 @@ impl<'db> ClassLiteral<'db> {
|
||||||
// The symbol was not found in the class scope. It might still be implicitly defined in `@classmethod`s.
|
// The symbol was not found in the class scope. It might still be implicitly defined in `@classmethod`s.
|
||||||
return Self::implicit_attribute(db, body_scope, name, MethodDecorator::ClassMethod);
|
return Self::implicit_attribute(db, body_scope, name, MethodDecorator::ClassMethod);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// For enum classes, `nonmember(value)` creates a non-member attribute.
|
||||||
|
// At runtime, the enum metaclass unwraps the value, so accessing the attribute
|
||||||
|
// returns the inner value, not the `nonmember` wrapper.
|
||||||
|
if let Some(ty) = member.inner.place.ignore_possibly_undefined() {
|
||||||
|
if let Some(value_ty) = try_unwrap_nonmember_value(db, ty) {
|
||||||
|
if is_enum_class_by_inheritance(db, self) {
|
||||||
|
return Member::definitely_declared(value_ty);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
member
|
member
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -60,11 +60,7 @@ pub(crate) fn enum_metadata<'db>(
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
if !Type::ClassLiteral(class).is_subtype_of(db, KnownClass::Enum.to_subclass_of(db))
|
if !is_enum_class_by_inheritance(db, class) {
|
||||||
&& !class
|
|
||||||
.metaclass(db)
|
|
||||||
.is_subtype_of(db, KnownClass::EnumType.to_subclass_of(db))
|
|
||||||
{
|
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -294,3 +290,63 @@ pub(crate) fn is_enum_class<'db>(db: &'db dyn Db, ty: Type<'db>) -> bool {
|
||||||
_ => false,
|
_ => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Checks if a class is an enum class by inheritance (either a subtype of `Enum`
|
||||||
|
/// or has a metaclass that is a subtype of `EnumType`).
|
||||||
|
///
|
||||||
|
/// This is a lighter-weight check than `enum_metadata`, which additionally
|
||||||
|
/// verifies that the class has members.
|
||||||
|
pub(crate) fn is_enum_class_by_inheritance<'db>(db: &'db dyn Db, class: ClassLiteral<'db>) -> bool {
|
||||||
|
Type::ClassLiteral(class).is_subtype_of(db, KnownClass::Enum.to_subclass_of(db))
|
||||||
|
|| class
|
||||||
|
.metaclass(db)
|
||||||
|
.is_subtype_of(db, KnownClass::EnumType.to_subclass_of(db))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extracts the inner value type from an `enum.nonmember()` wrapper.
|
||||||
|
///
|
||||||
|
/// At runtime, the enum metaclass unwraps `nonmember(value)`, so accessing the attribute
|
||||||
|
/// returns the inner value, not the `nonmember` wrapper.
|
||||||
|
///
|
||||||
|
/// Returns `Some(value_type)` if the type is a `nonmember[T]`, otherwise `None`.
|
||||||
|
pub(crate) fn try_unwrap_nonmember_value<'db>(db: &'db dyn Db, ty: Type<'db>) -> Option<Type<'db>> {
|
||||||
|
match ty {
|
||||||
|
Type::NominalInstance(instance) if instance.has_known_class(db, KnownClass::Nonmember) => {
|
||||||
|
Some(
|
||||||
|
ty.member(db, "value")
|
||||||
|
.place
|
||||||
|
.ignore_possibly_undefined()
|
||||||
|
.unwrap_or(Type::unknown()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Type::Union(union) => {
|
||||||
|
// TODO: This is a hack. The proper fix is to avoid unioning Unknown from
|
||||||
|
// declarations into Place when we have concrete bindings.
|
||||||
|
//
|
||||||
|
// For now, we filter out Unknown and expect exactly one nonmember type
|
||||||
|
// to remain. If there are other non-Unknown types mixed in, we bail out.
|
||||||
|
let mut non_unknown = union.elements(db).iter().filter(|elem| !elem.is_unknown());
|
||||||
|
|
||||||
|
let first = non_unknown.next()?;
|
||||||
|
|
||||||
|
// Ensure there's exactly one non-Unknown element.
|
||||||
|
if non_unknown.next().is_some() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Type::NominalInstance(instance) = first {
|
||||||
|
if instance.has_known_class(db, KnownClass::Nonmember) {
|
||||||
|
return Some(
|
||||||
|
first
|
||||||
|
.member(db, "value")
|
||||||
|
.place
|
||||||
|
.ignore_possibly_undefined()
|
||||||
|
.unwrap_or(Type::unknown()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue