From 4941975e744309972a03f7ee5a9a7b9083362c3c Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Sun, 16 Feb 2025 22:01:02 +0000 Subject: [PATCH] [red-knot] Recognize `...` as a singleton (#16184) --- .../mdtest/narrow/conditionals/is.md | 36 +++++++++++++++ .../mdtest/type_properties/is_singleton.md | 38 ++++++++++++++++ crates/red_knot_python_semantic/src/types.rs | 44 ++++++++++++++++--- .../src/types/infer.rs | 14 +++--- 4 files changed, 118 insertions(+), 14 deletions(-) diff --git a/crates/red_knot_python_semantic/resources/mdtest/narrow/conditionals/is.md b/crates/red_knot_python_semantic/resources/mdtest/narrow/conditionals/is.md index f5d430f7d1abb..8a95bfc278f81 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/narrow/conditionals/is.md +++ b/crates/red_knot_python_semantic/resources/mdtest/narrow/conditionals/is.md @@ -64,3 +64,39 @@ def _(flag1: bool, flag2: bool): else: reveal_type(x) # revealed: Literal[1] ``` + +## `is` for `EllipsisType` (Python 3.10+) + +```toml +[environment] +python-version = "3.10" +``` + +```py +from types import EllipsisType + +def _(x: int | EllipsisType): + if x is ...: + reveal_type(x) # revealed: EllipsisType + else: + reveal_type(x) # revealed: int +``` + +## `is` for `EllipsisType` (Python 3.9 and below) + +```toml +[environment] +python-version = "3.9" +``` + +```py +def _(flag: bool): + x = ... if flag else 42 + + reveal_type(x) # revealed: ellipsis | Literal[42] + + if x is ...: + reveal_type(x) # revealed: ellipsis + else: + reveal_type(x) # revealed: Literal[42] +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_singleton.md b/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_singleton.md index a3e1ed9a969fd..cb709bfb1e7e3 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_singleton.md +++ b/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_singleton.md @@ -54,3 +54,41 @@ from knot_extensions import is_singleton, static_assert static_assert(is_singleton(_NoDefaultType)) ``` + +## `builtins.ellipsis`/`types.EllipsisType` + +### All Python versions + +The type of the builtin symbol `Ellipsis` is the same as the type of an ellipsis literal (`...`). +The type is not actually exposed from the standard library on Python \<3.10, but we still recognise +the type as a singleton on any Python version. + +```toml +[environment] +python-version = "3.9" +``` + +```py +import sys +from knot_extensions import is_singleton, static_assert + +static_assert(is_singleton(Ellipsis.__class__)) +static_assert(is_singleton((...).__class__)) +``` + +### Python 3.10+ + +On Python 3.10+, the standard library exposes the type of `...` as `types.EllipsisType`, and we also +recognise this as a singleton type when it is referenced directly: + +```toml +[environment] +python-version = "3.10" +``` + +```py +import types +from knot_extensions import static_assert, is_singleton + +static_assert(is_singleton(types.EllipsisType)) +``` diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index e5d26e072694f..a309887fdc9e6 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -1756,6 +1756,7 @@ impl<'db> Type<'db> { KnownClass::NoneType | KnownClass::NoDefaultType | KnownClass::VersionInfo + | KnownClass::EllipsisType | KnownClass::TypeAliasType, ) => true, Some( @@ -2865,6 +2866,9 @@ pub enum KnownClass { OrderedDict, // sys VersionInfo, + // Exposed as `types.EllipsisType` on Python >=3.10; + // backported as `builtins.ellipsis` by typeshed on Python <=3.9 + EllipsisType, } impl<'db> KnownClass { @@ -2872,7 +2876,7 @@ impl<'db> KnownClass { matches!(self, Self::Bool) } - pub const fn as_str(&self) -> &'static str { + pub fn as_str(&self, db: &'db dyn Db) -> &'static str { match self { Self::Bool => "bool", Self::Object => "object", @@ -2912,6 +2916,15 @@ impl<'db> KnownClass { // which is impossible to replicate in the stubs since the sole instance of the class // also has that name in the `sys` module.) Self::VersionInfo => "_version_info", + Self::EllipsisType => { + // Exposed as `types.EllipsisType` on Python >=3.10; + // backported as `builtins.ellipsis` by typeshed on Python <=3.9 + if Program::get(db).python_version(db) >= PythonVersion::PY310 { + "EllipsisType" + } else { + "ellipsis" + } + } } } @@ -2920,7 +2933,7 @@ impl<'db> KnownClass { } pub fn to_class_literal(self, db: &'db dyn Db) -> Type<'db> { - known_module_symbol(db, self.canonical_module(db), self.as_str()) + known_module_symbol(db, self.canonical_module(db), self.as_str(db)) .ignore_possibly_unbound() .unwrap_or(Type::unknown()) } @@ -2935,7 +2948,7 @@ impl<'db> KnownClass { /// Return `true` if this symbol can be resolved to a class definition `class` in typeshed, /// *and* `class` is a subclass of `other`. pub fn is_subclass_of(self, db: &'db dyn Db, other: Class<'db>) -> bool { - known_module_symbol(db, self.canonical_module(db), self.as_str()) + known_module_symbol(db, self.canonical_module(db), self.as_str(db)) .ignore_possibly_unbound() .and_then(Type::into_class_literal) .is_some_and(|ClassLiteralType { class }| class.is_subclass_of(db, other)) @@ -2979,6 +2992,15 @@ impl<'db> KnownClass { KnownModule::TypingExtensions } } + Self::EllipsisType => { + // Exposed as `types.EllipsisType` on Python >=3.10; + // backported as `builtins.ellipsis` by typeshed on Python <=3.9 + if Program::get(db).python_version(db) >= PythonVersion::PY310 { + KnownModule::Types + } else { + KnownModule::Builtins + } + } Self::ChainMap | Self::Counter | Self::DefaultDict @@ -2991,9 +3013,14 @@ impl<'db> KnownClass { /// /// A singleton class is a class where it is known that only one instance can ever exist at runtime. const fn is_singleton(self) -> bool { - // TODO there are other singleton types (EllipsisType, NotImplementedType) + // TODO there are other singleton types (NotImplementedType -- any others?) match self { - Self::NoneType | Self::NoDefaultType | Self::VersionInfo | Self::TypeAliasType => true, + Self::NoneType + | Self::EllipsisType + | Self::NoDefaultType + | Self::VersionInfo + | Self::TypeAliasType => true, + Self::Bool | Self::Object | Self::Bytes @@ -3060,6 +3087,12 @@ impl<'db> KnownClass { "_SpecialForm" => Self::SpecialForm, "_NoDefaultType" => Self::NoDefaultType, "_version_info" => Self::VersionInfo, + "ellipsis" if Program::get(db).python_version(db) <= PythonVersion::PY39 => { + Self::EllipsisType + } + "EllipsisType" if Program::get(db).python_version(db) >= PythonVersion::PY310 => { + Self::EllipsisType + } _ => return None, }; @@ -3096,6 +3129,7 @@ impl<'db> KnownClass { | Self::ModuleType | Self::VersionInfo | Self::BaseException + | Self::EllipsisType | Self::BaseExceptionGroup | Self::FunctionType => module == self.canonical_module(db), Self::NoneType => matches!(module, KnownModule::Typeshed | KnownModule::Types), diff --git a/crates/red_knot_python_semantic/src/types/infer.rs b/crates/red_knot_python_semantic/src/types/infer.rs index bf1fef80032dc..a7b4e7c8100cc 100644 --- a/crates/red_knot_python_semantic/src/types/infer.rs +++ b/crates/red_knot_python_semantic/src/types/infer.rs @@ -2814,17 +2814,15 @@ impl<'db> TypeInferenceBuilder<'db> { fn infer_number_literal_expression(&mut self, literal: &ast::ExprNumberLiteral) -> Type<'db> { let ast::ExprNumberLiteral { range: _, value } = literal; + let db = self.db(); match value { ast::Number::Int(n) => n .as_i64() .map(Type::IntLiteral) - .unwrap_or_else(|| KnownClass::Int.to_instance(self.db())), - ast::Number::Float(_) => KnownClass::Float.to_instance(self.db()), - ast::Number::Complex { .. } => builtins_symbol(self.db(), "complex") - .ignore_possibly_unbound() - .unwrap_or(Type::unknown()) - .to_instance(self.db()), + .unwrap_or_else(|| KnownClass::Int.to_instance(db)), + ast::Number::Float(_) => KnownClass::Float.to_instance(db), + ast::Number::Complex { .. } => KnownClass::Complex.to_instance(db), } } @@ -2908,9 +2906,7 @@ impl<'db> TypeInferenceBuilder<'db> { &mut self, _literal: &ast::ExprEllipsisLiteral, ) -> Type<'db> { - builtins_symbol(self.db(), "Ellipsis") - .ignore_possibly_unbound() - .unwrap_or(Type::unknown()) + KnownClass::EllipsisType.to_instance(self.db()) } fn infer_tuple_expression(&mut self, tuple: &ast::ExprTuple) -> Type<'db> {