From d4f3396489443718461d79327e66171c01c2cf7d Mon Sep 17 00:00:00 2001 From: Bohdan Vanieiev Date: Mon, 1 Apr 2024 17:06:16 +0200 Subject: [PATCH 1/5] [`pyupgrade`] Replace str,Enum with StrEnum `UP042` Basically it closes https://github.com/astral-sh/ruff/discussions/3867 --- .../test/fixtures/pyupgrade/UP042.py | 15 +++ .../src/checkers/ast/analyze/statement.rs | 5 + crates/ruff_linter/src/codes.rs | 1 + crates/ruff_linter/src/fix/edits.rs | 6 + crates/ruff_linter/src/rules/pyupgrade/mod.rs | 1 + .../src/rules/pyupgrade/rules/mod.rs | 2 + .../rules/pyupgrade/rules/replace_str_enum.rs | 123 ++++++++++++++++++ ...er__rules__pyupgrade__tests__UP042.py.snap | 71 ++++++++++ ruff.schema.json | 1 + 9 files changed, 225 insertions(+) create mode 100644 crates/ruff_linter/resources/test/fixtures/pyupgrade/UP042.py create mode 100644 crates/ruff_linter/src/rules/pyupgrade/rules/replace_str_enum.rs create mode 100644 crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP042.py.snap diff --git a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP042.py b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP042.py new file mode 100644 index 0000000000000..db7a097457957 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP042.py @@ -0,0 +1,15 @@ +from enum import Enum + +class A(str, Enum): + ... + +class B(Enum, str): + ... + +class D(int, str, Enum): + ... + +class E(str, int, Enum): + ... + +# TODO: add more cases diff --git a/crates/ruff_linter/src/checkers/ast/analyze/statement.rs b/crates/ruff_linter/src/checkers/ast/analyze/statement.rs index 7e8293172916f..cc027dc208dc0 100644 --- a/crates/ruff_linter/src/checkers/ast/analyze/statement.rs +++ b/crates/ruff_linter/src/checkers/ast/analyze/statement.rs @@ -406,6 +406,11 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) { if checker.enabled(Rule::UselessObjectInheritance) { pyupgrade::rules::useless_object_inheritance(checker, class_def); } + if checker.enabled(Rule::ReplaceStrEnum) { + if checker.settings.target_version >= PythonVersion::Py311 { + pyupgrade::rules::replace_str_enum(checker, class_def); + } + } if checker.enabled(Rule::UnnecessaryClassParentheses) { pyupgrade::rules::unnecessary_class_parentheses(checker, class_def); } diff --git a/crates/ruff_linter/src/codes.rs b/crates/ruff_linter/src/codes.rs index a6db70d943302..210d7b56827f2 100644 --- a/crates/ruff_linter/src/codes.rs +++ b/crates/ruff_linter/src/codes.rs @@ -546,6 +546,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { (Pyupgrade, "039") => (RuleGroup::Stable, rules::pyupgrade::rules::UnnecessaryClassParentheses), (Pyupgrade, "040") => (RuleGroup::Stable, rules::pyupgrade::rules::NonPEP695TypeAlias), (Pyupgrade, "041") => (RuleGroup::Stable, rules::pyupgrade::rules::TimeoutErrorAlias), + (Pyupgrade, "042") => (RuleGroup::Stable, rules::pyupgrade::rules::ReplaceStrEnum), // pydocstyle (Pydocstyle, "100") => (RuleGroup::Stable, rules::pydocstyle::rules::UndocumentedPublicModule), diff --git a/crates/ruff_linter/src/fix/edits.rs b/crates/ruff_linter/src/fix/edits.rs index 0a70cc4e2327c..0a6086d78d43a 100644 --- a/crates/ruff_linter/src/fix/edits.rs +++ b/crates/ruff_linter/src/fix/edits.rs @@ -136,6 +136,12 @@ pub(crate) enum Parentheses { /// /// Supports the removal of parentheses when this is the only (kw)arg left. /// For this behavior, set `remove_parentheses` to `true`. +/// +/// NOTE: does not support multiple consecutive calls within a single [`Fix`]. +/// Because the first call deletes one arg, one arg remains (as expected), +/// the second call will return an [`Edit`] with positions on a locator which +/// has 2 args, so the second call will corrupt the source (likely, comma +/// and a space will remain). pub(crate) fn remove_argument( argument: &T, arguments: &Arguments, diff --git a/crates/ruff_linter/src/rules/pyupgrade/mod.rs b/crates/ruff_linter/src/rules/pyupgrade/mod.rs index 10fae94973062..427b448d765b1 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/mod.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/mod.rs @@ -61,6 +61,7 @@ mod tests { #[test_case(Rule::ReplaceUniversalNewlines, Path::new("UP021.py"))] #[test_case(Rule::SuperCallWithParameters, Path::new("UP008.py"))] #[test_case(Rule::TimeoutErrorAlias, Path::new("UP041.py"))] + #[test_case(Rule::ReplaceStrEnum, Path::new("UP042.py"))] #[test_case(Rule::TypeOfPrimitive, Path::new("UP003.py"))] #[test_case(Rule::TypingTextStrAlias, Path::new("UP019.py"))] #[test_case(Rule::UTF8EncodingDeclaration, Path::new("UP009_0.py"))] diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/mod.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/mod.rs index aa6a60732fbed..3b7928f6e9020 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/mod.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/mod.rs @@ -18,6 +18,7 @@ pub(crate) use printf_string_formatting::*; pub(crate) use quoted_annotation::*; pub(crate) use redundant_open_modes::*; pub(crate) use replace_stdout_stderr::*; +pub(crate) use replace_str_enum::*; pub(crate) use replace_universal_newlines::*; pub(crate) use super_call_with_parameters::*; pub(crate) use timeout_error_alias::*; @@ -58,6 +59,7 @@ mod printf_string_formatting; mod quoted_annotation; mod redundant_open_modes; mod replace_stdout_stderr; +mod replace_str_enum; mod replace_universal_newlines; mod super_call_with_parameters; mod timeout_error_alias; diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/replace_str_enum.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/replace_str_enum.rs new file mode 100644 index 0000000000000..d506db31882d2 --- /dev/null +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/replace_str_enum.rs @@ -0,0 +1,123 @@ +use crate::checkers::ast::Checker; +use crate::importer::ImportRequest; +use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; +use ruff_macros::{derive_message_formats, violation}; +use ruff_python_ast::{self as ast}; +use ruff_text_size::Ranged; + +/// ## What it does +/// Checks for classes that inherit from both `str` and `enum.Enum`. +/// +/// ## Why is this bad? +/// Since Python 3.11, `enum.StrEnum` exists and is preferred over +/// inheriting from `str` and `enum.Enum`. +/// +/// ## Example +/// ```python +/// class Foo(str, enum.Enum): +/// ... +/// ``` +/// +/// Use instead: +/// ```python +/// class Foo(enum.StrEnum): +/// ... +/// ``` +/// +/// ## References +/// - [enum.StrEnum](https://docs.python.org/3/library/enum.html#enum.StrEnum) + +#[violation] +pub struct ReplaceStrEnum { + name: String, + args_len: usize, +} + +impl Violation for ReplaceStrEnum { + const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes; + + #[derive_message_formats] + fn message(&self) -> String { + let ReplaceStrEnum { name, .. } = self; + format!( + "Class {name} inherits from both `str` and `enum.Enum`. Prefer `enum.StrEnum` instead." + ) + } + + fn fix_title(&self) -> Option { + let ReplaceStrEnum { args_len, .. } = self; + + if *args_len == 2 { + Some("Replace `str` and `enum.Enum` with `enum.StrEnum`".to_string()) + } else { + None + } + } +} + +/// UP042 +pub(crate) fn replace_str_enum(checker: &mut Checker, class_def: &ast::StmtClassDef) { + let Some(arguments) = class_def.arguments.as_deref() else { + // class does not inherit anything, exit early + return; + }; + + let mut inherits_str = false; + let mut inherits_enum = false; + for base in arguments.args.iter() { + if let Some(qualified_name) = checker.semantic().resolve_qualified_name(base) { + if matches!(qualified_name.segments(), ["", "str"]) { + inherits_str = true; + } else if matches!(qualified_name.segments(), ["enum", "Enum"]) { + inherits_enum = true; + } + } + + if inherits_str && inherits_enum { + // no need to check other inherited classes, we found both str & enum.Enum + break; + } + } + + if !inherits_str || !inherits_enum { + // exit early if class does not inherit both str and enum.Enum + return; + }; + + let mut diagnostic = Diagnostic::new( + ReplaceStrEnum { + name: class_def.name.to_string(), + args_len: arguments.len(), + }, + class_def.range(), + ); + + if arguments.len() == 2 { + // a fix is available only for classes that inherit exactly 2 arguments: str, Enum, + // because `remove_argument` cannot be called multiple times consecutively... + // for classes that inherit str, Enum and something else, generate a warning. + diagnostic.try_set_fix(|| { + let (import_edit, binding) = checker.importer().get_or_import_symbol( + &ImportRequest::import("enum", "StrEnum"), + class_def.start(), + checker.semantic(), + )?; + + // `binding` here is `StrEnum`. + + // class inherits exactly 2 arguments. + // replace all `(str, Enum)` arguments with `(StrEnum)`. + let fix = Fix::unsafe_edits( + import_edit, + [Edit::range_replacement( + format!("({binding})"), + arguments.range(), + )], + ); + + Ok(fix) + }); + } + + checker.diagnostics.push(diagnostic); +} diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP042.py.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP042.py.snap new file mode 100644 index 0000000000000..b40bd414b2d8e --- /dev/null +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP042.py.snap @@ -0,0 +1,71 @@ +--- +source: crates/ruff_linter/src/rules/pyupgrade/mod.rs +--- +UP042.py:3:1: UP042 [*] Class A inherits from both `str` and `enum.Enum`. Prefer `enum.StrEnum` instead. + | +1 | from enum import Enum +2 | +3 | / class A(str, Enum): +4 | | ... + | |_______^ UP042 +5 | +6 | class B(Enum, str): + | + = help: Replace `str` and `enum.Enum` with `enum.StrEnum` + +ℹ Unsafe fix +1 |-from enum import Enum + 1 |+from enum import Enum, StrEnum +2 2 | +3 |-class A(str, Enum): + 3 |+class A(StrEnum): +4 4 | ... +5 5 | +6 6 | class B(Enum, str): + +UP042.py:6:1: UP042 [*] Class B inherits from both `str` and `enum.Enum`. Prefer `enum.StrEnum` instead. + | +4 | ... +5 | +6 | / class B(Enum, str): +7 | | ... + | |_______^ UP042 +8 | +9 | class D(int, str, Enum): + | + = help: Replace `str` and `enum.Enum` with `enum.StrEnum` + +ℹ Unsafe fix +1 |-from enum import Enum + 1 |+from enum import Enum, StrEnum +2 2 | +3 3 | class A(str, Enum): +4 4 | ... +5 5 | +6 |-class B(Enum, str): + 6 |+class B(StrEnum): +7 7 | ... +8 8 | +9 9 | class D(int, str, Enum): + +UP042.py:9:1: UP042 Class D inherits from both `str` and `enum.Enum`. Prefer `enum.StrEnum` instead. + | + 7 | ... + 8 | + 9 | / class D(int, str, Enum): +10 | | ... + | |_______^ UP042 +11 | +12 | class E(str, int, Enum): + | + +UP042.py:12:1: UP042 Class E inherits from both `str` and `enum.Enum`. Prefer `enum.StrEnum` instead. + | +10 | ... +11 | +12 | / class E(str, int, Enum): +13 | | ... + | |_______^ UP042 +14 | +15 | # TODO: add more cases + | diff --git a/ruff.schema.json b/ruff.schema.json index 1c26154c2d19f..ec167eb8049fe 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -3864,6 +3864,7 @@ "UP04", "UP040", "UP041", + "UP042", "W", "W1", "W19", From 125d468bd6d797e58e4dd56a402db5ae0d0de38d Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Fri, 5 Apr 2024 21:40:42 -0400 Subject: [PATCH 2/5] Tweaks to documentation and error message --- .../test/fixtures/pyupgrade/UP042.py | 16 ++- crates/ruff_linter/src/fix/edits.rs | 6 - .../rules/pyupgrade/rules/replace_str_enum.rs | 104 ++++++++++++------ ...er__rules__pyupgrade__tests__UP042.py.snap | 72 +++++------- 4 files changed, 103 insertions(+), 95 deletions(-) diff --git a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP042.py b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP042.py index db7a097457957..392cf0ff5eee0 100644 --- a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP042.py +++ b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP042.py @@ -1,15 +1,13 @@ from enum import Enum -class A(str, Enum): - ... -class B(Enum, str): - ... +class A(str, Enum): ... -class D(int, str, Enum): - ... -class E(str, int, Enum): - ... +class B(Enum, str): ... -# TODO: add more cases + +class D(int, str, Enum): ... + + +class E(str, int, Enum): ... diff --git a/crates/ruff_linter/src/fix/edits.rs b/crates/ruff_linter/src/fix/edits.rs index 0a6086d78d43a..0a70cc4e2327c 100644 --- a/crates/ruff_linter/src/fix/edits.rs +++ b/crates/ruff_linter/src/fix/edits.rs @@ -136,12 +136,6 @@ pub(crate) enum Parentheses { /// /// Supports the removal of parentheses when this is the only (kw)arg left. /// For this behavior, set `remove_parentheses` to `true`. -/// -/// NOTE: does not support multiple consecutive calls within a single [`Fix`]. -/// Because the first call deletes one arg, one arg remains (as expected), -/// the second call will return an [`Edit`] with positions on a locator which -/// has 2 args, so the second call will corrupt the source (likely, comma -/// and a space will remain). pub(crate) fn remove_argument( argument: &T, arguments: &Arguments, diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/replace_str_enum.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/replace_str_enum.rs index d506db31882d2..d8b726423338d 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/replace_str_enum.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/replace_str_enum.rs @@ -1,36 +1,82 @@ -use crate::checkers::ast::Checker; -use crate::importer::ImportRequest; use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_ast::{self as ast}; +use ruff_python_ast as ast; +use ruff_python_ast::identifier::Identifier; use ruff_text_size::Ranged; +use crate::checkers::ast::Checker; +use crate::importer::ImportRequest; + /// ## What it does /// Checks for classes that inherit from both `str` and `enum.Enum`. /// /// ## Why is this bad? -/// Since Python 3.11, `enum.StrEnum` exists and is preferred over -/// inheriting from `str` and `enum.Enum`. +/// Python 3.11 introduced `enum.StrEnum`, which is preferred over inheriting +/// from both `str` and `enum.Enum`. /// /// ## Example /// ```python +/// import enum +/// +/// /// class Foo(str, enum.Enum): /// ... /// ``` /// /// Use instead: /// ```python +/// import enum +/// +/// /// class Foo(enum.StrEnum): /// ... /// ``` /// +/// ## Fix safety +/// Python 3.11 introduced a [breaking change] for enums that inherit from both +/// `str` and `enum.Enum`. Consider the following enum: +/// +/// ```python +/// from enum import Enum +/// +/// +/// class Foo(str, Enum): +/// BAR = "bar" +/// ``` +/// +/// In Python 3.11, the formatted representation of `Foo.BAR` changed as +/// follows: +/// +/// ```python +/// # Python 3.10 +/// f"{Foo.BAR}" # > bar +/// # Python 3.11 +/// f"{Foo.BAR}" # > Foo.BAR +/// ``` +/// +/// Migrating from `str` and `enum.Enum` to `enum.StrEnum` will restore the +/// previous behavior, such that: +/// +/// ```python +/// from enum import StrEnum +/// +/// class Foo(StrEnum): +/// BAR = "bar" +/// +/// f"{Foo.BAR}" # > bar +/// ``` +/// +/// As such, migrating to `enum.StrEnum` will introduce a behavior change for +/// code that relies on the Python 3.11 behavior. +/// /// ## References /// - [enum.StrEnum](https://docs.python.org/3/library/enum.html#enum.StrEnum) +/// +/// [breaking change]: https://blog.pecar.me/python-enum #[violation] pub struct ReplaceStrEnum { name: String, - args_len: usize, } impl Violation for ReplaceStrEnum { @@ -39,19 +85,11 @@ impl Violation for ReplaceStrEnum { #[derive_message_formats] fn message(&self) -> String { let ReplaceStrEnum { name, .. } = self; - format!( - "Class {name} inherits from both `str` and `enum.Enum`. Prefer `enum.StrEnum` instead." - ) + format!("Class {name} inherits from both `str` and `enum.Enum`") } fn fix_title(&self) -> Option { - let ReplaceStrEnum { args_len, .. } = self; - - if *args_len == 2 { - Some("Replace `str` and `enum.Enum` with `enum.StrEnum`".to_string()) - } else { - None - } + Some("Inherit from `enum.StrEnum`".to_string()) } } @@ -62,60 +100,54 @@ pub(crate) fn replace_str_enum(checker: &mut Checker, class_def: &ast::StmtClass return; }; + // Determine whether the class inherits from both `str` and `enum.Enum`. let mut inherits_str = false; let mut inherits_enum = false; for base in arguments.args.iter() { if let Some(qualified_name) = checker.semantic().resolve_qualified_name(base) { - if matches!(qualified_name.segments(), ["", "str"]) { - inherits_str = true; - } else if matches!(qualified_name.segments(), ["enum", "Enum"]) { - inherits_enum = true; + match qualified_name.segments() { + ["", "str"] => inherits_str = true, + ["enum", "Enum"] => inherits_enum = true, + _ => {} } } + // Short-circuit if both `str` and `enum.Enum` are found. if inherits_str && inherits_enum { - // no need to check other inherited classes, we found both str & enum.Enum break; } } + // If the class does not inherit both `str` and `enum.Enum`, exit early. if !inherits_str || !inherits_enum { - // exit early if class does not inherit both str and enum.Enum return; }; let mut diagnostic = Diagnostic::new( ReplaceStrEnum { name: class_def.name.to_string(), - args_len: arguments.len(), }, - class_def.range(), + class_def.identifier(), ); + // If the base classes are _exactly_ `str` and `enum.Enum`, apply a fix. + // TODO(charlie): As an alternative, we could remove both arguments, and replace one of the two + // with `StrEnum`. However, `remove_argument` can't be applied multiple times within a single + // fix; doing so leads to a syntax error. if arguments.len() == 2 { - // a fix is available only for classes that inherit exactly 2 arguments: str, Enum, - // because `remove_argument` cannot be called multiple times consecutively... - // for classes that inherit str, Enum and something else, generate a warning. diagnostic.try_set_fix(|| { let (import_edit, binding) = checker.importer().get_or_import_symbol( &ImportRequest::import("enum", "StrEnum"), class_def.start(), checker.semantic(), )?; - - // `binding` here is `StrEnum`. - - // class inherits exactly 2 arguments. - // replace all `(str, Enum)` arguments with `(StrEnum)`. - let fix = Fix::unsafe_edits( + Ok(Fix::unsafe_edits( import_edit, [Edit::range_replacement( format!("({binding})"), arguments.range(), )], - ); - - Ok(fix) + )) }); } diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP042.py.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP042.py.snap index b40bd414b2d8e..e7bff6e26969f 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP042.py.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP042.py.snap @@ -1,71 +1,55 @@ --- source: crates/ruff_linter/src/rules/pyupgrade/mod.rs --- -UP042.py:3:1: UP042 [*] Class A inherits from both `str` and `enum.Enum`. Prefer `enum.StrEnum` instead. +UP042.py:4:7: UP042 [*] Class A inherits from both `str` and `enum.Enum` | -1 | from enum import Enum -2 | -3 | / class A(str, Enum): -4 | | ... - | |_______^ UP042 -5 | -6 | class B(Enum, str): +4 | class A(str, Enum): ... + | ^ UP042 | - = help: Replace `str` and `enum.Enum` with `enum.StrEnum` + = help: Inherit from `enum.StrEnum` ℹ Unsafe fix 1 |-from enum import Enum 1 |+from enum import Enum, StrEnum 2 2 | -3 |-class A(str, Enum): - 3 |+class A(StrEnum): -4 4 | ... +3 3 | +4 |-class A(str, Enum): ... + 4 |+class A(StrEnum): ... 5 5 | -6 6 | class B(Enum, str): +6 6 | +7 7 | class B(Enum, str): ... -UP042.py:6:1: UP042 [*] Class B inherits from both `str` and `enum.Enum`. Prefer `enum.StrEnum` instead. +UP042.py:7:7: UP042 [*] Class B inherits from both `str` and `enum.Enum` | -4 | ... -5 | -6 | / class B(Enum, str): -7 | | ... - | |_______^ UP042 -8 | -9 | class D(int, str, Enum): +7 | class B(Enum, str): ... + | ^ UP042 | - = help: Replace `str` and `enum.Enum` with `enum.StrEnum` + = help: Inherit from `enum.StrEnum` ℹ Unsafe fix 1 |-from enum import Enum 1 |+from enum import Enum, StrEnum 2 2 | -3 3 | class A(str, Enum): -4 4 | ... +3 3 | +4 4 | class A(str, Enum): ... 5 5 | -6 |-class B(Enum, str): - 6 |+class B(StrEnum): -7 7 | ... +6 6 | +7 |-class B(Enum, str): ... + 7 |+class B(StrEnum): ... 8 8 | -9 9 | class D(int, str, Enum): +9 9 | +10 10 | class D(int, str, Enum): ... -UP042.py:9:1: UP042 Class D inherits from both `str` and `enum.Enum`. Prefer `enum.StrEnum` instead. +UP042.py:10:7: UP042 Class D inherits from both `str` and `enum.Enum` | - 7 | ... - 8 | - 9 | / class D(int, str, Enum): -10 | | ... - | |_______^ UP042 -11 | -12 | class E(str, int, Enum): +10 | class D(int, str, Enum): ... + | ^ UP042 | + = help: Inherit from `enum.StrEnum` -UP042.py:12:1: UP042 Class E inherits from both `str` and `enum.Enum`. Prefer `enum.StrEnum` instead. +UP042.py:13:7: UP042 Class E inherits from both `str` and `enum.Enum` | -10 | ... -11 | -12 | / class E(str, int, Enum): -13 | | ... - | |_______^ UP042 -14 | -15 | # TODO: add more cases +13 | class E(str, int, Enum): ... + | ^ UP042 | + = help: Inherit from `enum.StrEnum` From 47149130a8cbf9960c42e6abb6a84197e8e7a12e Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Fri, 5 Apr 2024 21:41:34 -0400 Subject: [PATCH 3/5] Move to preview --- crates/ruff_linter/src/codes.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/ruff_linter/src/codes.rs b/crates/ruff_linter/src/codes.rs index cb9d672220b14..eb8553d155817 100644 --- a/crates/ruff_linter/src/codes.rs +++ b/crates/ruff_linter/src/codes.rs @@ -547,7 +547,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { (Pyupgrade, "039") => (RuleGroup::Stable, rules::pyupgrade::rules::UnnecessaryClassParentheses), (Pyupgrade, "040") => (RuleGroup::Stable, rules::pyupgrade::rules::NonPEP695TypeAlias), (Pyupgrade, "041") => (RuleGroup::Stable, rules::pyupgrade::rules::TimeoutErrorAlias), - (Pyupgrade, "042") => (RuleGroup::Stable, rules::pyupgrade::rules::ReplaceStrEnum), + (Pyupgrade, "042") => (RuleGroup::Preview, rules::pyupgrade::rules::ReplaceStrEnum), // pydocstyle (Pydocstyle, "100") => (RuleGroup::Stable, rules::pydocstyle::rules::UndocumentedPublicModule), From cee7d1b74e310462e955e896a2538d2ad86a10eb Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Fri, 5 Apr 2024 21:45:15 -0400 Subject: [PATCH 4/5] Fix clippy --- .../ruff_linter/src/rules/pyupgrade/rules/replace_str_enum.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/replace_str_enum.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/replace_str_enum.rs index d8b726423338d..d27952e5d5d5c 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/replace_str_enum.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/replace_str_enum.rs @@ -84,7 +84,7 @@ impl Violation for ReplaceStrEnum { #[derive_message_formats] fn message(&self) -> String { - let ReplaceStrEnum { name, .. } = self; + let ReplaceStrEnum { name } = self; format!("Class {name} inherits from both `str` and `enum.Enum`") } From d677f25671836d6141fbdc56c782bfd1f4226381 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Fri, 5 Apr 2024 21:46:11 -0400 Subject: [PATCH 5/5] Fix doc formatting --- .../src/rules/pyupgrade/rules/replace_str_enum.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/replace_str_enum.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/replace_str_enum.rs index d27952e5d5d5c..4717ce2a5347f 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/replace_str_enum.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/replace_str_enum.rs @@ -15,6 +15,7 @@ use crate::importer::ImportRequest; /// from both `str` and `enum.Enum`. /// /// ## Example +/// /// ```python /// import enum /// @@ -24,6 +25,7 @@ use crate::importer::ImportRequest; /// ``` /// /// Use instead: +/// /// ```python /// import enum /// @@ -33,6 +35,7 @@ use crate::importer::ImportRequest; /// ``` /// /// ## Fix safety +/// /// Python 3.11 introduced a [breaking change] for enums that inherit from both /// `str` and `enum.Enum`. Consider the following enum: /// @@ -60,9 +63,11 @@ use crate::importer::ImportRequest; /// ```python /// from enum import StrEnum /// +/// /// class Foo(StrEnum): /// BAR = "bar" /// +/// /// f"{Foo.BAR}" # > bar /// ``` ///