Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Add new setting to enforce future imports for all annotations
Browse files Browse the repository at this point in the history
Daverball committed Dec 13, 2024

Verified

This commit was signed with the committer’s verified signature.
Daverball David Salvisberg
1 parent eef57df commit 3d884d6
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
@@ -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.
14 changes: 13 additions & 1 deletion flake8_type_checking/checker.py
Original file line number Diff line number Diff line change
@@ -1020,6 +1020,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__()

@@ -1043,6 +1044,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] = {}
@@ -1935,6 +1939,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)
@@ -1960,6 +1965,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,
@@ -2123,7 +2129,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:
7 changes: 7 additions & 0 deletions flake8_type_checking/plugin.py
Original file line number Diff line number Diff line change
@@ -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(
1 change: 1 addition & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -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)
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 3d884d6

Please sign in to comment.