mirror of https://github.com/astral-sh/ruff
[`flake8-type-checking`] Fix `TC003` false positive with `future-annotations` (#21125)
Summary -- Fixes #21121 by upgrading `RuntimeEvaluated` annotations like `dataclasses.KW_ONLY` to `RuntimeRequired`. We already had special handling for `TypingOnly` annotations in this context but not `RuntimeEvaluated`. Combining that with the `future-annotations` setting, which allowed ignoring the `RuntimeEvaluated` flag, led to the reported bug where we would try to move `KW_ONLY` into a `TYPE_CHECKING` block. Test Plan -- A new test based on the issue
This commit is contained in:
parent
9bacd19c5a
commit
1c7ea690a8
|
|
@ -14,3 +14,14 @@ def f():
|
||||||
import os
|
import os
|
||||||
|
|
||||||
print(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
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
@ -1400,6 +1400,14 @@ impl<'a> Visitor<'a> for Checker<'a> {
|
||||||
AnnotationContext::RuntimeRequired => {
|
AnnotationContext::RuntimeRequired => {
|
||||||
self.visit_runtime_required_annotation(annotation);
|
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 => {
|
AnnotationContext::RuntimeEvaluated => {
|
||||||
self.visit_runtime_evaluated_annotation(annotation);
|
self.visit_runtime_evaluated_annotation(annotation);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -98,6 +98,26 @@ mod tests {
|
||||||
Ok(())
|
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
|
// 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.
|
// so we want to make sure their fixes are not going around in circles.
|
||||||
#[test_case(Rule::UnquotedTypeAlias, Path::new("TC007.py"))]
|
#[test_case(Rule::UnquotedTypeAlias, Path::new("TC007.py"))]
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -64,6 +64,7 @@ mod tests {
|
||||||
#[test_case(Rule::QuotedAnnotation, Path::new("UP037_0.py"))]
|
#[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_1.py"))]
|
||||||
#[test_case(Rule::QuotedAnnotation, Path::new("UP037_2.pyi"))]
|
#[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.py"))]
|
||||||
#[test_case(Rule::RedundantOpenModes, Path::new("UP015_1.py"))]
|
#[test_case(Rule::RedundantOpenModes, Path::new("UP015_1.py"))]
|
||||||
#[test_case(Rule::ReplaceStdoutStderr, Path::new("UP022.py"))]
|
#[test_case(Rule::ReplaceStdoutStderr, Path::new("UP022.py"))]
|
||||||
|
|
@ -156,6 +157,20 @@ mod tests {
|
||||||
Ok(())
|
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.py"))]
|
||||||
#[test_case(Rule::NonPEP695TypeAlias, Path::new("UP040.pyi"))]
|
#[test_case(Rule::NonPEP695TypeAlias, Path::new("UP040.pyi"))]
|
||||||
#[test_case(Rule::NonPEP695GenericClass, Path::new("UP046_0.py"))]
|
#[test_case(Rule::NonPEP695GenericClass, Path::new("UP046_0.py"))]
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
---
|
||||||
|
source: crates/ruff_linter/src/rules/pyupgrade/mod.rs
|
||||||
|
---
|
||||||
|
|
||||||
Loading…
Reference in New Issue