[`pyupgrade`] Extend `UP019` to detect `typing_extensions.Text` (`UP019`) (#20825)

Co-authored-by: Micha Reiser <micha@reiser.io>
This commit is contained in:
Dan Parizher 2025-10-15 02:52:14 -04:00 committed by GitHub
parent abf685b030
commit 9e1aafd0ce
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 183 additions and 9 deletions

View File

@ -18,3 +18,20 @@ def print_third_word(word: Hello.Text) -> None:
def print_fourth_word(word: Goodbye) -> None: def print_fourth_word(word: Goodbye) -> None:
print(word) print(word)
import typing_extensions
import typing_extensions as TypingExt
from typing_extensions import Text as TextAlias
def print_fifth_word(word: typing_extensions.Text) -> None:
print(word)
def print_sixth_word(word: TypingExt.Text) -> None:
print(word)
def print_seventh_word(word: TextAlias) -> None:
print(word)

View File

@ -265,3 +265,7 @@ pub(crate) const fn is_fix_read_whole_file_enabled(settings: &LinterSettings) ->
pub(crate) const fn is_fix_write_whole_file_enabled(settings: &LinterSettings) -> bool { pub(crate) const fn is_fix_write_whole_file_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled() settings.preview.is_enabled()
} }
pub(crate) const fn is_typing_extensions_str_alias_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()
}

View File

