Skip to content

Commit

Permalink
feat: Add new setting to enforce future imports for all annotations
Browse files Browse the repository at this point in the history
  • Loading branch information
Daverball authored Dec 13, 2024
1 parent 87543b7 commit b3c2c4b
Show file tree
Hide file tree
Showing 5 changed files with 58 additions and 1 deletion.
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,24 @@ imports that *can* be moved.
type-checking-strict = true # default false
```

### Force `from __future__ import annotations` import

The plugin, by default, will only report a TC100 error, if annotations
contain references to typing only symbols. If you want to enforce a more
consistent style and use a future import in every file that makes use
of annotations, you can enable this setting.

When `force-future-annotation` is enabled, the plugin will flag all
files that contain annotations but not future import.

- **setting name**: `type-checking-force-future-annotation`
- **type**: `bool`

```ini
[flake8]
type-checking-force-future-annotation = true # default false
```

### Pydantic support

If you use Pydantic models in your code, you should enable Pydantic support.
Expand Down
14 changes: 13 additions & 1 deletion flake8_type_checking/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -1045,6 +1045,7 @@ def __init__(
pydantic_enabled_baseclass_passlist: list[str],
typing_modules: list[str] | None = None,
exempt_modules: list[str] | None = None,
force_future_annotation: bool = False,
) -> None:
super().__init__()

Expand All @@ -1068,6 +1069,9 @@ def __init__(
#: Import patterns we want to avoid mapping
self.exempt_modules: list[str] = exempt_modules or []

#: Whether or not TC100 should always be emitted if there are annotations
self.force_future_annotation = force_future_annotation

#: All imports, in each category
self.application_imports: dict[str, Import] = {}
self.third_party_imports: dict[str, Import] = {}
Expand Down Expand Up @@ -1968,6 +1972,7 @@ def __init__(self, node: ast.Module, options: Namespace | None) -> None:

typing_modules = getattr(options, 'type_checking_typing_modules', [])
exempt_modules = getattr(options, 'type_checking_exempt_modules', [])
force_future_annotation = getattr(options, 'type_checking_force_future_annotation', False)
pydantic_enabled = getattr(options, 'type_checking_pydantic_enabled', False)
pydantic_enabled_baseclass_passlist = getattr(options, 'type_checking_pydantic_enabled_baseclass_passlist', [])
sqlalchemy_enabled = getattr(options, 'type_checking_sqlalchemy_enabled', False)
Expand All @@ -1993,6 +1998,7 @@ def __init__(self, node: ast.Module, options: Namespace | None) -> None:
cattrs_enabled=cattrs_enabled,
typing_modules=typing_modules,
exempt_modules=exempt_modules,
force_future_annotation=force_future_annotation,
sqlalchemy_enabled=sqlalchemy_enabled,
sqlalchemy_mapped_dotted_names=sqlalchemy_mapped_dotted_names,
fastapi_dependency_support_enabled=fastapi_dependency_support_enabled,
Expand Down Expand Up @@ -2159,7 +2165,13 @@ def missing_quotes_or_futures_import(self) -> Flake8Generator:

# if any of the symbols imported/declared in type checking blocks are used
# in an annotation outside a type checking block, then we need to emit TC100
if encountered_missing_quotes and not self.visitor.futures_annotation:
if (
encountered_missing_quotes
or (
self.visitor.force_future_annotation
and (self.visitor.unwrapped_annotations or self.visitor.wrapped_annotations)
)
) and not self.visitor.futures_annotation:
yield 1, 0, TC100, None

def futures_excess_quotes(self) -> Flake8Generator:
Expand Down
7 changes: 7 additions & 0 deletions flake8_type_checking/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,13 @@ def add_options(cls, option_manager: OptionManager) -> None: # pragma: no cover
default=False,
help='Flag individual imports rather than looking at the module.',
)
option_manager.add_option(
'--type-checking-force-future-annotation',
action='store_true',
parse_from_config=True,
default=False,
help='Always emit TC100 as long as there are any annotations and no future import.',
)

# Third-party library options
option_manager.add_option(
Expand Down
1 change: 1 addition & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ def _get_error(example: str, *, error_code_filter: Optional[str] = None, **kwarg
mock_options.type_checking_sqlalchemy_mapped_dotted_names = []
mock_options.type_checking_injector_enabled = False
mock_options.type_checking_strict = False
mock_options.type_checking_force_future_annotation = False
# kwarg overrides
for k, v in kwargs.items():
setattr(mock_options, k, v)
Expand Down
19 changes: 19 additions & 0 deletions tests/test_force_future_annotation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import textwrap

from flake8_type_checking.constants import TC100
from tests.conftest import _get_error


def test_force_future_annotation():
"""TC100 should be emitted even if there are no forward references to typing-only symbols."""
example = textwrap.dedent(
'''
from x import Y
a: Y
'''
)
assert _get_error(example, error_code_filter='TC100', type_checking_force_future_annotation=False) == set()
assert _get_error(example, error_code_filter='TC100', type_checking_force_future_annotation=True) == {
'1:0 ' + TC100
}

0 comments on commit b3c2c4b

Please sign in to comment.