Skip to content

Commit d200296

Browse files
Avoid suggesting InitVar for __post_init__ that references PEP 695 type parameters (#23226)
Closes #19628.
1 parent 64084f8 commit d200296

3 files changed

Lines changed: 51 additions & 0 deletions

File tree

crates/ruff_linter/resources/test/fixtures/ruff/RUF033.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,3 +140,12 @@ class D:
140140
def __post_init__(self, x: int = """
141141
""") -> None:
142142
self.x = x
143+
144+
145+
# https://github.com/astral-sh/ruff/issues/19628
146+
# No fix: annotation references a type variable scoped to `__post_init__`
147+
@dataclass
148+
class E:
149+
def __post_init__[T: (str, bytes)](self, a: T | None = None, b: T | None = None) -> None:
150+
self.a = str(a)
151+
self.b = str(b)

crates/ruff_linter/src/rules/ruff/rules/post_init_default.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use anyhow::Context;
22

33
use ruff_macros::{ViolationMetadata, derive_message_formats};
44
use ruff_python_ast as ast;
5+
use ruff_python_ast::helpers::any_over_expr;
56
use ruff_python_ast::token::parenthesized_range;
67
use ruff_python_semantic::{Scope, ScopeKind};
78
use ruff_python_trivia::{indentation_at_offset, textwrap};
@@ -146,6 +147,21 @@ fn use_initvar(
146147
));
147148
}
148149

150+
// If the annotation references a type variable scoped to `__post_init__`
151+
// (PEP 695), moving it to the class body would produce a `NameError`.
152+
if let Some(annotation) = parameter.annotation() {
153+
if let Some(type_params) = &post_init_def.type_params {
154+
if any_over_expr(annotation, &|expr| {
155+
expr.as_name_expr()
156+
.is_some_and(|name| type_params.iter().any(|tp| tp.name().id == name.id))
157+
}) {
158+
return Err(anyhow::anyhow!(
159+
"Annotation references a type variable scoped to `__post_init__`"
160+
));
161+
}
162+
}
163+
}
164+
149165
// Ensure that `dataclasses.InitVar` is accessible. For example,
150166
// + `from dataclasses import InitVar`
151167
let (import_edit, initvar_binding) = checker.importer().get_or_import_symbol(

crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF033_RUF033.py.snap

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -489,4 +489,30 @@ help: Use `dataclasses.InitVar` instead
489489
141 + """
490490
142 + def __post_init__(self, x: int) -> None:
491491
143 | self.x = x
492+
144 |
493+
145 |
492494
note: This is an unsafe fix and may change runtime behavior
495+
496+
RUF033 `__post_init__` method with argument defaults
497+
--> RUF033.py:149:60
498+
|
499+
147 | @dataclass
500+
148 | class E:
501+
149 | def __post_init__[T: (str, bytes)](self, a: T | None = None, b: T | None = None) -> None:
502+
| ^^^^
503+
150 | self.a = str(a)
504+
151 | self.b = str(b)
505+
|
506+
help: Use `dataclasses.InitVar` instead
507+
508+
RUF033 `__post_init__` method with argument defaults
509+
--> RUF033.py:149:80
510+
|
511+
147 | @dataclass
512+
148 | class E:
513+
149 | def __post_init__[T: (str, bytes)](self, a: T | None = None, b: T | None = None) -> None:
514+
| ^^^^
515+
150 | self.a = str(a)
516+
151 | self.b = str(b)
517+
|
518+
help: Use `dataclasses.InitVar` instead

0 commit comments

Comments
 (0)