Skip to content

Commit

Permalink
[pyupgrade] Implement unnecessary-default-type-args (UP043) (#1…
Browse files Browse the repository at this point in the history
…2371)

## Summary

Add new rule and implement for `unnecessary default type arguments`
under the `UP` category (`UP043`).

```py
// < py313
Generator[int, None, None] 

// >= py313
Generator[int]
```

I think that as Python 3.13 develops, there might be more default type
arguments added besides `Generator` and `AsyncGenerator`. So, I made
this more flexible to accommodate future changes.

related issue: #12286

## Test Plan

snapshot included..!
  • Loading branch information
cake-monotone authored Jul 17, 2024
1 parent 1435b0f commit 1df51b1
Show file tree
Hide file tree
Showing 8 changed files with 306 additions and 0 deletions.
41 changes: 41 additions & 0 deletions crates/ruff_linter/resources/test/fixtures/pyupgrade/UP043.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
from typing import Generator, AsyncGenerator


def func() -> Generator[int, None, None]:
yield 42


def func() -> Generator[int, None]:
yield 42


def func() -> Generator[int]:
yield 42


def func() -> Generator[int, int, int]:
foo = yield 42
return foo


def func() -> Generator[int, int, None]:
_ = yield 42
return None


def func() -> Generator[int, None, int]:
yield 42
return 42


async def func() -> AsyncGenerator[int, None]:
yield 42


async def func() -> AsyncGenerator[int]:
yield 42


async def func() -> AsyncGenerator[int, int]:
foo = yield 42
return foo
6 changes: 6 additions & 0 deletions crates/ruff_linter/src/checkers/ast/analyze/expression.rs
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,12 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
ruff::rules::never_union(checker, expr);
}

if checker.enabled(Rule::UnnecessaryDefaultTypeArgs) {
if checker.settings.target_version >= PythonVersion::Py313 {
pyupgrade::rules::unnecessary_default_type_args(checker, expr);
}
}

