Skip to content

Commit cb86a82

Browse files
committed
Merge branch 'main' into charlie/metaclass
* main: [ty] Move binary expression logic out of `builder.rs` (#23649) [ty] Limit recursion depth when displaying self-referential function types (#23647) [ty] Move comparison logic out of `builder.rs` (#23646) Avoid inserting redundant `None` elements in UP045 (#23459) [ty] Add more ParamSpec validation for `P.args` and `P.kwargs` (#23640) [ty] Sync vendored typeshed stubs (#23642) [ty] Fix inference of `t.__mro__` if `t` is an instance of `type[Any]` (#23632) [ty] Detect inconsistent generic base class specializations (#23615) [ty] Validate type variable defaults don't reference later type parameters or type parameters out of scope (#23623) [ty] Ban nested `Required`/`NotRequired`, and ban them both outside of `TypedDict` fields (#23627) [ty] Reject generic metaclasses parameterized by type variables (#23628) Update default Python version examples (#23605) fix binops with NewType of float and Unknown (#23620) [ty] Reject functions with PEP-695 type parameters that shadow type parameters from enclosing scopes (#23619) [ty] Reject ellipsis literals in odd places in type/annotation expressions (#23611) [ty] hash-cons `UseDefMap` fields (#23283)
2 parents 26e1cc8 + 63b7ee8 commit cb86a82

62 files changed

Lines changed: 4751 additions & 2424 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -254,8 +254,8 @@ exclude = [
254254
line-length = 88
255255
indent-width = 4
256256
257-
# Assume Python 3.9
258-
target-version = "py39"
257+
# Assume Python 3.10
258+
target-version = "py310"
259259
260260
[lint]
261261
# Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default.

crates/ruff_linter/resources/test/fixtures/pyupgrade/UP045.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,3 +82,12 @@ class ServiceRefOrValue:
8282
int
8383
# text
8484
] = None
85+
86+
87+
# Regression test for: https://github.com/astral-sh/ruff/issues/23429
88+
# Optional[None | X] should not produce None | None
89+
bar: None | Optional[None | int] = None
90+
bar: Optional[None | int] = None
91+
bar: Optional[int | None] = None
92+
bar: Optional[None | int | str] = None
93+
bar: Optional[None | None] = None

crates/ruff_linter/src/rules/pyupgrade/rules/use_pep604_annotation.rs

Lines changed: 57 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use ruff_macros::{ViolationMetadata, derive_message_formats};
22
use ruff_python_ast::PythonVersion;
33
use ruff_python_ast::helpers::{pep_604_optional, pep_604_union};
4-
use ruff_python_ast::{self as ast, Expr};
4+
use ruff_python_ast::{self as ast, Expr, Operator};
55
use ruff_python_semantic::analyze::typing::{Pep604Operator, to_pep604_operator};
66
use ruff_source_file::LineRanges;
77
use ruff_text_size::Ranged;
@@ -190,17 +190,40 @@ pub(crate) fn non_pep604_annotation(
190190
}
191191
}
192192

