diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index d1688b1c56a66..32d26ea075f88 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -28,7 +28,7 @@ use crate::symbol::{Boundness, Symbol}; use crate::types::diagnostic::TypeCheckDiagnosticsBuilder; use crate::types::mro::{ClassBase, Mro, MroError, MroIterator}; use crate::types::narrow::narrowing_constraint; -use crate::{Db, FxOrderSet, Module, Program}; +use crate::{Db, FxOrderSet, Module, Program, PythonVersion}; mod builder; mod diagnostic; @@ -1897,13 +1897,13 @@ impl<'db> KnownClass { } pub fn to_class_literal(self, db: &'db dyn Db) -> Type<'db> { - core_module_symbol(db, self.canonical_module(), self.as_str()) + core_module_symbol(db, self.canonical_module(db), self.as_str()) .ignore_possibly_unbound() .unwrap_or(Type::Unknown) } /// Return the module in which we should look up the definition for this class - pub(crate) const fn canonical_module(self) -> CoreStdlibModule { + pub(crate) fn canonical_module(self, db: &'db dyn Db) -> CoreStdlibModule { match self { Self::Bool | Self::Object @@ -1921,10 +1921,18 @@ impl<'db> KnownClass { Self::GenericAlias | Self::ModuleType | Self::FunctionType => CoreStdlibModule::Types, Self::NoneType => CoreStdlibModule::Typeshed, Self::SpecialForm | Self::TypeVar | Self::TypeAliasType => CoreStdlibModule::Typing, - // TODO when we understand sys.version_info, we will need an explicit fallback here, - // because typing_extensions has a 3.13+ re-export for the `typing.NoDefault` - // singleton, but not for `typing._NoDefaultType` - Self::NoDefaultType => CoreStdlibModule::TypingExtensions, + Self::NoDefaultType => { + let python_version = Program::get(db).target_version(db); + + // typing_extensions has a 3.13+ re-export for the `typing.NoDefault` + // singleton, but not for `typing._NoDefaultType`. So we need to switch + // to `typing._NoDefaultType` for newer versions: + if python_version >= PythonVersion::PY313 { + CoreStdlibModule::Typing + } else { + CoreStdlibModule::TypingExtensions + } + } } } @@ -1984,11 +1992,11 @@ impl<'db> KnownClass { }; let module = file_to_module(db, file)?; - candidate.check_module(&module).then_some(candidate) + candidate.check_module(db, &module).then_some(candidate) } /// Return `true` if the module of `self` matches `module_name` - fn check_module(self, module: &Module) -> bool { + fn check_module(self, db: &'db dyn Db, module: &Module) -> bool { if !module.search_path().is_standard_library() { return false; } @@ -2008,7 +2016,7 @@ impl<'db> KnownClass { | Self::GenericAlias | Self::ModuleType | Self::VersionInfo - | Self::FunctionType => module.name() == self.canonical_module().as_str(), + | Self::FunctionType => module.name() == self.canonical_module(db).as_str(), Self::NoneType => matches!(module.name().as_str(), "_typeshed" | "types"), Self::SpecialForm | Self::TypeVar | Self::TypeAliasType | Self::NoDefaultType => { matches!(module.name().as_str(), "typing" | "typing_extensions") @@ -3683,13 +3691,28 @@ pub(crate) mod tests { #[test_case(Ty::None)] #[test_case(Ty::BooleanLiteral(true))] #[test_case(Ty::BooleanLiteral(false))] - #[test_case(Ty::KnownClassInstance(KnownClass::NoDefaultType))] fn is_singleton(from: Ty) { let db = setup_db(); assert!(from.into_type(&db).is_singleton(&db)); } + /// Explicitly test for Python version <3.13 and >=3.13, to ensure that + /// the fallback to `typing_extensions` is working correctly. + /// See [`KnownClass::canonical_module`] for more information. + #[test_case(PythonVersion::PY312)] + #[test_case(PythonVersion::PY313)] + fn no_default_type_is_singleton(python_version: PythonVersion) { + let db = TestDbBuilder::new() + .with_python_version(python_version) + .build() + .unwrap(); + + let no_default = Ty::KnownClassInstance(KnownClass::NoDefaultType).into_type(&db); + + assert!(no_default.is_singleton(&db)); + } + #[test_case(Ty::None)] #[test_case(Ty::BooleanLiteral(true))] #[test_case(Ty::IntLiteral(1))]