mirror of
https://github.com/astral-sh/ruff.git
synced 2025-08-20 10:30:56 +00:00
Allow assigning ellipsis literal as parameter default value (#14982)
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>
This commit is contained in:
parent
2ea63620cf
commit
b26448926a
6 changed files with 126 additions and 1 deletions
|
@ -122,3 +122,10 @@ class Foo: ...
|
||||||
x = Foo()
|
x = Foo()
|
||||||
reveal_type(x) # revealed: 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]
|
||||||
|
```
|
||||||
|
|
|
@ -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: ...
|
||||||
|
```
|
|
@ -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]
|
#[must_use]
|
||||||
pub(crate) fn finish(mut self) -> TypeCheckDiagnostics {
|
pub(crate) fn finish(mut self) -> TypeCheckDiagnostics {
|
||||||
self.bomb.defuse();
|
self.bomb.defuse();
|
||||||
|
|
|
@ -435,6 +435,10 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||||
matches!(self.region, InferenceRegion::Deferred(_)) || self.deferred_state.is_deferred()
|
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.
|
/// Get the already-inferred type of an expression node.
|
||||||
///
|
///
|
||||||
/// ## Panics
|
/// ## Panics
|
||||||
|
@ -1174,6 +1178,12 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||||
let inferred_ty = if let Some(default_ty) = default_ty {
|
let inferred_ty = if let Some(default_ty) = default_ty {
|
||||||
if default_ty.is_assignable_to(self.db(), declared_ty) {
|
if default_ty.is_assignable_to(self.db(), declared_ty) {
|
||||||
UnionType::from_elements(self.db(), [declared_ty, default_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 {
|
} else {
|
||||||
self.context.report_lint(
|
self.context.report_lint(
|
||||||
&INVALID_PARAMETER_DEFAULT,
|
&INVALID_PARAMETER_DEFAULT,
|
||||||
|
@ -1896,7 +1906,13 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||||
let name_ast_id = name.scoped_expression_id(self.db(), self.scope());
|
let name_ast_id = name.scoped_expression_id(self.db(), self.scope());
|
||||||
unpacked.get(name_ast_id).unwrap_or(Type::Unknown)
|
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) =
|
if let Some(known_instance) =
|
||||||
|
@ -1963,6 +1979,11 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||||
|
|
||||||
if let Some(value) = value.as_deref() {
|
if let Some(value) = value.as_deref() {
|
||||||
let value_ty = self.infer_expression(value);
|
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(
|
self.add_declaration_with_binding(
|
||||||
assignment.into(),
|
assignment.into(),
|
||||||
definition,
|
definition,
|
||||||
|
|
|
@ -45,6 +45,15 @@ impl<'db> Unpacker<'db> {
|
||||||
let mut value_ty = infer_expression_types(self.db(), value.expression())
|
let mut value_ty = infer_expression_types(self.db(), value.expression())
|
||||||
.expression_ty(value.scoped_expression_id(self.db(), self.scope));
|
.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 value.is_iterable() {
|
||||||
// If the value is an iterable, then the type that needs to be unpacked is the iterator
|
// If the value is an iterable, then the type that needs to be unpacked is the iterator
|
||||||
// type.
|
// type.
|
||||||
|
|
|
@ -76,6 +76,11 @@ impl<'db> UnpackValue<'db> {
|
||||||
matches!(self, UnpackValue::Iterable(_))
|
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.
|
/// Returns the underlying [`Expression`] that is being unpacked.
|
||||||
pub(crate) const fn expression(self) -> Expression<'db> {
|
pub(crate) const fn expression(self) -> Expression<'db> {
|
||||||
match self {
|
match self {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue