-
Notifications
You must be signed in to change notification settings - Fork 1.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[flake8-type-checking] Adds implementation for TCH007 and TCH008 #12927
base: main
Are you sure you want to change the base?
Changes from 28 commits
3de92a5
e1ca904
3201588
bc45146
0df3d12
4cb32e1
d9cea8f
096eb9d
e420697
fa9d4df
dd78b52
08b7ddc
bd31311
b185e4b
b17c959
643ddb6
09986c7
4044dfa
f51ea65
81bb172
2555f8a
a36bab6
f2f384e
6799411
231804b
bfffb7b
fd051a8
0fc0d4b
f1ec697
fd8c045
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
from __future__ import annotations | ||
|
||
from typing import Dict, TypeAlias, TYPE_CHECKING | ||
|
||
if TYPE_CHECKING: | ||
from typing import Dict | ||
|
||
from foo import Foo | ||
|
||
OptStr: TypeAlias = str | None | ||
Bar: TypeAlias = Foo[int] | ||
|
||
a: TypeAlias = int # OK | ||
b: TypeAlias = Dict # OK | ||
c: TypeAlias = Foo # TCH007 | ||
d: TypeAlias = Foo | None # TCH007 | ||
e: TypeAlias = OptStr # TCH007 | ||
f: TypeAlias = Bar # TCH007 | ||
g: TypeAlias = Foo | Bar # TCH007 x2 | ||
h: TypeAlias = Foo[str] # TCH007 | ||
i: TypeAlias = (Foo | # TCH007 x2 (fix removes comment currently) | ||
Bar) | ||
|
||
type C = Foo # OK | ||
type D = Foo | None # OK | ||
type E = OptStr # OK | ||
type F = Bar # OK | ||
type G = Foo | Bar # OK | ||
type H = Foo[str] # OK | ||
type I = (Foo | # OK | ||
Bar) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
from __future__ import annotations | ||
|
||
from typing import TypeAlias, TYPE_CHECKING | ||
|
||
from foo import Foo | ||
|
||
if TYPE_CHECKING: | ||
from typing import Dict | ||
|
||
OptStr: TypeAlias = str | None | ||
Bar: TypeAlias = Foo[int] | ||
else: | ||
Bar = Foo | ||
|
||
a: TypeAlias = 'int' # TCH008 | ||
b: TypeAlias = 'Dict' # OK | ||
c: TypeAlias = 'Foo' # TCH008 | ||
d: TypeAlias = 'Foo[str]' # OK | ||
e: TypeAlias = 'Foo.bar' # OK | ||
f: TypeAlias = 'Foo | None' # TCH008 | ||
g: TypeAlias = 'OptStr' # OK | ||
h: TypeAlias = 'Bar' # TCH008 | ||
i: TypeAlias = Foo['str'] # TCH008 | ||
j: TypeAlias = 'Baz' # OK | ||
k: TypeAlias = 'k | None' # OK | ||
l: TypeAlias = 'int' | None # TCH008 (because TC010 is not enabled) | ||
m: TypeAlias = ('int' # TCH008 | ||
| None) | ||
n: TypeAlias = ('int' # TCH008 (fix removes comment currently) | ||
' | None') | ||
|
||
type B = 'Dict' # TCH008 | ||
type D = 'Foo[str]' # TCH008 | ||
type E = 'Foo.bar' # TCH008 | ||
type G = 'OptStr' # TCH008 | ||
type I = Foo['str'] # TCH008 | ||
type J = 'Baz' # TCH008 | ||
type K = 'K | None' # TCH008 | ||
type L = 'int' | None # TCH008 (because TC010 is not enabled) | ||
Daverball marked this conversation as resolved.
Show resolved
Hide resolved
Daverball marked this conversation as resolved.
Show resolved
Hide resolved
|
||
type M = ('int' # TCH008 | ||
| None) | ||
type N = ('int' # TCH008 (fix removes comment currently) | ||
' | None') | ||
|
||
|
||
class Baz: | ||
a: TypeAlias = 'Baz' # OK | ||
type A = 'Baz' # TCH008 | ||
|
||
class Nested: | ||
a: TypeAlias = 'Baz' # OK | ||
type A = 'Baz' # TCH008 |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -52,6 +52,8 @@ pub(crate) fn deferred_scopes(checker: &mut Checker) { | |
// Identify any valid runtime imports. If a module is imported at runtime, and | ||
// used at runtime, then by default, we avoid flagging any other | ||
// imports from that model as typing-only. | ||
// FIXME: This does not seem quite right, if only TC004 is enabled | ||
// then we don't need to collect the runtime imports | ||
let enforce_typing_imports = !checker.source_type.is_stub() | ||
&& checker.any_enabled(&[ | ||
Rule::RuntimeImportInTypeCheckingBlock, | ||
|
@@ -375,6 +377,8 @@ pub(crate) fn deferred_scopes(checker: &mut Checker) { | |
} | ||
|
||
if matches!(scope.kind, ScopeKind::Function(_) | ScopeKind::Module) { | ||
// FIXME: This does not seem quite right, if only TC004 is enabled | ||
// then we don't need to collect the runtime imports | ||
Comment on lines
+380
to
+381
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What's the reason we can't fix this today? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We can, I just didn't want to introduce unrelated changes to this PR, so I opted for a comment as reminder to myself to clean this up in a follow-up pull request. |
||
if enforce_typing_imports { | ||
let runtime_imports: Vec<&Binding> = checker | ||
.semantic | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -879,9 +879,7 @@ impl<'a> Visitor<'a> for Checker<'a> { | |
if let Some(type_params) = type_params { | ||
self.visit_type_params(type_params); | ||
} | ||
self.visit | ||
.type_param_definitions | ||
.push((value, self.semantic.snapshot())); | ||
self.visit_generic_type_alias(value); | ||
self.semantic.pop_scope(); | ||
self.visit_expr(name); | ||
} | ||
|
@@ -952,7 +950,7 @@ impl<'a> Visitor<'a> for Checker<'a> { | |
|
||
if let Some(expr) = value { | ||
if self.semantic.match_typing_expr(annotation, "TypeAlias") { | ||
self.visit_type_definition(expr); | ||
self.visit_explicit_type_alias(expr); | ||
} else { | ||
self.visit_expr(expr); | ||
} | ||
|
@@ -1842,6 +1840,31 @@ impl<'a> Checker<'a> { | |
self.semantic.flags = snapshot; | ||
} | ||
|
||
/// Visit an [`Expr`], and treat it as a [PEP 613] explicit type alias. | ||
/// | ||
/// [PEP 613]: https://peps.python.org/pep-0613/ | ||
fn visit_explicit_type_alias(&mut self, expr: &'a Expr) { | ||
let snapshot = self.semantic.flags; | ||
self.semantic.flags |= SemanticModelFlags::EXPLICIT_TYPE_ALIAS; | ||
self.visit_type_definition(expr); | ||
self.semantic.flags = snapshot; | ||
} | ||
|
||
/// Visit an [`Expr`], and treat it as a [PEP 695] generic type alias. | ||
/// | ||
/// [PEP 695]: https://peps.python.org/pep-0695/#generic-type-alias | ||
fn visit_generic_type_alias(&mut self, expr: &'a Expr) { | ||
let snapshot = self.semantic.flags; | ||
// even though we don't visit these nodes immediately we need to | ||
// modify the semantic flags before we push the expression and its | ||
// corresponding semantic snapshot | ||
self.semantic.flags |= SemanticModelFlags::GENERIC_TYPE_ALIAS; | ||
Comment on lines
+1843
to
+1861
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Naming nit: I would append More substantive naming issue: I think that using "generic type alias" as an all-encompassing description of PEP 695 type aliases is inaccurate and confusing. (It is strange that the PEP text uses this as a header over that entire section of the PEP, but it's clear in code examples in that section that only a Using "explicit type alias" to describe PEP 613 type aliases is not unreasonable, given that's the title of PEP 613, but I still think it's ambiguous (the If we don't mind referencing PEPs, then I think we should just go all-in on that, and opt for concise and totally unambiguous, with names like If we want to avoid using PEP numbers like this (which is totally unambiguous, but does require knowing or looking up the PEP numbers), then I think we have to be more verbose to be unambiguous: One other note: either way, a code example in the docstring of each method could go a long way towards making it immediately obvious to the reader what form each one is handling. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'll have a little think about what I prefer. Initially I did use the PEP numbers in their names, but it seemed too easy to accidentally read one as the other when you're just skimming the code, so I decided to switch to the names used in the PEP title/section header respectively, even if they're far from ideal. The descriptive names are a little too tongue-twistery for my liking, but they may be the correct choice after all. |
||
self.visit | ||
.type_param_definitions | ||
.push((expr, self.semantic.snapshot())); | ||
self.semantic.flags = snapshot; | ||
} | ||
|
||
/// Visit an [`Expr`], and treat it as a type definition. | ||
fn visit_type_definition(&mut self, expr: &'a Expr) { | ||
let snapshot = self.semantic.flags; | ||
|
@@ -2001,6 +2024,21 @@ impl<'a> Checker<'a> { | |
flags.insert(BindingFlags::UNPACKED_ASSIGNMENT); | ||
} | ||
|
||
match parent { | ||
Stmt::TypeAlias(_) => flags.insert(BindingFlags::GENERIC_TYPE_ALIAS), | ||
Stmt::AnnAssign(ast::StmtAnnAssign { annotation, .. }) => { | ||
// TODO: It is a bit unfortunate that we do this check twice | ||
// maybe we should change how we visit this statement | ||
// so the semantic flag for the type alias sticks around | ||
// until after we've handled this store, so we can check | ||
// the flag instead of duplicating this check | ||
if self.semantic.match_typing_expr(annotation, "TypeAlias") { | ||
flags.insert(BindingFlags::EXPLICIT_TYPE_ALIAS); | ||
} | ||
} | ||
_ => {} | ||
} | ||
|
||
let scope = self.semantic.current_scope(); | ||
|
||
if scope.kind.is_module() | ||
|
@@ -2256,7 +2294,17 @@ impl<'a> Checker<'a> { | |
|
||
self.semantic.flags |= | ||
SemanticModelFlags::TYPE_DEFINITION | type_definition_flag; | ||
self.visit_expr(parsed_annotation.expression()); | ||
let parsed_expr = parsed_annotation.expression(); | ||
self.visit_expr(parsed_expr); | ||
if self.semantic.in_type_alias() { | ||
if self.enabled(Rule::QuotedTypeAlias) { | ||
flake8_type_checking::rules::quoted_type_alias( | ||
self, | ||
parsed_expr, | ||
string_expr, | ||
); | ||
} | ||
} | ||
self.parsed_type_annotation = None; | ||
} else { | ||
if self.enabled(Rule::ForwardAnnotationSyntaxError) { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,9 +1,11 @@ | ||
pub(crate) use empty_type_checking_block::*; | ||
pub(crate) use runtime_import_in_type_checking_block::*; | ||
pub(crate) use runtime_string_union::*; | ||
pub(crate) use type_alias_quotes::*; | ||
pub(crate) use typing_only_runtime_import::*; | ||
|
||
mod empty_type_checking_block; | ||
mod runtime_import_in_type_checking_block; | ||
mod runtime_string_union; | ||
mod type_alias_quotes; | ||
mod typing_only_runtime_import; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What does the fix look like when comments are present?
example:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Until I know why this fix was marked unsafe in the other rules I will continue always to mark this fix as unsafe. If the only reason was the fix potentially eating comments I can apply the same comment detection logic to TCH007.