Skip to content

Commit

Permalink
Allow assigning ellipsis literal as parameter default value (#14982)
Browse files Browse the repository at this point in the history
Resolves #14840

## Summary

Usage of ellipsis literal as default argument is allowed in stub files.

## Test Plan

Added mdtest for both python files and stub files.


---------

Co-authored-by: Carl Meyer <carl@oddbird.net>
Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
  • Loading branch information
3 people authored Jan 5, 2025
1 parent 2ea6362 commit b264489
Show file tree
Hide file tree
Showing 6 changed files with 126 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -122,3 +122,10 @@ class Foo: ...
x = Foo()
reveal_type(x) # revealed: Foo
```

## Annotated assignments in stub files are inferred correctly

```pyi path=main.pyi
x: int = 1
reveal_type(x) # revealed: Literal[1]
```
78 changes: 78 additions & 0 deletions crates/red_knot_python_semantic/resources/mdtest/stubs/ellipsis.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# Ellipsis

## Function and methods

The ellipsis literal `...` can be used as a placeholder default value for a function parameter, in a
stub file only, regardless of the type of the parameter.

```py path=test.pyi
def f(x: int = ...) -> None:
reveal_type(x) # revealed: int

def f2(x: str = ...) -> None:
reveal_type(x) # revealed: str
```

## Class and module symbols

The ellipsis literal can be assigned to a class or module symbol, regardless of its declared type,
in a stub file only.

```py path=test.pyi
y: bytes = ...
reveal_type(y) # revealed: bytes
x = ...
reveal_type(x) # revealed: Unknown

class Foo:
y: int = ...

reveal_type(Foo.y) # revealed: int
```

## Unpacking ellipsis literal in assignment

No diagnostic is emitted if an ellipsis literal is "unpacked" in a stub file as part of an
assignment statement:

```py path=test.pyi
x, y = ...
reveal_type(x) # revealed: Unknown
reveal_type(y) # revealed: Unknown
```

## Unpacking ellipsis literal in for loops

Iterating over an ellipsis literal as part of a `for` loop in a stub is invalid, however, and
results in a diagnostic:

```py path=test.pyi
# error: [not-iterable] "Object of type `ellipsis` is not iterable"
for a, b in ...:
reveal_type(a) # revealed: Unknown
reveal_type(b) # revealed: Unknown
```

## Ellipsis usage in non stub file

In a non-stub file, there's no special treatment of ellipsis literals. An ellipsis literal can only
be assigned if `EllipsisType` is actually assignable to the annotated type.

```py
# error: 7 [invalid-parameter-default] "Default value of type `ellipsis` is not assignable to annotated parameter type `int`"
def f(x: int = ...) -> None: ...

# error: 1 [invalid-assignment] "Object of type `ellipsis` is not assignable to `int`"
a: int = ...
b = ...
reveal_type(b) # revealed: ellipsis
```

## Use of `Ellipsis` symbol

There is no special treatment of the builtin name `Ellipsis` in stubs, only of `...` literals.

```py path=test.pyi
# error: 7 [invalid-parameter-default] "Default value of type `ellipsis` is not assignable to annotated parameter type `int`"
def f(x: int = Ellipsis) -> None: ...
```
5 changes: 5 additions & 0 deletions crates/red_knot_python_semantic/src/types/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,11 @@ impl<'db> InferContext<'db> {
}
}

/// Are we currently inferring types in a stub file?
pub(crate) fn in_stub(&self) -> bool {
self.file.is_stub(self.db().upcast())
}

#[must_use]
pub(crate) fn finish(mut self) -> TypeCheckDiagnostics {
self.bomb.defuse();
Expand Down
23 changes: 22 additions & 1 deletion crates/red_knot_python_semantic/src/types/infer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -435,6 +435,10 @@ impl<'db> TypeInferenceBuilder<'db> {
matches!(self.region, InferenceRegion::Deferred(_)) || self.deferred_state.is_deferred()
}

fn in_stub(&self) -> bool {
self.context.in_stub()
}

/// Get the already-inferred type of an expression node.
///
/// ## Panics
Expand Down Expand Up @@ -1174,6 +1178,12 @@ impl<'db> TypeInferenceBuilder<'db> {
let inferred_ty = if let Some(default_ty) = default_ty {
if default_ty.is_assignable_to(self.db(), declared_ty) {
UnionType::from_elements(self.db(), [declared_ty, default_ty])
} else if self.in_stub()
&& default
.as_ref()
.is_some_and(|d| d.is_ellipsis_literal_expr())
{
declared_ty
} else {
self.context.report_lint(
&INVALID_PARAMETER_DEFAULT,
Expand Down Expand Up @@ -1896,7 +1906,13 @@ impl<'db> TypeInferenceBuilder<'db> {
let name_ast_id = name.scoped_expression_id(self.db(), self.scope());
unpacked.get(name_ast_id).unwrap_or(Type::Unknown)
}
TargetKind::Name => value_ty,
TargetKind::Name => {
if self.in_stub() && value.is_ellipsis_literal_expr() {
Type::Unknown
} else {
value_ty
}
}
};

if let Some(known_instance) =
Expand Down Expand Up @@ -1963,6 +1979,11 @@ impl<'db> TypeInferenceBuilder<'db> {

if let Some(value) = value.as_deref() {
let value_ty = self.infer_expression(value);
let value_ty = if self.in_stub() && value.is_ellipsis_literal_expr() {
annotation_ty
} else {
value_ty
};
self.add_declaration_with_binding(
assignment.into(),
definition,
Expand Down
9 changes: 9 additions & 0 deletions crates/red_knot_python_semantic/src/types/unpacker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,15 @@ impl<'db> Unpacker<'db> {
let mut value_ty = infer_expression_types(self.db(), value.expression())
.expression_ty(value.scoped_expression_id(self.db(), self.scope));

if value.is_assign()
&& self.context.in_stub()
&& value
.expression()
.node_ref(self.db())
.is_ellipsis_literal_expr()
{
value_ty = Type::Unknown;
}
if value.is_iterable() {
// If the value is an iterable, then the type that needs to be unpacked is the iterator
// type.
Expand Down
5 changes: 5 additions & 0 deletions crates/red_knot_python_semantic/src/unpack.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,11 @@ impl<'db> UnpackValue<'db> {
matches!(self, UnpackValue::Iterable(_))
}

/// Returns `true` if the value is being assigned to a target.
pub(crate) const fn is_assign(self) -> bool {
matches!(self, UnpackValue::Assign(_))
}

/// Returns the underlying [`Expression`] that is being unpacked.
pub(crate) const fn expression(self) -> Expression<'db> {
match self {
Expand Down

0 comments on commit b264489

Please sign in to comment.