Skip to content

Commit d8e3fcc

Browse files
authored
[pyupgrade] Do not upgrade functional TypedDicts with private field names to the class-based syntax (UP013) (#16219)
1 parent 66a0467 commit d8e3fcc

File tree

3 files changed

+32
-3
lines changed

3 files changed

+32
-3
lines changed

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

+6
Original file line numberDiff line numberDiff line change
@@ -46,3 +46,9 @@
4646
X = TypedDict("X", {
4747
"some_config": int, # important
4848
})
49+
50+
# Private names should not be reported (OK)
51+
WithPrivate = TypedDict("WithPrivate", {"__x": int})
52+
53+
# Dunder names should not be reported (OK)
54+
WithDunder = TypedDict("WithDunder", {"__x__": int})

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

+21-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
use ruff_diagnostics::{Applicability, Diagnostic, Edit, Fix, FixAvailability, Violation};
22
use ruff_macros::{derive_message_formats, ViolationMetadata};
3-
use ruff_python_ast::helpers::is_dunder;
43
use ruff_python_ast::{self as ast, Arguments, Expr, ExprContext, Identifier, Keyword, Stmt};
54
use ruff_python_codegen::Generator;
65
use ruff_python_semantic::SemanticModel;
@@ -15,12 +14,22 @@ use crate::checkers::ast::Checker;
1514
/// Checks for `TypedDict` declarations that use functional syntax.
1615
///
1716
/// ## Why is this bad?
18-
/// `TypedDict` subclasses can be defined either through a functional syntax
17+
/// `TypedDict` types can be defined either through a functional syntax
1918
/// (`Foo = TypedDict(...)`) or a class syntax (`class Foo(TypedDict): ...`).
2019
///
2120
/// The class syntax is more readable and generally preferred over the
2221
/// functional syntax.
2322
///
23+
/// Nonetheless, there are some situations in which it is impossible to use
24+
/// the class-based syntax. This rule will not apply to those cases. Namely,
25+
/// it is impossible to use the class-based syntax if any `TypedDict` fields are:
26+
/// - Not valid [python identifiers] (for example, `@x`)
27+
/// - [Python keywords] such as `in`
28+
/// - [Private names] such as `__id` that would undergo [name mangling] at runtime
29+
/// if the class-based syntax was used
30+
/// - [Dunder names] such as `__int__` that can confuse type checkers if they're used
31+
/// with the class-based syntax.
32+
///
2433
/// ## Example
2534
/// ```python
2635
/// from typing import TypedDict
@@ -45,6 +54,12 @@ use crate::checkers::ast::Checker;
4554
///
4655
/// ## References
4756
/// - [Python documentation: `typing.TypedDict`](https://docs.python.org/3/library/typing.html#typing.TypedDict)
57+
///
58+
/// [Private names]: https://docs.python.org/3/tutorial/classes.html#private-variables
59+
/// [name mangling]: https://docs.python.org/3/reference/expressions.html#private-name-mangling
60+
/// [python identifiers]: https://docs.python.org/3/reference/lexical_analysis.html#identifiers
61+
/// [Python keywords]: https://docs.python.org/3/reference/lexical_analysis.html#keywords
62+
/// [Dunder names]: https://docs.python.org/3/reference/lexical_analysis.html#reserved-classes-of-identifiers
4863
#[derive(ViolationMetadata)]
4964
pub(crate) struct ConvertTypedDictFunctionalToClass {
5065
name: String,
@@ -185,7 +200,10 @@ fn fields_from_dict_literal(items: &[ast::DictItem]) -> Option<Vec<Stmt>> {
185200
if !is_identifier(field.to_str()) {
186201
return None;
187202
}
188-
if is_dunder(field.to_str()) {
203+
// Converting TypedDict to class-based syntax is not safe if fields contain
204+
// private or dunder names, because private names will be mangled and dunder
205+
// names can confuse type checkers.
206+
if field.to_str().starts_with("__") {
189207
return None;
190208
}
191209
Some(create_field_assignment_stmt(field.to_str(), value))

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

+5
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,8 @@ UP013.py:46:1: UP013 [*] Convert `X` from `TypedDict` functional to class syntax
264264
47 | | "some_config": int, # important
265265
48 | | })
266266
| |__^ UP013
267+
49 |
268+
50 | # Private names should not be reported (OK)
267269
|
268270
= help: Convert `X` to class syntax
269271

@@ -276,3 +278,6 @@ UP013.py:46:1: UP013 [*] Convert `X` from `TypedDict` functional to class syntax
276278
48 |-})
277279
46 |+class X(TypedDict):
278280
47 |+ some_config: int
281+
49 48 |
282+
50 49 | # Private names should not be reported (OK)
283+
51 50 | WithPrivate = TypedDict("WithPrivate", {"__x": int})

0 commit comments

Comments
 (0)