Skip to content

Commit

Permalink
Add option to always report missing FA imports
Browse files Browse the repository at this point in the history
  • Loading branch information
Rogdham committed May 4, 2022
1 parent c43d03b commit a8dc356
Show file tree
Hide file tree
Showing 4 changed files with 55 additions and 11 deletions.
15 changes: 14 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,14 @@ Verifies python 3.7+ files use `from __future__ import annotations` if a type is

Pairs well with [pyupgrade](https://github.com/asottile/pyupgrade) with the `--py37-plus` flag or higher, since pyupgrade only replaces type annotations with the PEP 563 rules if `from __future__ import annotations` is present.

For example:
## flake8 codes

| Code | Description |
|-------|---------------------------------------------------------------------------|
| FA100 | Missing import if a type used in the module can be rewritten using PEP563 |
| FA101 | Missing import when no rewrite using PEP563 is available (see config) |

## Example

```python
import typing as t
Expand All @@ -34,3 +41,9 @@ def function(a_dict: dict[str, int | None]) -> None:
a_list: list[str] = []
a_list.append("hello")
```

## Configuration

This plugin has a single configuration which is the `--force-future-annotations` option.

If set, missing `from __future__ import annotations` will be reported regardless of a rewrite available according to PEP 563; in this case, code FA101 is used instead of FA100.
33 changes: 27 additions & 6 deletions flake8_future_annotations/checker.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
from __future__ import annotations

import ast
from typing import Iterator
from typing import Any, Iterator

# The code F is required in order for errors to appear.
ERROR_MESSAGE = "FA100 Missing from __future__ import annotations but imports: {}"
ERROR_MESSAGE_100 = "FA100 Missing from __future__ import annotations but imports: {}"
ERROR_MESSAGE_101 = "FA101 Missing from __future__ import annotations"
SIMPLIFIABLE_TYPES = (
"DefaultDict",
"Deque",
Expand Down Expand Up @@ -78,6 +79,7 @@ def visit_Attribute(self, node: ast.Attribute) -> None:
class FutureAnnotationsChecker:
name = "flake8-future-annotations"
version = "0.0.4"
force_future_annotations = False

def __init__(self, tree: ast.Module, filename: str) -> None:
self.tree = tree
Expand All @@ -86,9 +88,28 @@ def __init__(self, tree: ast.Module, filename: str) -> None:
def run(self) -> Iterator[tuple[int, int, str, type]]:
visitor = FutureAnnotationsVisitor()
visitor.visit(self.tree)
if visitor.imports_future_annotations or not visitor.typing_imports:
if visitor.imports_future_annotations:
return

imports = ", ".join(visitor.typing_imports)
lineno, char_offset = 1, 0
yield lineno, char_offset, ERROR_MESSAGE.format(imports), type(self)
lineno, char_offset, message = 1, 0, None

if visitor.typing_imports:
message = ERROR_MESSAGE_100.format(", ".join(visitor.typing_imports))
elif self.force_future_annotations:
message = ERROR_MESSAGE_101

if message is not None:
yield lineno, char_offset, message, type(self)

@staticmethod
def add_options(option_manager: Any) -> None:
option_manager.add_option(
"--force-future-annotations",
action="store_true",
parse_from_config=True,
help="Force the use of future annotations imports",
)

@classmethod
def parse_options(cls, options: Any) -> None:
cls.force_future_annotations = options.force_future_annotations
5 changes: 4 additions & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,12 @@
from flake8_future_annotations.checker import FutureAnnotationsChecker


def run_validator_for_test_file(filename: str) -> list[tuple[int, int, str, type]]:
def run_validator_for_test_file(
filename: str, force_future_annotations: bool
) -> list[tuple[int, int, str, type]]:
raw_content = Path(filename).read_text(encoding="utf-8")
tree = ast.parse(raw_content)

checker = FutureAnnotationsChecker(tree=tree, filename=filename)
checker.force_future_annotations = force_future_annotations
return list(checker.run())
13 changes: 10 additions & 3 deletions tests/test_checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,18 @@ def test_version() -> None:


@pytest.mark.parametrize("filepath", ALL_TEST_FILES)
def test_ok_cases_produces_no_errors(filepath: str) -> None:
errors = run_validator_for_test_file(str(filepath))
@pytest.mark.parametrize("force_future_annotations", (False, True))
def test_ok_cases_produces_no_errors(
filepath: str, force_future_annotations: bool
) -> None:
errors = run_validator_for_test_file(str(filepath), force_future_annotations)

if "ok" in filepath:
assert len(errors) == 0, (str(filepath), errors)
if "uses_future" in filepath or not force_future_annotations:
assert len(errors) == 0, (str(filepath), errors)
else:
assert len(errors) == 1, (str(filepath), errors)
assert errors[0][2][:5] == "FA101", (str(filepath), "error code")

else:
assert len(errors) == 1, (str(filepath), errors)
Expand Down

0 comments on commit a8dc356

Please sign in to comment.