diff --git a/crates/ty_python_semantic/resources/mdtest/type_properties/is_equivalent_to.md b/crates/ty_python_semantic/resources/mdtest/type_properties/is_equivalent_to.md index 678d1c1a11..83579a0e06 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_properties/is_equivalent_to.md +++ b/crates/ty_python_semantic/resources/mdtest/type_properties/is_equivalent_to.md @@ -13,7 +13,7 @@ materializations of `B`, and all materializations of `B` are also materializatio ### Fully static ```py -from typing_extensions import Literal, LiteralString, Never +from typing_extensions import Literal, LiteralString, Protocol, Never from ty_extensions import Unknown, is_equivalent_to, static_assert, TypeOf, AlwaysTruthy, AlwaysFalsy from enum import Enum @@ -43,6 +43,16 @@ static_assert(is_equivalent_to(Literal[Single.VALUE], Single)) static_assert(is_equivalent_to(Single, Literal[Single.VALUE])) static_assert(is_equivalent_to(Literal[Single.VALUE], Literal[Single.VALUE])) +static_assert(is_equivalent_to(tuple[Single] | int | str, str | int | tuple[Literal[Single.VALUE]])) + +class Protocol1(Protocol): + a: Single + +class Protocol2(Protocol): + a: Literal[Single.VALUE] + +static_assert(is_equivalent_to(Protocol1, Protocol2)) + static_assert(is_equivalent_to(Never, Never)) static_assert(is_equivalent_to(AlwaysTruthy, AlwaysTruthy)) static_assert(is_equivalent_to(AlwaysFalsy, AlwaysFalsy)) diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 8a01a5440b..9a0d9c86f8 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -1062,6 +1062,12 @@ impl<'db> Type<'db> { type_is.with_type(db, type_is.return_type(db).normalized_impl(db, v)) }), Type::Dynamic(dynamic) => Type::Dynamic(dynamic.normalized()), + Type::EnumLiteral(enum_literal) + if is_single_member_enum(db, enum_literal.enum_class(db)) => + { + // Always normalize single-member enums to their class instance (`Literal[Single.VALUE]` => `Single`) + enum_literal.enum_class_instance(db) + } Type::LiteralString | Type::AlwaysFalsy | Type::AlwaysTruthy diff --git a/crates/ty_python_semantic/src/types/property_tests/type_generation.rs b/crates/ty_python_semantic/src/types/property_tests/type_generation.rs index d0d7097af3..59c9a449b7 100644 --- a/crates/ty_python_semantic/src/types/property_tests/type_generation.rs +++ b/crates/ty_python_semantic/src/types/property_tests/type_generation.rs @@ -1,5 +1,6 @@ use crate::db::tests::TestDb; use crate::place::{builtins_symbol, known_module_symbol}; +use crate::types::enums::is_single_member_enum; use crate::types::tuple::TupleType; use crate::types::{ BoundMethodType, CallableType, EnumLiteralType, IntersectionBuilder, KnownClass, Parameter, @@ -27,6 +28,8 @@ pub(crate) enum Ty { BytesLiteral(&'static str), // An enum literal variant, using `uuid.SafeUUID` as base EnumLiteral(&'static str), + // A single-member enum literal, using `dataclasses.MISSING` + SingleMemberEnumLiteral, // BuiltinInstance("str") corresponds to an instance of the builtin `str` class BuiltinInstance(&'static str), /// Members of the `abc` stdlib module @@ -145,6 +148,15 @@ impl Ty { .expect_class_literal(), Name::new(name), )), + Ty::SingleMemberEnumLiteral => { + let ty = known_module_symbol(db, KnownModule::Dataclasses, "MISSING") + .place + .expect_type(); + debug_assert!( + matches!(ty, Type::NominalInstance(instance) if is_single_member_enum(db, instance.class.class_literal(db).0)) + ); + ty + } Ty::BuiltinInstance(s) => builtins_symbol(db, s) .place .expect_type() @@ -265,6 +277,7 @@ fn arbitrary_core_type(g: &mut Gen, fully_static: bool) -> Ty { Ty::EnumLiteral("safe"), Ty::EnumLiteral("unsafe"), Ty::EnumLiteral("unknown"), + Ty::SingleMemberEnumLiteral, Ty::KnownClassInstance(KnownClass::Object), Ty::KnownClassInstance(KnownClass::Str), Ty::KnownClassInstance(KnownClass::Int),