[red-knot] Recognize `...` as a singleton (#16184)

This commit is contained in:
Alex Waygood 2025-02-16 22:01:02 +00:00 committed by GitHub
parent d4b4f65e20
commit 4941975e74
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 118 additions and 14 deletions

View File

@ -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]
```

View File

@ -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))
```

View File

@ -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),

View File

@ -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> {