Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow assigning ellipsis literal as parameter default value #14982

Merged
merged 38 commits into from
Jan 5, 2025
Merged
Show file tree
Hide file tree
Changes from 35 commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
37f5607
Allow assigning ellipsis literal as parameter default value
Glyphack Dec 15, 2024
69fe381
Add Ellipsis symbol to tests
Glyphack Dec 15, 2024
3d69045
Update crates/red_knot_python_semantic/resources/mdtest/stubs/ellipsi…
Glyphack Dec 16, 2024
3b730ba
Update crates/red_knot_python_semantic/resources/mdtest/stubs/ellipsi…
Glyphack Dec 16, 2024
39399ce
Update crates/red_knot_python_semantic/resources/mdtest/stubs/ellipsi…
Glyphack Dec 16, 2024
3c81c9b
Update crates/red_knot_python_semantic/resources/mdtest/stubs/ellipsi…
Glyphack Dec 16, 2024
d28b2e7
Update crates/red_knot_python_semantic/resources/mdtest/stubs/ellipsi…
Glyphack Dec 16, 2024
342cd2b
Update crates/red_knot_python_semantic/resources/mdtest/stubs/ellipsi…
Glyphack Dec 16, 2024
f2d7bcb
Update crates/red_knot_python_semantic/src/types/infer.rs
Glyphack Dec 16, 2024
1151921
Allow usage of ellipsis literal in all scopes
Glyphack Dec 19, 2024
b5791e1
Update crates/red_knot_python_semantic/resources/mdtest/stubs/ellipsi…
Glyphack Dec 20, 2024
539d83c
Update crates/red_knot_python_semantic/resources/mdtest/stubs/ellipsi…
Glyphack Dec 20, 2024
f31c237
Update crates/red_knot_python_semantic/resources/mdtest/stubs/ellipsi…
Glyphack Dec 20, 2024
e4ce7d3
Update crates/red_knot_python_semantic/resources/mdtest/stubs/ellipsi…
Glyphack Dec 20, 2024
bd216c1
Update crates/red_knot_python_semantic/resources/mdtest/stubs/ellipsi…
Glyphack Dec 20, 2024
c0709f0
Update crates/red_knot_python_semantic/resources/mdtest/stubs/ellipsi…
Glyphack Dec 20, 2024
98dab34
Update crates/red_knot_python_semantic/src/types/infer.rs
Glyphack Dec 20, 2024
13f91d7
Infer Type::Unknown for a variable with value `...`
Glyphack Dec 30, 2024
c65731b
Add a test for annotated assignments in stub files
Glyphack Dec 30, 2024
2753d87
Allow unpacking `...` in stub files
Glyphack Dec 30, 2024
4f8ae46
Clippy fix
Glyphack Dec 30, 2024
dd6396b
Do not allow for loops to iterate over ellipsis literal
Glyphack Dec 30, 2024
30f8f65
Refactor unpacker code for assignment statements
Glyphack Dec 31, 2024
c6521ed
Remove is_assignment_stmt flag
Glyphack Dec 31, 2024
2be530d
Create is_assign method for UnpackValue
Glyphack Dec 31, 2024
fbce5a2
Update crates/red_knot_python_semantic/resources/mdtest/stubs/ellipsi…
Glyphack Jan 5, 2025
07bc3fa
Update crates/red_knot_python_semantic/resources/mdtest/stubs/ellipsi…
Glyphack Jan 5, 2025
819293d
Update crates/red_knot_python_semantic/resources/mdtest/stubs/ellipsi…
Glyphack Jan 5, 2025
e46df8c
Update crates/red_knot_python_semantic/resources/mdtest/stubs/ellipsi…
Glyphack Jan 5, 2025
1f61e40
Update crates/red_knot_python_semantic/resources/mdtest/stubs/ellipsi…
Glyphack Jan 5, 2025
29e5b38
Rename is_stub to in_stub
Glyphack Jan 5, 2025
c489755
Remove unnecessary else branch
Glyphack Jan 5, 2025
b9eb966
Update crates/red_knot_python_semantic/resources/mdtest/assignment/an…
Glyphack Jan 5, 2025
e445018
Add a helper for InferContext::in_stub
Glyphack Jan 5, 2025
5d0de37
Update crates/red_knot_python_semantic/src/types/unpacker.rs
Glyphack Jan 5, 2025
277b30c
Use InferContext::in_stub
Glyphack Jan 5, 2025
eaca9c9
Revert unnecessary change
Glyphack Jan 5, 2025
d8315da
Refactor condition for ellipsis literal
Glyphack Jan 5, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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]
```
carljm marked this conversation as resolved.
Show resolved Hide resolved
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

Glyphack marked this conversation as resolved.
Show resolved Hide resolved
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

Glyphack marked this conversation as resolved.
Show resolved Hide resolved
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
Glyphack marked this conversation as resolved.
Show resolved Hide resolved
Glyphack marked this conversation as resolved.
Show resolved Hide resolved
# 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)
carljm marked this conversation as resolved.
Show resolved Hide resolved
}
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
13 changes: 11 additions & 2 deletions crates/red_knot_python_semantic/src/types/unpacker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,16 +35,25 @@ impl<'db> Unpacker<'db> {
self.context.db()
}

/// Unpack the value to the target expression.
pub(crate) fn unpack(&mut self, target: &ast::Expr, value: UnpackValue<'db>) {
debug_assert!(
matches!(target, ast::Expr::List(_) | ast::Expr::Tuple(_)),
"Unpacking target must be a list or tuple expression"
);

let mut value_ty = infer_expression_types(self.db(), value.expression())
.expression_ty(value.scoped_expression_id(self.db(), self.scope));

let is_in_stub_file = value
.expression()
.file(self.db())
.is_stub(self.db().upcast());
AlexWaygood marked this conversation as resolved.
Show resolved Hide resolved
let value_is_ellipsis_literal = value
.expression()
.node_ref(self.db())
.is_ellipsis_literal_expr();
if value.is_assign() && is_in_stub_file && value_is_ellipsis_literal {
value_ty = Type::Unknown;
}
AlexWaygood marked this conversation as resolved.
Show resolved Hide resolved
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 {
carljm marked this conversation as resolved.
Show resolved Hide resolved
matches!(self, UnpackValue::Assign(_))
}

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