Skip to content

Commit

Permalink
[pyflakes] Avoid false positives in @no_type_check contexts (F821, …
Browse files Browse the repository at this point in the history
…F722) (#14615)
  • Loading branch information
ntBre authored Nov 26, 2024
1 parent b94d6cf commit 9f446fa
Show file tree
Hide file tree
Showing 9 changed files with 136 additions and 3 deletions.
21 changes: 21 additions & 0 deletions crates/ruff_linter/resources/test/fixtures/pyflakes/F722_1.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
"""Regression test for #13824.
Don't report an error when the function being annotated has the
`@no_type_check` decorator.
However, we still want to ignore this annotation on classes. See
https://github.com/python/typing/pull/1615/files and the discussion on #14615.
"""

from typing import no_type_check


@no_type_check
def f(arg: "this isn't python") -> "this isn't python either":
x: "this also isn't python" = 0


@no_type_check
class C:
def f(arg: "this isn't python") -> "this isn't python either":
x: "this also isn't python" = 1
21 changes: 21 additions & 0 deletions crates/ruff_linter/resources/test/fixtures/pyflakes/F821_30.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
"""Regression test for #13824.
Don't report an error when the function being annotated has the
`@no_type_check` decorator.
However, we still want to ignore this annotation on classes. See
https://github.com/python/typing/pull/1615/files and the discussion on #14615.
"""

import typing


@typing.no_type_check
def f(arg: "A") -> "R":
x: "A" = 1


@typing.no_type_check
class C:
def f(self, arg: "B") -> "S":
x: "B" = 1
9 changes: 9 additions & 0 deletions crates/ruff_linter/src/checkers/ast/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -723,6 +723,12 @@ impl<'a> Visitor<'a> for Checker<'a> {
// Visit the decorators and arguments, but avoid the body, which will be
// deferred.
for decorator in decorator_list {
if self
.semantic
.match_typing_expr(&decorator.expression, "no_type_check")
{
self.semantic.flags |= SemanticModelFlags::NO_TYPE_CHECK;
}
self.visit_decorator(decorator);
}

Expand Down Expand Up @@ -1851,6 +1857,9 @@ impl<'a> Checker<'a> {

/// Visit an [`Expr`], and treat it as a type definition.
fn visit_type_definition(&mut self, expr: &'a Expr) {
if self.semantic.in_no_type_check() {
return;
}
let snapshot = self.semantic.flags;
self.semantic.flags |= SemanticModelFlags::TYPE_DEFINITION;
self.visit_expr(expr);
Expand Down
4 changes: 3 additions & 1 deletion crates/ruff_linter/src/rules/pyflakes/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,8 @@ mod tests {
#[test_case(Rule::YieldOutsideFunction, Path::new("F704.py"))]
#[test_case(Rule::ReturnOutsideFunction, Path::new("F706.py"))]
#[test_case(Rule::DefaultExceptNotLast, Path::new("F707.py"))]
#[test_case(Rule::ForwardAnnotationSyntaxError, Path::new("F722.py"))]
#[test_case(Rule::ForwardAnnotationSyntaxError, Path::new("F722_0.py"))]
#[test_case(Rule::ForwardAnnotationSyntaxError, Path::new("F722_1.py"))]
#[test_case(Rule::RedefinedWhileUnused, Path::new("F811_0.py"))]
#[test_case(Rule::RedefinedWhileUnused, Path::new("F811_1.py"))]
#[test_case(Rule::RedefinedWhileUnused, Path::new("F811_2.py"))]
Expand Down Expand Up @@ -159,6 +160,7 @@ mod tests {
#[test_case(Rule::UndefinedName, Path::new("F821_26.pyi"))]
#[test_case(Rule::UndefinedName, Path::new("F821_27.py"))]
#[test_case(Rule::UndefinedName, Path::new("F821_28.py"))]
#[test_case(Rule::UndefinedName, Path::new("F821_30.py"))]
#[test_case(Rule::UndefinedExport, Path::new("F822_0.py"))]
#[test_case(Rule::UndefinedExport, Path::new("F822_0.pyi"))]
#[test_case(Rule::UndefinedExport, Path::new("F822_1.py"))]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
snapshot_kind: text
---
F722.py:9:12: F722 Syntax error in forward annotation: `///`
F722_0.py:9:12: F722 Syntax error in forward annotation: `///`
|
9 | def g() -> "///":
| ^^^^^ F722
10 | pass
|

F722.py:13:4: F722 Syntax error in forward annotation: `List[int]☃`
F722_0.py:13:4: F722 Syntax error in forward annotation: `List[int]☃`
|
13 | X: """List[int]"""'' = []
| ^^^^^^^^^^^^^^^^^^ F722
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
---
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
snapshot_kind: text
---
F722_1.py:20:16: F722 Syntax error in forward annotation: `this isn't python`
|
18 | @no_type_check
19 | class C:
20 | def f(arg: "this isn't python") -> "this isn't python either":
| ^^^^^^^^^^^^^^^^^^^ F722
21 | x: "this also isn't python" = 1
|

F722_1.py:20:40: F722 Syntax error in forward annotation: `this isn't python either`
|
18 | @no_type_check
19 | class C:
20 | def f(arg: "this isn't python") -> "this isn't python either":
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ F722
21 | x: "this also isn't python" = 1
|

F722_1.py:21:12: F722 Syntax error in forward annotation: `this also isn't python`
|
19 | class C:
20 | def f(arg: "this isn't python") -> "this isn't python either":
21 | x: "this also isn't python" = 1
| ^^^^^^^^^^^^^^^^^^^^^^^^ F722
|
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
---
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
snapshot_kind: text
---
F821_30.py:20:23: F821 Undefined name `B`
|
18 | @typing.no_type_check
19 | class C:
20 | def f(self, arg: "B") -> "S":
| ^ F821
21 | x: "B" = 1
|

F821_30.py:20:31: F821 Undefined name `S`
|
18 | @typing.no_type_check
19 | class C:
20 | def f(self, arg: "B") -> "S":
| ^ F821
21 | x: "B" = 1
|

F821_30.py:21:13: F821 Undefined name `B`
|
19 | class C:
20 | def f(self, arg: "B") -> "S":
21 | x: "B" = 1
| ^ F821
|
22 changes: 22 additions & 0 deletions crates/ruff_python_semantic/src/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1584,6 +1584,11 @@ impl<'a> SemanticModel<'a> {
self.flags.intersects(SemanticModelFlags::ANNOTATION)
}

/// Return `true` if the model is in a `@no_type_check` context.
pub const fn in_no_type_check(&self) -> bool {
self.flags.intersects(SemanticModelFlags::NO_TYPE_CHECK)
}

/// Return `true` if the model is in a typing-only type annotation.
pub const fn in_typing_only_annotation(&self) -> bool {
self.flags
Expand Down Expand Up @@ -2222,6 +2227,23 @@ bitflags! {
/// [PEP 257]: https://peps.python.org/pep-0257/#what-is-a-docstring
const ATTRIBUTE_DOCSTRING = 1 << 25;

/// The model is in a [no_type_check] context.
///
/// This is used to skip type checking when the `@no_type_check` decorator is found.
///
/// For example (adapted from [#13824]):
/// ```python
/// from typing import no_type_check
///
/// @no_type_check
/// def fn(arg: "A") -> "R":
/// pass
/// ```
///
/// [no_type_check]: https://docs.python.org/3/library/typing.html#typing.no_type_check
/// [#13824]: https://github.com/astral-sh/ruff/issues/13824
const NO_TYPE_CHECK = 1 << 26;

/// The context is in any type annotation.
const ANNOTATION = Self::TYPING_ONLY_ANNOTATION.bits() | Self::RUNTIME_EVALUATED_ANNOTATION.bits() | Self::RUNTIME_REQUIRED_ANNOTATION.bits();

Expand Down

0 comments on commit 9f446fa

Please sign in to comment.