193-
diagnostic.set_fix(Fix::applicable_edit(
194-
Edit::range_replacement(
195-
pad(
196-
checker.generator().expr(&pep_604_optional(inner)),
193+
// If the inner expression is a `BitOr` union that already
194+
// contains `None`, strip it out and re-add it only at the end.
195+
// This avoids generating `None | None` which is a runtime
196+
// `TypeError`. For example, `Optional[None | int]` should
197+
// become `int | None`, not `None | int | None`.
198+
let fix_expr = if let Expr::BinOp(ast::ExprBinOp {
199+
op: Operator::BitOr,
200+
..
201+
}) = inner
202+
{
203+
let elements = collect_non_none(inner);
204+
if elements.is_empty() {
205+
// All elements were `None`; don't provide a fix.
206+
None
207+
} else {
208+
Some(pep_604_optional(&pep_604_union(&elements)))
209+
}
210+
} else {
211+
Some(pep_604_optional(inner))
212+
};
213+
214+
if let Some(fix_expr) = fix_expr {
215+
diagnostic.set_fix(Fix::applicable_edit(
216+
Edit::range_replacement(
217+
pad(
218+
checker.generator().expr(&fix_expr),
219+
expr.range(),
220+
checker.locator(),
221+
),
197222
expr.range(),
198-
checker.locator(),
199223
),
200-
expr.range(),
201-
),
202-
applicability,
203-
));
224+
applicability,
225+
));
226+
}
204227
}
205228
}
206229
}
@@ -332,3 +355,27 @@ fn is_named_tuple(checker: &Checker, expr: &Expr) -> bool {
332355
fn is_optional_none(operator: Pep604Operator, slice: &Expr) -> bool {
333356
matches!(operator, Pep604Operator::Optional) && matches!(slice, Expr::NoneLiteral(_))
334357
}
358+
359+
/// Collect all non-`None` leaf elements of a chain of `BitOr` binary operations.
360+
///
361+
/// For example, `a | None | b` is collected as `[a, b]`.
362+
fn collect_non_none(expr: &Expr) -> Vec<Expr> {
363+
fn inner(expr: &Expr, elements: &mut Vec<Expr>) {
364+
if let Expr::BinOp(ast::ExprBinOp {
365+
left,
366+
op: Operator::BitOr,
367+
right,
368+
..
369+
}) = expr
370+
{
371+
inner(left, elements);
372+
inner(right, elements);
373+
} else if !expr.is_none_literal_expr() {
374+
elements.push(expr.clone());
375+
}
376+
}
377+
378+
let mut elements = Vec::new();
379+
inner(expr, &mut elements);
380+
elements
381+
}

crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP045.py.snap

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,4 +311,93 @@ help: Convert to `X | None`
311311
- # text
312312
- ] = None
313313
81 + foo: int | None = None
314+
82 |
315+
83 |
316+
84 | # Regression test for: https://github.com/astral-sh/ruff/issues/23429
314317
note: This is an unsafe fix and may change runtime behavior
318+
319+
UP045 [*] Use `X | None` for type annotations
320+
--> UP045.py:89:13
321+
|
322+
87 | # Regression test for: https://github.com/astral-sh/ruff/issues/23429
323+
88 | # Optional[None | X] should not produce None | None
324+
89 | bar: None | Optional[None | int] = None
325+
| ^^^^^^^^^^^^^^^^^^^^
326+
90 | bar: Optional[None | int] = None
327+
91 | bar: Optional[int | None] = None
328+
|
329+
help: Convert to `X | None`
330+
86 |
331+
87 | # Regression test for: https://github.com/astral-sh/ruff/issues/23429
332+
88 | # Optional[None | X] should not produce None | None
333+
- bar: None | Optional[None | int] = None
334+
89 + bar: None | int | None = None
335+
90 | bar: Optional[None | int] = None
336+
91 | bar: Optional[int | None] = None
337+
92 | bar: Optional[None | int | str] = None
338+
339+
UP045 [*] Use `X | None` for type annotations
340+
--> UP045.py:90:6
341+
|
342+
88 | # Optional[None | X] should not produce None | None
343+
89 | bar: None | Optional[None | int] = None
344+
90 | bar: Optional[None | int] = None
345+
| ^^^^^^^^^^^^^^^^^^^^
346+
91 | bar: Optional[int | None] = None
347+
92 | bar: Optional[None | int | str] = None
348+
|
349+
help: Convert to `X | None`
350+
87 | # Regression test for: https://github.com/astral-sh/ruff/issues/23429
351+
88 | # Optional[None | X] should not produce None | None
352+
89 | bar: None | Optional[None | int] = None
353+
- bar: Optional[None | int] = None
354+
90 + bar: int | None = None
355+
91 | bar: Optional[int | None] = None
356+
92 | bar: Optional[None | int | str] = None
357+
93 | bar: Optional[None | None] = None
358+
359+
UP045 [*] Use `X | None` for type annotations
360+
--> UP045.py:91:6
361+
|
362+
89 | bar: None | Optional[None | int] = None
363+
90 | bar: Optional[None | int] = None
364+
91 | bar: Optional[int | None] = None
365+
| ^^^^^^^^^^^^^^^^^^^^
366+
92 | bar: Optional[None | int | str] = None
367+
93 | bar: Optional[None | None] = None
368+
|
369+
help: Convert to `X | None`
370+
88 | # Optional[None | X] should not produce None | None
371+
89 | bar: None | Optional[None | int] = None
372+
90 | bar: Optional[None | int] = None
373+
- bar: Optional[int | None] = None
374+
91 + bar: int | None = None
375+
92 | bar: Optional[None | int | str] = None
376+
93 | bar: Optional[None | None] = None
377+
378+
UP045 [*] Use `X | None` for type annotations
379+
--> UP045.py:92:6
380+
|
381+
90 | bar: Optional[None | int] = None
382+
91 | bar: Optional[int | None] = None
383+
92 | bar: Optional[None | int | str] = None
384+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
385+
93 | bar: Optional[None | None] = None
386+
|
387+
help: Convert to `X | None`
388+
89 | bar: None | Optional[None | int] = None
389+
90 | bar: Optional[None | int] = None
390+
91 | bar: Optional[int | None] = None
391+
- bar: Optional[None | int | str] = None
392+
92 + bar: int | str | None = None
393+
93 | bar: Optional[None | None] = None
394+
395+
UP045 Use `X | None` for type annotations
396+
--> UP045.py:93:6
397+
|
398+
91 | bar: Optional[int | None] = None
399+
92 | bar: Optional[None | int | str] = None
400+
93 | bar: Optional[None | None] = None
401+
| ^^^^^^^^^^^^^^^^^^^^^
402+
|
403+
help: Convert to `X | None`

0 commit comments

Comments
 (0)