Skip to content

Commit

Permalink
[red-knot] Recognize ... as a singleton (#16184)
Browse files Browse the repository at this point in the history
  • Loading branch information
AlexWaygood authored Feb 16, 2025
1 parent d4b4f65 commit 4941975
Show file tree
Hide file tree
Showing 4 changed files with 118 additions and 14 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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]
```
Original file line number Diff line number Diff line change
Expand Up @@ -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))
```
44 changes: 39 additions & 5 deletions crates/red_knot_python_semantic/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1756,6 +1756,7 @@ impl<'db> Type<'db> {
KnownClass::NoneType
| KnownClass::NoDefaultType
| KnownClass::VersionInfo
| KnownClass::EllipsisType
| KnownClass::TypeAliasType,
) => true,
Some(
Expand Down Expand Up @@ -2865,14 +2866,17 @@ 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 {
pub const fn is_bool(self) -> bool {
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",
Expand Down Expand Up @@ -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"
}
}
}
}

Expand All @@ -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())
}
Expand All @@ -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))
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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,
};

Expand Down Expand Up @@ -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),
Expand Down
14 changes: 5 additions & 9 deletions crates/red_knot_python_semantic/src/types/infer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
}
}

Expand Down Expand Up @@ -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> {
Expand Down

0 comments on commit 4941975

Please sign in to comment.