mirror of https://github.com/astral-sh/ruff
[`pyupgrade`] Do not upgrade functional TypedDicts with private field names to the class-based syntax (`UP013`) (#16219)
This commit is contained in:
parent
66a0467305
commit
d8e3fcca97
|
|
@ -46,3 +46,9 @@ MyType = TypedDict("MyType", dict())
|
||||||
X = TypedDict("X", {
|
X = TypedDict("X", {
|
||||||
"some_config": int, # important
|
"some_config": int, # important
|
||||||
})
|
})
|
||||||
|
|
||||||
|
# Private names should not be reported (OK)
|
||||||
|
WithPrivate = TypedDict("WithPrivate", {"__x": int})
|
||||||
|
|
||||||
|
# Dunder names should not be reported (OK)
|
||||||
|
WithDunder = TypedDict("WithDunder", {"__x__": int})
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
use ruff_diagnostics::{Applicability, Diagnostic, Edit, Fix, FixAvailability, Violation};
|
use ruff_diagnostics::{Applicability, Diagnostic, Edit, Fix, FixAvailability, Violation};
|
||||||
use ruff_macros::{derive_message_formats, ViolationMetadata};
|
use ruff_macros::{derive_message_formats, ViolationMetadata};
|
||||||
use ruff_python_ast::helpers::is_dunder;
|
|
||||||
use ruff_python_ast::{self as ast, Arguments, Expr, ExprContext, Identifier, Keyword, Stmt};
|
use ruff_python_ast::{self as ast, Arguments, Expr, ExprContext, Identifier, Keyword, Stmt};
|
||||||
use ruff_python_codegen::Generator;
|
use ruff_python_codegen::Generator;
|
||||||
use ruff_python_semantic::SemanticModel;
|
use ruff_python_semantic::SemanticModel;
|
||||||
|
|
@ -15,12 +14,22 @@ use crate::checkers::ast::Checker;
|
||||||
/// Checks for `TypedDict` declarations that use functional syntax.
|
/// Checks for `TypedDict` declarations that use functional syntax.
|
||||||
///
|
///
|
||||||
/// ## Why is this bad?
|
/// ## Why is this bad?
|
||||||
/// `TypedDict` subclasses can be defined either through a functional syntax
|
/// `TypedDict` types can be defined either through a functional syntax
|
||||||
/// (`Foo = TypedDict(...)`) or a class syntax (`class Foo(TypedDict): ...`).
|
/// (`Foo = TypedDict(...)`) or a class syntax (`class Foo(TypedDict): ...`).
|
||||||
///
|
///
|
||||||
/// The class syntax is more readable and generally preferred over the
|
/// The class syntax is more readable and generally preferred over the
|
||||||
/// functional syntax.
|
/// functional syntax.
|
||||||
///
|
///
|
||||||
|
/// Nonetheless, there are some situations in which it is impossible to use
|
||||||
|
/// the class-based syntax. This rule will not apply to those cases. Namely,
|
||||||
|
/// it is impossible to use the class-based syntax if any `TypedDict` fields are:
|
||||||
|
/// - Not valid [python identifiers] (for example, `@x`)
|
||||||
|
/// - [Python keywords] such as `in`
|
||||||
|
/// - [Private names] such as `__id` that would undergo [name mangling] at runtime
|
||||||
|
/// if the class-based syntax was used
|
||||||
|
/// - [Dunder names] such as `__int__` that can confuse type checkers if they're used
|
||||||
|
/// with the class-based syntax.
|
||||||
|
///
|
||||||
/// ## Example
|
/// ## Example
|
||||||
/// ```python
|
/// ```python
|
||||||
/// from typing import TypedDict
|
/// from typing import TypedDict
|
||||||
|
|
@ -45,6 +54,12 @@ use crate::checkers::ast::Checker;
|
||||||
///
|
///
|
||||||
/// ## References
|
/// ## References
|
||||||
/// - [Python documentation: `typing.TypedDict`](https://docs.python.org/3/library/typing.html#typing.TypedDict)
|
/// - [Python documentation: `typing.TypedDict`](https://docs.python.org/3/library/typing.html#typing.TypedDict)
|
||||||
|
///
|
||||||
|
/// [Private names]: https://docs.python.org/3/tutorial/classes.html#private-variables
|
||||||
|
/// [name mangling]: https://docs.python.org/3/reference/expressions.html#private-name-mangling
|
||||||
|
/// [python identifiers]: https://docs.python.org/3/reference/lexical_analysis.html#identifiers
|
||||||
|
/// [Python keywords]: https://docs.python.org/3/reference/lexical_analysis.html#keywords
|
||||||
|
/// [Dunder names]: https://docs.python.org/3/reference/lexical_analysis.html#reserved-classes-of-identifiers
|
||||||
#[derive(ViolationMetadata)]
|
#[derive(ViolationMetadata)]
|
||||||
pub(crate) struct ConvertTypedDictFunctionalToClass {
|
pub(crate) struct ConvertTypedDictFunctionalToClass {
|
||||||
name: String,
|
name: String,
|
||||||
|
|
@ -185,7 +200,10 @@ fn fields_from_dict_literal(items: &[ast::DictItem]) -> Option<Vec<Stmt>> {
|
||||||
if !is_identifier(field.to_str()) {
|
if !is_identifier(field.to_str()) {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
if is_dunder(field.to_str()) {
|
// Converting TypedDict to class-based syntax is not safe if fields contain
|
||||||
|
// private or dunder names, because private names will be mangled and dunder
|
||||||
|
// names can confuse type checkers.
|
||||||
|
if field.to_str().starts_with("__") {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
Some(create_field_assignment_stmt(field.to_str(), value))
|
Some(create_field_assignment_stmt(field.to_str(), value))
|
||||||
|
|
|
||||||
|
|
@ -264,6 +264,8 @@ UP013.py:46:1: UP013 [*] Convert `X` from `TypedDict` functional to class syntax
|
||||||
47 | | "some_config": int, # important
|
47 | | "some_config": int, # important
|
||||||
48 | | })
|
48 | | })
|
||||||
| |__^ UP013
|
| |__^ UP013
|
||||||
|
49 |
|
||||||
|
50 | # Private names should not be reported (OK)
|
||||||
|
|
|
|
||||||
= help: Convert `X` to class syntax
|
= help: Convert `X` to class syntax
|
||||||
|
|
||||||
|
|
@ -276,3 +278,6 @@ UP013.py:46:1: UP013 [*] Convert `X` from `TypedDict` functional to class syntax
|
||||||
48 |-})
|
48 |-})
|
||||||
46 |+class X(TypedDict):
|
46 |+class X(TypedDict):
|
||||||
47 |+ some_config: int
|
47 |+ some_config: int
|
||||||
|
49 48 |
|
||||||
|
50 49 | # Private names should not be reported (OK)
|
||||||
|
51 50 | WithPrivate = TypedDict("WithPrivate", {"__x": int})
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue