Skip to content

Commit

Permalink
Avoid marking InitVar as typing-only
Browse files Browse the repository at this point in the history
  • Loading branch information
charliermarsh committed Jan 29, 2024
1 parent 4ccbacd commit a52afe7
Show file tree
Hide file tree
Showing 7 changed files with 91 additions and 3 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
"""Test: avoid marking an `InitVar` as typing-only."""

from __future__ import annotations

from dataclasses import FrozenInstanceError, InitVar, dataclass


@dataclass
class C:
i: int
j: int = None
database: InitVar[int] = None

err: FrozenInstanceError = None

def __post_init__(self, database):
...
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 @@ -721,6 +721,14 @@ where
AnnotationContext::RuntimeEvaluated => {
self.visit_runtime_evaluated_annotation(annotation);
}
AnnotationContext::TypingOnly
if flake8_type_checking::helpers::is_dataclass_meta_annotation(
annotation,
self.semantic(),
) =>
{
self.visit_runtime_required_annotation(annotation);
}
AnnotationContext::TypingOnly => self.visit_annotation(annotation),
}

Expand Down
33 changes: 31 additions & 2 deletions crates/ruff_linter/src/rules/flake8_type_checking/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ use anyhow::Result;

use ruff_diagnostics::Edit;
use ruff_python_ast::call_path::from_qualified_name;
use ruff_python_ast::helpers::map_callable;
use ruff_python_ast::helpers::{map_callable, map_subscript};
use ruff_python_ast::{self as ast, Decorator, Expr};
use ruff_python_codegen::{Generator, Stylist};
use ruff_python_semantic::{
analyze, Binding, BindingKind, NodeId, ResolvedReference, SemanticModel,
analyze, Binding, BindingKind, NodeId, ResolvedReference, ScopeKind, SemanticModel,
};
use ruff_source_file::Locator;
use ruff_text_size::Ranged;
Expand Down Expand Up @@ -104,6 +104,35 @@ fn runtime_required_decorators(
})
}

/// Returns `true` if an annotation will be inspected at runtime by the `dataclasses` module.
///
/// Specifically, detects whether an annotation is to either `dataclasses.InitVar` or
/// `typing.ClassVar` within a `@dataclass` class definition.
///
/// See: <https://docs.python.org/3/library/dataclasses.html#init-only-variables>
pub(crate) fn is_dataclass_meta_annotation(annotation: &Expr, semantic: &SemanticModel) -> bool {
// Determine whether the assignment is in a `@dataclass` class definition.
if let ScopeKind::Class(class_def) = semantic.current_scope().kind {
if class_def.decorator_list.iter().any(|decorator| {
semantic
.resolve_call_path(map_callable(&decorator.expression))
.is_some_and(|call_path| {
matches!(call_path.as_slice(), ["dataclasses", "dataclass"])
})
}) {
// Determine whether the annotation is `typing.ClassVar` or `dataclasses.InitVar`.
return semantic
.resolve_call_path(map_subscript(annotation))
.is_some_and(|call_path| {
matches!(call_path.as_slice(), ["dataclasses", "InitVar"])
|| semantic.match_typing_call_path(&call_path, "ClassVar")
});
}
}

false
}

/// Returns `true` if a function is registered as a `singledispatch` interface.
///
/// For example, `fun` below is a `singledispatch` interface:
Expand Down
5 changes: 4 additions & 1 deletion crates/ruff_linter/src/rules/flake8_type_checking/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ mod tests {
#[test_case(Rule::RuntimeStringUnion, Path::new("TCH006_2.py"))]
#[test_case(Rule::TypingOnlyFirstPartyImport, Path::new("TCH001.py"))]
#[test_case(Rule::TypingOnlyStandardLibraryImport, Path::new("TCH003.py"))]
#[test_case(Rule::TypingOnlyStandardLibraryImport, Path::new("init_var.py"))]
#[test_case(Rule::TypingOnlyStandardLibraryImport, Path::new("snapshot.py"))]
#[test_case(Rule::TypingOnlyThirdPartyImport, Path::new("TCH002.py"))]
#[test_case(Rule::TypingOnlyThirdPartyImport, Path::new("quote.py"))]
Expand Down Expand Up @@ -75,7 +76,9 @@ mod tests {
}

#[test_case(Rule::TypingOnlyThirdPartyImport, Path::new("strict.py"))]
#[test_case(Rule::TypingOnlyStandardLibraryImport, Path::new("init_var.py"))]
fn strict(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("strict_{}_{}", rule_code.as_ref(), path.to_string_lossy());
let diagnostics = test_path(
Path::new("flake8_type_checking").join(path).as_path(),
&settings::LinterSettings {
Expand All @@ -86,7 +89,7 @@ mod tests {
..settings::LinterSettings::for_rule(rule_code)
},
)?;
assert_messages!(diagnostics);
assert_messages!(snapshot, diagnostics);
Ok(())
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
---
source: crates/ruff_linter/src/rules/flake8_type_checking/mod.rs
---
init_var.py:5:25: TCH003 [*] Move standard library import `dataclasses.FrozenInstanceError` into a type-checking block
|
3 | from __future__ import annotations
4 |
5 | from dataclasses import FrozenInstanceError, InitVar, dataclass
| ^^^^^^^^^^^^^^^^^^^ TCH003
|
= help: Move into type-checking block

Unsafe fix
2 2 |
3 3 | from __future__ import annotations
4 4 |
5 |-from dataclasses import FrozenInstanceError, InitVar, dataclass
5 |+from dataclasses import InitVar, dataclass
6 |+from typing import TYPE_CHECKING
7 |+
8 |+if TYPE_CHECKING:
9 |+ from dataclasses import FrozenInstanceError
6 10 |
7 11 |
8 12 | @dataclass


Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
source: crates/ruff_linter/src/rules/flake8_type_checking/mod.rs
---

0 comments on commit a52afe7

Please sign in to comment.