-
Notifications
You must be signed in to change notification settings - Fork 1.2k
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
Implement flake8-future-annotations
FA100
#3979
Changes from 12 commits
0456611
d5c81de
cc9e9ce
9acaee8
73b4653
69abeb0
4373751
822cdb7
fd41668
8bdd0c1
4569ac6
e6775ea
d872a25
b82b04f
92cc016
64e2012
b52241f
af9be69
e16416b
33a73b9
d47ef9a
41f65c4
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,7 @@ | ||
from typing import List | ||
import typing as t | ||
|
||
|
||
def main(_: List[int]) -> None: | ||
a_list: t.List[str] = [] | ||
a_list.append("hello") |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
from typing import List | ||
|
||
|
||
def main() -> None: | ||
a_list: List[str] = [] | ||
a_list.append("hello") |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
from typing import Dict, List, Optional, Set, Union, cast | ||
|
||
|
||
def main() -> None: | ||
a_list: List[Optional[str]] = [] | ||
a_list.append("hello") | ||
a_dict = cast(Dict[int | None, Union[int, Set[bool]]], {}) | ||
a_dict[1] = {True, False} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
import typing | ||
|
||
|
||
def main() -> None: | ||
a_list: typing.List[str] = [] | ||
a_list.append("hello") |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
import typing as t | ||
|
||
|
||
def main() -> None: | ||
a_list: t.List[str] = [] | ||
a_list.append("hello") |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
def main() -> None: | ||
a_list: list[str] = [] | ||
a_list.append("hello") | ||
|
||
|
||
def hello(y: dict[str, int]) -> None: | ||
del y |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
def main() -> None: | ||
a_list: list[str] | None = [] | ||
a_list.append("hello") | ||
|
||
|
||
def hello(y: dict[str, int] | None) -> None: | ||
del y |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
def main() -> None: | ||
a_list: list[str | None] = [] | ||
a_list.append("hello") | ||
|
||
|
||
def hello(y: dict[str | None, int]) -> None: | ||
z: tuple[str, str | None, str] = tuple(y) | ||
del z |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
def main() -> str: | ||
a_str = "hello" | ||
return a_str |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
from typing import NamedTuple | ||
|
||
|
||
class Stuff(NamedTuple): | ||
x: int | ||
|
||
|
||
def main() -> None: | ||
a_list = Stuff(5) | ||
print(a_list) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
from __future__ import annotations | ||
|
||
|
||
def main() -> None: | ||
a_list: list[str] = [] | ||
a_list.append("hello") |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
import typing | ||
|
||
IRRELEVANT = typing.TypeVar | ||
|
||
|
||
def main() -> None: | ||
List: list[str] = [] | ||
List.append("hello") |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
//! Rules from [flake8-future-annotations](https://pypi.org/project/flake8-future-annotations/). | ||
pub(crate) mod rules; | ||
|
||
#[cfg(test)] | ||
mod tests { | ||
use std::path::Path; | ||
|
||
use anyhow::Result; | ||
use test_case::test_case; | ||
|
||
use crate::registry::Rule; | ||
use crate::test::test_path; | ||
use crate::{assert_messages, settings}; | ||
|
||
#[test_case(Path::new("edge_case.py"); "edge_case")] | ||
#[test_case(Path::new("from_typing_import.py"); "from_typing_import")] | ||
#[test_case(Path::new("from_typing_import_many.py"); "from_typing_import_many")] | ||
#[test_case(Path::new("import_typing.py"); "import_typing")] | ||
#[test_case(Path::new("import_typing_as.py"); "import_typing_as")] | ||
#[test_case(Path::new("no_future_import_uses_lowercase.py"); "no_future_import_uses_lowercase")] | ||
#[test_case(Path::new("no_future_import_uses_union.py"); "no_future_import_uses_union")] | ||
#[test_case(Path::new("no_future_import_uses_union_inner.py"); "no_future_import_uses_union_inner")] | ||
#[test_case(Path::new("ok_no_types.py"); "ok_no_types")] | ||
#[test_case(Path::new("ok_non_simplifiable_types.py"); "ok_non_simplifiable_types")] | ||
#[test_case(Path::new("ok_uses_future.py"); "ok_uses_future")] | ||
#[test_case(Path::new("ok_variable_name.py"); "ok_variable_name")] | ||
fn rules(path: &Path) -> Result<()> { | ||
let snapshot = path.to_string_lossy().into_owned(); | ||
let diagnostics = test_path( | ||
Path::new("flake8_future_annotations").join(path).as_path(), | ||
&settings::Settings::for_rules(vec![Rule::MissingFutureAnnotationsWithImports]), | ||
)?; | ||
assert_messages!(snapshot, diagnostics); | ||
Ok(()) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
use itertools::Itertools; | ||
use rustpython_parser::ast::{Alias, Expr, Stmt}; | ||
|
||
use ruff_diagnostics::{Diagnostic, Violation}; | ||
use ruff_macros::{derive_message_formats, violation}; | ||
use ruff_python_stdlib::typing::PEP_585_SUBSCRIPT_ELIGIBLE; | ||
|
||
use crate::checkers::ast::Checker; | ||
|
||
/// ## Example | ||
/// ```python | ||
/// import typing as t | ||
/// from typing import List | ||
/// | ||
/// | ||
/// def function(a_dict: t.Dict[str, t.Optional[int]]) -> None: | ||
/// a_list: List[str] = [] | ||
/// a_list.append("hello") | ||
/// ``` | ||
/// | ||
/// To fix the lint error: | ||
/// ```python | ||
/// from __future__ import annotations | ||
/// | ||
/// import typing as t | ||
/// from typing import List | ||
/// | ||
/// | ||
/// def function(a_dict: t.Dict[str, t.Optional[int]]) -> None: | ||
/// a_list: List[str] = [] | ||
/// a_list.append("hello") | ||
/// ``` | ||
/// | ||
/// After running additional pyupgrade autofixes: | ||
/// ```python | ||
/// from __future__ import annotations | ||
/// | ||
/// | ||
/// def function(a_dict: dict[str, int | None]) -> None: | ||
/// a_list: list[str] = [] | ||
/// a_list.append("hello") | ||
/// ``` | ||
#[violation] | ||
pub struct MissingFutureAnnotationsWithImports { | ||
pub names: Vec<String>, | ||
} | ||
|
||
impl Violation for MissingFutureAnnotationsWithImports { | ||
#[derive_message_formats] | ||
fn message(&self) -> String { | ||
let MissingFutureAnnotationsWithImports { names } = self; | ||
let names = names.iter().map(|name| format!("`{name}`")).join(", "); | ||
format!("Missing `from __future__ import annotations`, but imports: {names}") | ||
} | ||
} | ||
|
||
/// FA100 | ||
pub fn missing_future_annotations_from_typing_import( | ||
checker: &mut Checker, | ||
stmt: &Stmt, | ||
module: &str, | ||
names: &[Alias], | ||
) { | ||
if checker.ctx.annotations_future_enabled { | ||
return; | ||
} | ||
|
||
let names: Vec<String> = names | ||
.iter() | ||
.map(|name| name.node.name.as_str()) | ||
.filter(|alias| PEP_585_SUBSCRIPT_ELIGIBLE.contains(&[module, alias].as_slice())) | ||
.map(std::string::ToString::to_string) | ||
.sorted() | ||
.collect(); | ||
|
||
if !names.is_empty() { | ||
checker.diagnostics.push(Diagnostic::new( | ||
MissingFutureAnnotationsWithImports { names }, | ||
stmt.range(), | ||
)); | ||
} | ||
} | ||
|
||
/// FA100 | ||
pub fn missing_future_annotations_from_typing_usage(checker: &mut Checker, expr: &Expr) { | ||
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. Sorry, last question: what's the benefit in flagging both the usages and the imports? Would flagging the import alone be insufficient? 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. If the user uses |
||
if checker.ctx.annotations_future_enabled { | ||
return; | ||
} | ||
|
||
if let Some(binding) = checker.ctx.resolve_call_path(expr) { | ||
if PEP_585_SUBSCRIPT_ELIGIBLE.contains(&binding.as_slice()) { | ||
checker.diagnostics.push(Diagnostic::new( | ||
MissingFutureAnnotationsWithImports { | ||
names: vec![binding.iter().join(".")], | ||
}, | ||
expr.range(), | ||
)); | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
--- | ||
source: crates/ruff/src/rules/flake8_future_annotations/mod.rs | ||
--- | ||
edge_case.py:1:1: FA100 Missing `from __future__ import annotations`, but imports: `List` | ||
| | ||
1 | from typing import List | ||
| ^^^^^^^^^^^^^^^^^^^^^^^ FA100 | ||
2 | import typing as t | ||
| | ||
|
||
edge_case.py:6:13: FA100 Missing `from __future__ import annotations`, but imports: `typing.List` | ||
| | ||
6 | def main(_: List[int]) -> None: | ||
7 | a_list: t.List[str] = [] | ||
| ^^^^^^ FA100 | ||
8 | a_list.append("hello") | ||
| | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
--- | ||
source: crates/ruff/src/rules/flake8_future_annotations/mod.rs | ||
--- | ||
from_typing_import.py:1:1: FA100 Missing `from __future__ import annotations`, but imports: `List` | ||
| | ||
1 | from typing import List | ||
| ^^^^^^^^^^^^^^^^^^^^^^^ FA100 | ||
| | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
--- | ||
source: crates/ruff/src/rules/flake8_future_annotations/mod.rs | ||
--- | ||
from_typing_import_many.py:1:1: FA100 Missing `from __future__ import annotations`, but imports: `Dict`, `List`, `Optional`, `Set`, `Union` | ||
| | ||
1 | from typing import Dict, List, Optional, Set, Union, cast | ||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FA100 | ||
| | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
--- | ||
source: crates/ruff/src/rules/flake8_future_annotations/mod.rs | ||
--- | ||
import_typing.py:5:13: FA100 Missing `from __future__ import annotations`, but imports: `typing.List` | ||
| | ||
5 | def main() -> None: | ||
6 | a_list: typing.List[str] = [] | ||
| ^^^^^^^^^^^ FA100 | ||
7 | a_list.append("hello") | ||
| | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
--- | ||
source: crates/ruff/src/rules/flake8_future_annotations/mod.rs | ||
--- | ||
import_typing_as.py:5:13: FA100 Missing `from __future__ import annotations`, but imports: `typing.List` | ||
| | ||
5 | def main() -> None: | ||
6 | a_list: t.List[str] = [] | ||
| ^^^^^^ FA100 | ||
7 | a_list.append("hello") | ||
| | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
--- | ||
source: crates/ruff/src/rules/flake8_future_annotations/mod.rs | ||
--- | ||
|
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.
These should only be activated for minimum-version < Python 3.9, right? Since PEP 585 was part of Python 3.9 (so future annotations aren't required to use the standard-library variants in 3.9 and later).
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.
In practice, this works on Python 3.7+.
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.
But if your minimum-supported Python version is Python 3.9, you don't need
__future__
annotations to use the standard-library generics. So these errors would already be detected and fixed by the existingpyupgrade
rules, right?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.
Ah, you're asking about the upper version.
Unions like
str | None
didn't start working until Python 3.10, which are also covered by this plugin.After 3.10, the future annotations import is not as useful anymore.