if checker.any_enabled(&[
Rule::SysVersionSlice3,
Rule::SysVersion2,
Expand Down
1 change: 1 addition & 0 deletions crates/ruff_linter/src/codes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -521,6 +521,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Pyupgrade, "040") => (RuleGroup::Stable, rules::pyupgrade::rules::NonPEP695TypeAlias),
(Pyupgrade, "041") => (RuleGroup::Stable, rules::pyupgrade::rules::TimeoutErrorAlias),
(Pyupgrade, "042") => (RuleGroup::Preview, rules::pyupgrade::rules::ReplaceStrEnum),
(Pyupgrade, "043") => (RuleGroup::Preview, rules::pyupgrade::rules::UnnecessaryDefaultTypeArgs),

// pydocstyle
(Pydocstyle, "100") => (RuleGroup::Stable, rules::pydocstyle::rules::UndocumentedPublicModule),
Expand Down
1 change: 1 addition & 0 deletions crates/ruff_linter/src/rules/pyupgrade/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ mod tests {
#[test_case(Rule::UnicodeKindPrefix, Path::new("UP025.py"))]
#[test_case(Rule::UnnecessaryBuiltinImport, Path::new("UP029.py"))]
#[test_case(Rule::UnnecessaryClassParentheses, Path::new("UP039.py"))]
#[test_case(Rule::UnnecessaryDefaultTypeArgs, Path::new("UP043.py"))]
#[test_case(Rule::UnnecessaryEncodeUTF8, Path::new("UP012.py"))]
#[test_case(Rule::UnnecessaryFutureImport, Path::new("UP010.py"))]
#[test_case(Rule::UnpackedListComprehension, Path::new("UP027.py"))]
Expand Down
2 changes: 2 additions & 0 deletions crates/ruff_linter/src/rules/pyupgrade/rules/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ pub(crate) use unicode_kind_prefix::*;
pub(crate) use unnecessary_builtin_import::*;
pub(crate) use unnecessary_class_parentheses::*;
pub(crate) use unnecessary_coding_comment::*;
pub(crate) use unnecessary_default_type_args::*;
pub(crate) use unnecessary_encode_utf8::*;
pub(crate) use unnecessary_future_import::*;
pub(crate) use unpacked_list_comprehension::*;
Expand Down Expand Up @@ -69,6 +70,7 @@ mod unicode_kind_prefix;
mod unnecessary_builtin_import;
mod unnecessary_class_parentheses;
mod unnecessary_coding_comment;
mod unnecessary_default_type_args;
mod unnecessary_encode_utf8;
mod unnecessary_future_import;
mod unpacked_list_comprehension;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{self as ast, Expr};
use ruff_text_size::{Ranged, TextRange};

use crate::checkers::ast::Checker;

/// ## What it does
/// Checks for unnecessary default type arguments.
///
/// ## Why is this bad?
/// Python 3.13 introduced the ability for type parameters to specify default
/// values. As such, the default type arguments for some types in the standard
/// library (e.g., Generator, AsyncGenerator) are now optional.
///
/// Omitting type parameters that match the default values can make the code
/// more concise and easier to read.
///
/// ## Examples
///
/// ```python
/// from typing import Generator, AsyncGenerator
///
///
/// def sync_gen() -> Generator[int, None, None]:
/// yield 42
///
///
/// async def async_gen() -> AsyncGenerator[int, None]:
/// yield 42
/// ```
///
/// Use instead:
///
/// ```python
/// from typing import Generator, AsyncGenerator
///
///
/// def sync_gen() -> Generator[int]:
/// yield 42
///
///
/// async def async_gen() -> AsyncGenerator[int]:
/// yield 42
/// ```
///
/// ## References
///
/// - [PEP 696 – Type Defaults for Type Parameters](https://peps.python.org/pep-0696/)
/// - [typing.Generator](https://docs.python.org/3.13/library/typing.html#typing.Generator)
/// - [typing.AsyncGenerator](https://docs.python.org/3.13/library/typing.html#typing.AsyncGenerator)
#[violation]
pub struct UnnecessaryDefaultTypeArgs;

impl AlwaysFixableViolation for UnnecessaryDefaultTypeArgs {
#[derive_message_formats]
fn message(&self) -> String {
format!("Unnecessary default type arguments")
}

fn fix_title(&self) -> String {
format!("Remove default type arguments")
}
}

/// UP043
pub(crate) fn unnecessary_default_type_args(checker: &mut Checker, expr: &Expr) {
let Expr::Subscript(ast::ExprSubscript { value, slice, .. }) = expr else {
return;
};

let Expr::Tuple(ast::ExprTuple {
elts,
ctx: _,
range: _,
parenthesized: _,
}) = slice.as_ref()
else {
return;
};

// The type annotation must be `Generator` or `AsyncGenerator`.
let Some(type_annotation) = DefaultedTypeAnnotation::from_expr(value, checker.semantic())
else {
return;
};

let valid_elts = type_annotation.trim_unnecessary_defaults(elts);

// If we didn't trim any elements, then the default type arguments are necessary.
if *elts == valid_elts {
return;
}

let mut diagnostic = Diagnostic::new(UnnecessaryDefaultTypeArgs, expr.range());
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(
checker
.generator()
.expr(&Expr::Subscript(ast::ExprSubscript {
value: value.clone(),
slice: Box::new(if let [elt] = valid_elts.as_slice() {
elt.clone()
} else {
Expr::Tuple(ast::ExprTuple {
elts: valid_elts,
ctx: ast::ExprContext::Load,
range: TextRange::default(),
parenthesized: true,
})
}),
ctx: ast::ExprContext::Load,
range: TextRange::default(),
})),
expr.range(),
)));
checker.diagnostics.push(diagnostic);
}

/// Trim trailing `None` literals from the given elements.
///
/// For example, given `[int, None, None]`, return `[int]`.
fn trim_trailing_none(elts: &[Expr]) -> &[Expr] {
match elts.iter().rposition(|elt| !elt.is_none_literal_expr()) {
Some(trimmed_last_index) => elts[..=trimmed_last_index].as_ref(),
None => &[],
}
}