@ -126,6 +126,7 @@ mod tests {
} }
#[test_case(Rule::SuperCallWithParameters, Path::new("UP008.py"))] #[test_case(Rule::SuperCallWithParameters, Path::new("UP008.py"))]
#[test_case(Rule::TypingTextStrAlias, Path::new("UP019.py"))]
fn rules_preview(rule_code: Rule, path: &Path) -> Result<()> { fn rules_preview(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("{}__preview", path.to_string_lossy()); let snapshot = format!("{}__preview", path.to_string_lossy());
let diagnostics = test_path( let diagnostics = test_path(

View File

@ -1,15 +1,19 @@
use ruff_python_ast::Expr; use ruff_python_ast::Expr;
use std::fmt::{Display, Formatter};
use ruff_macros::{ViolationMetadata, derive_message_formats}; use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_semantic::Modules; use ruff_python_semantic::Modules;
use ruff_text_size::Ranged; use ruff_text_size::Ranged;
use crate::checkers::ast::Checker; use crate::checkers::ast::Checker;
use crate::preview::is_typing_extensions_str_alias_enabled;
use crate::{Edit, Fix, FixAvailability, Violation}; use crate::{Edit, Fix, FixAvailability, Violation};
/// ## What it does /// ## What it does
/// Checks for uses of `typing.Text`. /// Checks for uses of `typing.Text`.
/// ///
/// In preview mode, also checks for `typing_extensions.Text`.
///
/// ## Why is this bad? /// ## Why is this bad?
/// `typing.Text` is an alias for `str`, and only exists for Python 2 /// `typing.Text` is an alias for `str`, and only exists for Python 2
/// compatibility. As of Python 3.11, `typing.Text` is deprecated. Use `str` /// compatibility. As of Python 3.11, `typing.Text` is deprecated. Use `str`
@ -30,14 +34,16 @@ use crate::{Edit, Fix, FixAvailability, Violation};
/// ## References /// ## References
/// - [Python documentation: `typing.Text`](https://docs.python.org/3/library/typing.html#typing.Text) /// - [Python documentation: `typing.Text`](https://docs.python.org/3/library/typing.html#typing.Text)
#[derive(ViolationMetadata)] #[derive(ViolationMetadata)]
pub(crate) struct TypingTextStrAlias; pub(crate) struct TypingTextStrAlias {
module: TypingModule,
}
impl Violation for TypingTextStrAlias { impl Violation for TypingTextStrAlias {
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes; const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
#[derive_message_formats] #[derive_message_formats]
fn message(&self) -> String { fn message(&self) -> String {
"`typing.Text` is deprecated, use `str`".to_string() format!("`{}.Text` is deprecated, use `str`", self.module)
} }
fn fix_title(&self) -> Option<String> { fn fix_title(&self) -> Option<String> {
@ -47,16 +53,26 @@ impl Violation for TypingTextStrAlias {
/// UP019 /// UP019
pub(crate) fn typing_text_str_alias(checker: &Checker, expr: &Expr) { pub(crate) fn typing_text_str_alias(checker: &Checker, expr: &Expr) {
if !checker.semantic().seen_module(Modules::TYPING) { if !checker
.semantic()
.seen_module(Modules::TYPING | Modules::TYPING_EXTENSIONS)
{
return; return;
} }
if checker if let Some(qualified_name) = checker.semantic().resolve_qualified_name(expr) {
.semantic() let segments = qualified_name.segments();
.resolve_qualified_name(expr) let module = match segments {
.is_some_and(|qualified_name| matches!(qualified_name.segments(), ["typing", "Text"])) ["typing", "Text"] => TypingModule::Typing,
{ ["typing_extensions", "Text"]
let mut diagnostic = checker.report_diagnostic(TypingTextStrAlias, expr.range()); if is_typing_extensions_str_alias_enabled(checker.settings()) =>
{
TypingModule::TypingExtensions
}
_ => return,
};
let mut diagnostic = checker.report_diagnostic(TypingTextStrAlias { module }, expr.range());
diagnostic.add_primary_tag(ruff_db::diagnostic::DiagnosticTag::Deprecated); diagnostic.add_primary_tag(ruff_db::diagnostic::DiagnosticTag::Deprecated);
diagnostic.try_set_fix(|| { diagnostic.try_set_fix(|| {
let (import_edit, binding) = checker.importer().get_or_import_builtin_symbol( let (import_edit, binding) = checker.importer().get_or_import_builtin_symbol(
@ -71,3 +87,18 @@ pub(crate) fn typing_text_str_alias(checker: &Checker, expr: &Expr) {
}); });
} }
} }
#[derive(Copy, Clone, Debug)]
enum TypingModule {
Typing,
TypingExtensions,
}
impl Display for TypingModule {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
TypingModule::Typing => f.write_str("typing"),
TypingModule::TypingExtensions => f.write_str("typing_extensions"),
}
}
}

View File

@ -66,3 +66,5 @@ help: Replace with `str`
- def print_fourth_word(word: Goodbye) -> None: - def print_fourth_word(word: Goodbye) -> None:
19 + def print_fourth_word(word: str) -> None: 19 + def print_fourth_word(word: str) -> None:
20 | print(word) 20 | print(word)
21 |
22 |

View File

@ -0,0 +1,119 @@
---
source: crates/ruff_linter/src/rules/pyupgrade/mod.rs
---
UP019 [*] `typing.Text` is deprecated, use `str`
--> UP019.py:7:22
|
7 | def print_word(word: Text) -> None:
| ^^^^
8 | print(word)
|
help: Replace with `str`
4 | from typing import Text as Goodbye
5 |
6 |
- def print_word(word: Text) -> None:
7 + def print_word(word: str) -> None:
8 | print(word)
9 |
10 |
UP019 [*] `typing.Text` is deprecated, use `str`
--> UP019.py:11:29
|
11 | def print_second_word(word: typing.Text) -> None:
| ^^^^^^^^^^^
12 | print(word)
|
help: Replace with `str`
8 | print(word)
9 |
10 |
- def print_second_word(word: typing.Text) -> None:
11 + def print_second_word(word: str) -> None:
12 | print(word)
13 |
14 |
UP019 [*] `typing.Text` is deprecated, use `str`
--> UP019.py:15:28
|
15 | def print_third_word(word: Hello.Text) -> None:
| ^^^^^^^^^^
16 | print(word)
|
help: Replace with `str`
12 | print(word)
13 |
14 |
- def print_third_word(word: Hello.Text) -> None:
15 + def print_third_word(word: str) -> None:
16 | print(word)
17 |
18 |
UP019 [*] `typing.Text` is deprecated, use `str`
--> UP019.py:19:29
|
19 | def print_fourth_word(word: Goodbye) -> None:
| ^^^^^^^
20 | print(word)
|
help: Replace with `str`
16 | print(word)
17 |
18 |
- def print_fourth_word(word: Goodbye) -> None:
19 + def print_fourth_word(word: str) -> None:
20 | print(word)
21 |
22 |
UP019 [*] `typing_extensions.Text` is deprecated, use `str`
--> UP019.py:28:28
|
28 | def print_fifth_word(word: typing_extensions.Text) -> None:
| ^^^^^^^^^^^^^^^^^^^^^^
29 | print(word)
|
help: Replace with `str`
25 | from typing_extensions import Text as TextAlias
26 |
27 |
- def print_fifth_word(word: typing_extensions.Text) -> None:
28 + def print_fifth_word(word: str) -> None:
29 | print(word)
30 |
31 |
UP019 [*] `typing_extensions.Text` is deprecated, use `str`
--> UP019.py:32:28
|
32 | def print_sixth_word(word: TypingExt.Text) -> None:
| ^^^^^^^^^^^^^^
33 | print(word)
|
help: Replace with `str`
29 | print(word)
30 |
31 |
- def print_sixth_word(word: TypingExt.Text) -> None:
32 + def print_sixth_word(word: str) -> None:
33 | print(word)
34 |
35 |
UP019 [*] `typing_extensions.Text` is deprecated, use `str`
--> UP019.py:36:30
|
36 | def print_seventh_word(word: TextAlias) -> None:
| ^^^^^^^^^
37 | print(word)
|
help: Replace with `str`
33 | print(word)
34 |
35 |
- def print_seventh_word(word: TextAlias) -> None:
36 + def print_seventh_word(word: str) -> None:
37 | print(word)