Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,14 @@ def f():
import os

print(os)


# regression test for https://github.com/astral-sh/ruff/issues/21121
from dataclasses import KW_ONLY, dataclass


@dataclass
class DataClass:
a: int
_: KW_ONLY # should be an exception to TC003, even with future-annotations
b: int
17 changes: 17 additions & 0 deletions crates/ruff_linter/resources/test/fixtures/pyupgrade/UP037_3.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
"""
Regression test for an ecosystem hit on
https://github.com/astral-sh/ruff/pull/21125.

We should mark all of the components of special dataclass annotations as
runtime-required, not just the first layer.
"""

from dataclasses import dataclass
from typing import ClassVar, Optional


@dataclass(frozen=True)
class EmptyCell:
_singleton: ClassVar[Optional["EmptyCell"]] = None
# the behavior of _singleton above should match a non-ClassVar
_doubleton: "EmptyCell"
8 changes: 8 additions & 0 deletions crates/ruff_linter/src/checkers/ast/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1400,6 +1400,14 @@ impl<'a> Visitor<'a> for Checker<'a> {
AnnotationContext::RuntimeRequired => {
self.visit_runtime_required_annotation(annotation);
}
AnnotationContext::RuntimeEvaluated
if flake8_type_checking::helpers::is_dataclass_meta_annotation(
annotation,
self.semantic(),
) =>
{
self.visit_runtime_required_annotation(annotation);
}
AnnotationContext::RuntimeEvaluated => {
self.visit_runtime_evaluated_annotation(annotation);
}
Expand Down
20 changes: 20 additions & 0 deletions crates/ruff_linter/src/rules/flake8_type_checking/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,26 @@ mod tests {
Ok(())
}

#[test_case(Rule::TypingOnlyStandardLibraryImport, Path::new("TC003.py"))]
fn add_future_import_dataclass_kw_only_py313(rule: Rule, path: &Path) -> Result<()> {
let snapshot = format!(
"add_future_import_kw_only__{}_{}",
rule.noqa_code(),
path.to_string_lossy()
);
let diagnostics = test_path(
Path::new("flake8_type_checking").join(path).as_path(),
&settings::LinterSettings {
future_annotations: true,
// The issue in #21121 also didn't trigger on Python 3.14
unresolved_target_version: PythonVersion::PY313.into(),
..settings::LinterSettings::for_rule(rule)
},
)?;
assert_diagnostics!(snapshot, diagnostics);
Ok(())
}

// we test these rules as a pair, since they're opposites of one another
// so we want to make sure their fixes are not going around in circles.
#[test_case(Rule::UnquotedTypeAlias, Path::new("TC007.py"))]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
---
source: crates/ruff_linter/src/rules/flake8_type_checking/mod.rs
---
TC003 [*] Move standard library import `os` into a type-checking block
--> TC003.py:8:12
|
7 | def f():
8 | import os
| ^^
9 |
10 | x: os
|
help: Move into type-checking block
2 |
3 | For typing-only import detection tests, see `TC002.py`.
4 | """
5 + from typing import TYPE_CHECKING
6 +
7 + if TYPE_CHECKING:
8 + import os
9 |
10 |
11 | def f():
- import os
12 |
13 | x: os
14 |
note: This is an unsafe fix and may change runtime behavior
15 changes: 15 additions & 0 deletions crates/ruff_linter/src/rules/pyupgrade/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ mod tests {
#[test_case(Rule::QuotedAnnotation, Path::new("UP037_0.py"))]
#[test_case(Rule::QuotedAnnotation, Path::new("UP037_1.py"))]
#[test_case(Rule::QuotedAnnotation, Path::new("UP037_2.pyi"))]
#[test_case(Rule::QuotedAnnotation, Path::new("UP037_3.py"))]
#[test_case(Rule::RedundantOpenModes, Path::new("UP015.py"))]
#[test_case(Rule::RedundantOpenModes, Path::new("UP015_1.py"))]
#[test_case(Rule::ReplaceStdoutStderr, Path::new("UP022.py"))]
Expand Down Expand Up @@ -140,6 +141,20 @@ mod tests {
Ok(())
}

#[test_case(Rule::QuotedAnnotation, Path::new("UP037_3.py"))]
fn rules_py313(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("rules_py313__{}", path.to_string_lossy());
let diagnostics = test_path(
Path::new("pyupgrade").join(path).as_path(),
&settings::LinterSettings {
unresolved_target_version: PythonVersion::PY313.into(),
..settings::LinterSettings::for_rule(rule_code)
},
)?;
assert_diagnostics!(snapshot, diagnostics);
Ok(())
}

#[test_case(Rule::NonPEP695TypeAlias, Path::new("UP040.py"))]
#[test_case(Rule::NonPEP695TypeAlias, Path::new("UP040.pyi"))]
#[test_case(Rule::NonPEP695GenericClass, Path::new("UP046_0.py"))]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
---
source: crates/ruff_linter/src/rules/pyupgrade/mod.rs
---
UP037 [*] Remove quotes from type annotation
--> UP037_3.py:15:35
|
13 | @dataclass(frozen=True)
14 | class EmptyCell:
15 | _singleton: ClassVar[Optional["EmptyCell"]] = None
| ^^^^^^^^^^^
16 | # the behavior of _singleton above should match a non-ClassVar
17 | _doubleton: "EmptyCell"
|
help: Remove quotes
12 |
13 | @dataclass(frozen=True)
14 | class EmptyCell:
- _singleton: ClassVar[Optional["EmptyCell"]] = None
15 + _singleton: ClassVar[Optional[EmptyCell]] = None
16 | # the behavior of _singleton above should match a non-ClassVar
17 | _doubleton: "EmptyCell"

UP037 [*] Remove quotes from type annotation
--> UP037_3.py:17:17
|
15 | _singleton: ClassVar[Optional["EmptyCell"]] = None
16 | # the behavior of _singleton above should match a non-ClassVar
17 | _doubleton: "EmptyCell"
| ^^^^^^^^^^^
|
help: Remove quotes
14 | class EmptyCell:
15 | _singleton: ClassVar[Optional["EmptyCell"]] = None
16 | # the behavior of _singleton above should match a non-ClassVar
- _doubleton: "EmptyCell"
17 + _doubleton: EmptyCell
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
source: crates/ruff_linter/src/rules/pyupgrade/mod.rs
---