Skip to content
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

feat: Add new setting to enforce future imports for all annotations #200

Merged
merged 1 commit into from
Dec 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -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__()

Expand All @@ -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] = {}
Expand Down Expand Up @@ -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)
Expand All @@ -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,
Expand Down Expand Up @@ -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:
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
}
Loading