/// Type annotations that include default type arguments as of Python 3.13.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum DefaultedTypeAnnotation {
/// `typing.Generator[YieldType, SendType = None, ReturnType = None]`
Generator,
/// `typing.AsyncGenerator[YieldType, SendType = None]`
AsyncGenerator,
}

impl DefaultedTypeAnnotation {
/// Returns the [`DefaultedTypeAnnotation`], if the given expression is a type annotation that
/// includes default type arguments.
fn from_expr(expr: &Expr, semantic: &ruff_python_semantic::SemanticModel) -> Option<Self> {
let qualified_name = semantic.resolve_qualified_name(expr)?;
if semantic.match_typing_qualified_name(&qualified_name, "Generator") {
Some(Self::Generator)
} else if semantic.match_typing_qualified_name(&qualified_name, "AsyncGenerator") {
Some(Self::AsyncGenerator)
} else {
None
}
}

/// Trim any unnecessary default type arguments from the given elements.
fn trim_unnecessary_defaults(self, elts: &[Expr]) -> Vec<Expr> {
match self {
Self::Generator => {
// Check only if the number of elements is 2 or 3 (e.g., `Generator[int, None]` or `Generator[int, None, None]`).
// Otherwise, ignore (e.g., `Generator[]`, `Generator[int]`, `Generator[int, None, None, None]`)
if elts.len() != 2 && elts.len() != 3 {
return elts.to_vec();
}

std::iter::once(elts[0].clone())
.chain(trim_trailing_none(&elts[1..]).iter().cloned())
.collect::<Vec<_>>()
}
Self::AsyncGenerator => {
// Check only if the number of elements is 2 (e.g., `AsyncGenerator[int, None]`).
// Otherwise, ignore (e.g., `AsyncGenerator[]`, `AsyncGenerator[int]`, `AsyncGenerator[int, None, None]`)
if elts.len() != 2 {
return elts.to_vec();
}

std::iter::once(elts[0].clone())
.chain(trim_trailing_none(&elts[1..]).iter().cloned())
.collect::<Vec<_>>()
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
---
source: crates/ruff_linter/src/rules/pyupgrade/mod.rs
---
UP043.py:4:15: UP043 [*] Unnecessary default type arguments
|
4 | def func() -> Generator[int, None, None]:
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ UP043
5 | yield 42
|
= help: Remove default type arguments

Safe fix
1 1 | from typing import Generator, AsyncGenerator
2 2 |
3 3 |
4 |-def func() -> Generator[int, None, None]:
4 |+def func() -> Generator[int]:
5 5 | yield 42
6 6 |
7 7 |

UP043.py:8:15: UP043 [*] Unnecessary default type arguments
|
8 | def func() -> Generator[int, None]:
| ^^^^^^^^^^^^^^^^^^^^ UP043
9 | yield 42
|
= help: Remove default type arguments

Safe fix
5 5 | yield 42
6 6 |
7 7 |
8 |-def func() -> Generator[int, None]:
8 |+def func() -> Generator[int]:
9 9 | yield 42
10 10 |
11 11 |

UP043.py:21:15: UP043 [*] Unnecessary default type arguments
|
21 | def func() -> Generator[int, int, None]:
| ^^^^^^^^^^^^^^^^^^^^^^^^^ UP043
22 | _ = yield 42
23 | return None
|
= help: Remove default type arguments

Safe fix
18 18 | return foo
19 19 |
20 20 |
21 |-def func() -> Generator[int, int, None]:
21 |+def func() -> Generator[int, int]:
22 22 | _ = yield 42
23 23 | return None
24 24 |

UP043.py:31:21: UP043 [*] Unnecessary default type arguments
|
31 | async def func() -> AsyncGenerator[int, None]:
| ^^^^^^^^^^^^^^^^^^^^^^^^^ UP043
32 | yield 42
|
= help: Remove default type arguments

Safe fix
28 28 | return 42
29 29 |
30 30 |
31 |-async def func() -> AsyncGenerator[int, None]:
31 |+async def func() -> AsyncGenerator[int]:
32 32 | yield 42
33 33 |
34 34 |
1 change: 1 addition & 0 deletions ruff.schema.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 1df51b1

Please sign in to comment.