diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4fe1b9e..da0053c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,10 +1,10 @@ repos: - repo: https://github.com/psf/black - rev: 24.10.0 + rev: 25.12.0 hooks: - id: black - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v5.0.0 + rev: v6.0.0 hooks: - id: check-ast - id: check-added-large-files @@ -19,7 +19,7 @@ repos: - id: mixed-line-ending - id: trailing-whitespace - repo: https://github.com/pycqa/flake8 - rev: 7.1.1 + rev: 7.3.0 hooks: - id: flake8 additional_dependencies: [ @@ -34,18 +34,24 @@ repos: 'flake8-type-checking==2.9.1', ] - repo: https://github.com/asottile/pyupgrade - rev: v3.19.0 + rev: v3.21.2 hooks: - id: pyupgrade args: [ "--py39-plus", '--keep-runtime-typing' ] - repo: https://github.com/pycqa/isort - rev: 5.13.2 + rev: 7.0.0 hooks: - id: isort - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.13.0 + rev: v1.19.1 hooks: - id: mypy additional_dependencies: - pytest - flake8 + # NOTE: We want this hook to always run, but exactly once + # instead of for every file. So we exclude all files + exclude: '.*' + always_run: true + pass_filenames: false + args: ['-p', 'flake8_type_checking'] diff --git a/README.md b/README.md index 703486a..1cd4539 100644 --- a/README.md +++ b/README.md @@ -128,6 +128,21 @@ pip install flake8-type-checking These options are configurable, and can be set in your flake8 config. +## Python 3.14+ + +If your code is targeting Python 3.14+ you no longer need to wrap +annotations in quotes or add a future import. So in this case it's +recommended to add `type-checking-p314plus = true` to your flake8 +configuration and select the `TC1` rules. + +- **setting name**: `type-checking-p314plus` +- **type**: `bool` + +```ini +[flake8] +type-checking-py314plus = true # default false +``` + ### Typing modules If you re-export `typing` or `typing_extensions` members from a compatibility diff --git a/flake8_type_checking/checker.py b/flake8_type_checking/checker.py index 0e917c0..c103a59 100644 --- a/flake8_type_checking/checker.py +++ b/flake8_type_checking/checker.py @@ -223,7 +223,7 @@ def visit_Constant(self, node: ast.Constant) -> ast.Constant: # just needs to be available in global scope anywhere, we handle # this by special casing `ast.Constant` when we look for used type # checking symbols - self.uses[node.value].append((node, self.current_scope)) + self.uses[node.value].append((node, self.current_scope)) # type: ignore[index] return node @@ -287,6 +287,7 @@ def visit_annotation_name(self, node: ast.Name) -> None: def visit_annotation_string(self, node: ast.Constant) -> None: """Add all the names in the string to mapped names.""" + assert isinstance(node.value, str) visitor = StringAnnotationVisitor(self.plugin) visitor.parse_and_visit_string_annotation(node.value) self.plugin.soft_uses.update(visitor.names) @@ -838,6 +839,7 @@ def visit_annotation_name(self, node: ast.Name) -> None: def visit_annotation_string(self, node: ast.Constant) -> None: """Parse and visit nested string annotations.""" + assert isinstance(node.value, str) self.parse_and_visit_string_annotation(node.value) @@ -902,6 +904,7 @@ def visit_annotation_name(self, node: ast.Name) -> None: def visit_annotation_string(self, node: ast.Constant) -> None: """Register wrapped annotation and invalid binop literals.""" + assert isinstance(node.value, str) setattr(node, ANNOTATION_PROPERTY, True) # we don't want to register them as both so we don't emit redundant errors if getattr(node, BINOP_OPERAND_PROPERTY, False): @@ -930,7 +933,7 @@ def visit_annotated_value(self, node: ast.expr) -> None: if self.never_evaluates: return - if self.type == 'alias' or (self.type == 'annotation' and not self.import_visitor.futures_annotation): + if self.type == 'alias' or (self.type == 'annotation' and not self.import_visitor.are_annotations_deferred): # visit nodes in regular runtime context self.import_visitor.visit(node) return @@ -962,6 +965,7 @@ def visit_annotation_name(self, node: ast.Name) -> None: def visit_annotation_string(self, node: ast.Constant) -> None: """Collect all the names referenced inside the forward reference.""" + assert isinstance(node.value, str) visitor = StringAnnotationVisitor(self._typing_lookup) visitor.parse_and_visit_string_annotation(node.value) self.quoted_names.update(visitor.names) @@ -985,6 +989,7 @@ class ImportVisitor( def __init__( self, cwd: Path, + py314plus: bool, pydantic_enabled: bool, fastapi_enabled: bool, fastapi_dependency_support_enabled: bool, @@ -999,6 +1004,9 @@ def __init__( ) -> None: super().__init__() + #: Whether or not we should be using Python 3.14 semantics for annotations + self.py314plus = py314plus + #: Plugin settings self.pydantic_enabled = pydantic_enabled self.fastapi_enabled = fastapi_enabled @@ -1166,6 +1174,16 @@ def lookup_full_name(self, node: ast.AST) -> str | None: self._lookup_cache[node] = name return name + @property + def are_annotations_deferred(self) -> bool: + """ + Return whether or not annotations are deferred. + + This is the case when either there is a `from __future__ import annotations` + import present or we're targetting Python 3.14+. + """ + return self.py314plus or self.futures_annotation is True + def is_typing(self, node: ast.AST, symbol: str) -> bool: """Check if the given node matches the given typing symbol.""" full_name = self.lookup_full_name(node) @@ -1904,6 +1922,7 @@ class TypingOnlyImportsChecker: __slots__ = [ 'cwd', 'strict_mode', + 'py314plus', 'builtin_names', 'used_type_checking_names', 'visitor', @@ -1914,6 +1933,7 @@ class TypingOnlyImportsChecker: def __init__(self, node: ast.Module, options: Namespace | None) -> None: self.cwd = Path(os.getcwd()) self.strict_mode = getattr(options, 'type_checking_strict', False) + py314plus = getattr(options, 'type_checking_py314plus', False) # we use the same option as pyflakes to extend the list of builtins self.builtin_names = builtin_names @@ -1946,6 +1966,7 @@ def __init__(self, node: ast.Module, options: Namespace | None) -> None: self.visitor = ImportVisitor( self.cwd, + py314plus=py314plus, pydantic_enabled=pydantic_enabled, fastapi_enabled=fastapi_enabled, cattrs_enabled=cattrs_enabled, @@ -2124,13 +2145,13 @@ def missing_quotes_or_futures_import(self) -> Flake8Generator: self.visitor.force_future_annotation and (self.visitor.unwrapped_annotations or self.visitor.wrapped_annotations) ) - ) and not self.visitor.futures_annotation: + ) and not self.visitor.are_annotations_deferred: yield 1, 0, TC100, None def futures_excess_quotes(self) -> Flake8Generator: """TC101.""" # If futures imports are present, any ast.Constant captured in add_annotation should yield an error - if self.visitor.futures_annotation: + if self.visitor.are_annotations_deferred: for item in self.visitor.wrapped_annotations: if item.type != 'annotation': # TypeAlias value will not be affected by a futures import continue @@ -2204,7 +2225,7 @@ def excess_quotes(self) -> Flake8Generator: else: error = TC201.format(annotation=item.annotation) - if not self.visitor.futures_annotation: + if not self.visitor.are_annotations_deferred: yield item.lineno, item.col_offset, TC101.format(annotation=item.annotation), None yield item.lineno, item.col_offset, error, None diff --git a/flake8_type_checking/plugin.py b/flake8_type_checking/plugin.py index b3b70e3..23ac6d0 100644 --- a/flake8_type_checking/plugin.py +++ b/flake8_type_checking/plugin.py @@ -34,6 +34,13 @@ class Plugin: @classmethod def add_options(cls, option_manager: OptionManager) -> None: # pragma: no cover """Parse plugin options.""" + option_manager.add_option( + '--type-checking-py314plus', + action='store_true', + parse_from_config=True, + default=False, + help='Enables Python 3.14+ specific annotation semantics.', + ) option_manager.add_option( '--type-checking-typing-modules', comma_separated_list=True, diff --git a/tests/conftest.py b/tests/conftest.py index 7526cab..fbf2993 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -36,6 +36,7 @@ def _get_error(example: str, *, error_code_filter: Optional[str] = None, **kwarg mock_options.builtins = [] mock_options.extended_default_select = [] mock_options.enable_extensions = [] + mock_options.type_checking_py314plus = False mock_options.type_checking_pydantic_enabled = False mock_options.type_checking_exempt_modules = [] mock_options.type_checking_typing_modules = [] diff --git a/tests/test_attrs.py b/tests/test_attrs.py index 7abaf38..9df014b 100644 --- a/tests/test_attrs.py +++ b/tests/test_attrs.py @@ -28,8 +28,7 @@ def test_attrs_model(imp, dec): Test `attrs` classes together with a non-`attrs` class that has a class var of the same type. `attrs` classes are instantiated using different dataclass decorators. The `attrs` module is imported as whole. """ - example = textwrap.dedent( - f''' + example = textwrap.dedent(f''' {imp} from decimal import Decimal @@ -43,8 +42,7 @@ class Y: class Z: x: Decimal - ''' - ) + ''') assert _get_error(example, error_code_filter='TC001,TC002,TC003') == set() @@ -65,8 +63,7 @@ def test_complex_attrs_model(imp, dec, expected): Test `attrs` classes together with a non-`attrs` class tha has a class var of another type. `attrs` classes are instantiated using different dataclass decorators. The `attrs` module is imported as whole. """ - example = textwrap.dedent( - f''' + example = textwrap.dedent(f''' {imp} from decimals import Decimal from decimal import Context @@ -81,8 +78,7 @@ class Y: class Z: x: Context - ''' - ) + ''') assert _get_error(example, error_code_filter='TC001,TC002,TC003') == expected @@ -103,8 +99,7 @@ def test_complex_attrs_model_direct_import(imp, dec, expected): Test `attrs` classes together with a non-`attrs` class tha has a class var of another type. `attrs` classes are instantiated using different dataclass decorators which are imported as submodules. """ - example = textwrap.dedent( - f''' + example = textwrap.dedent(f''' {imp} from decimals import Decimal from decimal import Context @@ -119,8 +114,7 @@ class Y: class Z: x: Context - ''' - ) + ''') assert _get_error(example, error_code_filter='TC001,TC002,TC003') == expected @@ -150,8 +144,7 @@ def test_complex_attrs_model_as_import(imp, dec, expected): `attrs` classes are instantiated using different dataclass decorators which are imported as submodules using an alias. """ - example = textwrap.dedent( - f''' + example = textwrap.dedent(f''' {imp} from decimals import Decimal from decimal import Context @@ -166,8 +159,7 @@ class Y: class Z: x: Context - ''' - ) + ''') assert _get_error(example, error_code_filter='TC001,TC002,TC003') == expected @@ -193,8 +185,7 @@ def test_complex_attrs_model_slots_frozen(imp, dec, expected): Test `attrs` classes together with a non-`attrs` class tha has a class var of another type. `attrs` classes are instantiated using different dataclass decorators and arguments. """ - example = textwrap.dedent( - f''' + example = textwrap.dedent(f''' {imp} from decimals import Decimal from decimal import Context @@ -209,6 +200,5 @@ class Y: class Z: x: Context - ''' - ) + ''') assert _get_error(example, error_code_filter='TC001,TC002,TC003') == expected diff --git a/tests/test_errors.py b/tests/test_errors.py index 9050a68..b4fc18c 100644 --- a/tests/test_errors.py +++ b/tests/test_errors.py @@ -10,15 +10,13 @@ class TestFoundBugs: def test_mixed_errors(self): - example = textwrap.dedent( - f""" + example = textwrap.dedent(f""" import {mod} import pytest from x import y x: {mod} | pytest | y - """ - ) + """) assert _get_error(example) == { f"2:0 {TC001.format(module=f'{mod}')}", '3:0 ' + TC002.format(module='pytest'), @@ -26,8 +24,7 @@ def test_mixed_errors(self): } def test_type_checking_block_imports_dont_generate_errors(self): - example = textwrap.dedent( - """ + example = textwrap.dedent(""" import x from y import z @@ -40,8 +37,7 @@ def test_type_checking_block_imports_dont_generate_errors(self): def test(foo: z, bar: x): pass - """ - ) + """) assert _get_error(example) == { '2:0 ' + TC002.format(module='x'), '3:0 ' + TC002.format(module='y.z'), @@ -52,8 +48,7 @@ def test_model_declarations_dont_trigger_error(self): Initially found false positives in Django project, because name visitor did not capture the SomeModel usage in the example below. """ - example = textwrap.dedent( - """ + example = textwrap.dedent(""" from django.db import models from app.models import SomeModel @@ -62,14 +57,12 @@ class LoanProvider(models.Model): SomeModel, on_delete=models.CASCADE, ) - """ - ) + """) assert _get_error(example) == set() def test_all_list_declaration(self): """__all__ declarations originally generated false positives.""" - example = textwrap.dedent( - """ + example = textwrap.dedent(""" from app.models import SomeModel from another_app.models import AnotherModel @@ -77,14 +70,12 @@ def test_all_list_declaration(self): 'SomeModel', 'AnotherModel' ] - """ - ) + """) assert _get_error(example) == set() def test_all_tuple_declaration(self): """__all__ declarations originally generated false positives.""" - example = textwrap.dedent( - """ + example = textwrap.dedent(""" from app.models import SomeModel from another_app.models import AnotherModel @@ -92,14 +83,12 @@ def test_all_tuple_declaration(self): 'SomeModel', 'AnotherModel' ) - """ - ) + """) assert _get_error(example) == set() def test_callable_import(self): """__all__ declarations originally generated false positives.""" - example = textwrap.dedent( - """ + example = textwrap.dedent(""" from x import y class X: @@ -107,31 +96,25 @@ def __init__(self): self.all_sellable_models: list[CostModel] = y( country=self.country ) - """ - ) + """) assert _get_error(example) == set() def test_ellipsis(self): - example = textwrap.dedent( - """ + example = textwrap.dedent(""" x: Tuple[str, ...] - """ - ) + """) assert _get_error(example) == set() def test_literal(self): - example = textwrap.dedent( - """ + example = textwrap.dedent(""" from __future__ import annotations x: Literal['string'] - """ - ) + """) assert _get_error(example) == set() def test_conditional_import(self): - example = textwrap.dedent( - """ + example = textwrap.dedent(""" version = 2 if version == 2: @@ -140,8 +123,7 @@ def test_conditional_import(self): import y as x var: x - """ - ) + """) assert _get_error(example) == {"7:4 TC002 Move third-party import 'x' into a type-checking block"} def test_type_checking_block_formats_detected(self): @@ -327,8 +309,7 @@ def test_tc004_false_positive(self, example): def test_tc002_false_positive(self): """Re https://github.com/snok/flake8-type-checking/issues/120.""" - example = textwrap.dedent( - """ + example = textwrap.dedent(""" from logging import INFO from starlette.status import HTTP_500_INTERNAL_SERVER_ERROR @@ -336,8 +317,7 @@ def test_tc002_false_positive(self): class C: level: int = INFO status: int = HTTP_500_INTERNAL_SERVER_ERROR - """ - ) + """) assert _get_error(example) == set() def test_tc001_false_positive(self): @@ -346,15 +326,11 @@ def test_tc001_false_positive(self): def test_works_with_other_plugins(self, flake8_path): """Re https://github.com/snok/flake8-type-checking/issues/139.""" - (flake8_path / 'example.py').write_text( - textwrap.dedent( - ''' + (flake8_path / 'example.py').write_text(textwrap.dedent(''' def this_is_buggy(n): x = ++n return x - ''' - ) - ) + ''')) result = flake8_path.run_flake8() assert result.out_lines == [ './example.py:3:9: B002 Python does not support the unary prefix increment. ' @@ -363,10 +339,7 @@ def this_is_buggy(n): def test_shadowed_function_arg(self): """Re https://github.com/snok/flake8-type-checking/issues/160.""" - assert ( - _get_error( - textwrap.dedent( - ''' + assert _get_error(textwrap.dedent(''' from __future__ import annotations from typing import TYPE_CHECKING @@ -376,8 +349,4 @@ def test_shadowed_function_arg(self): def create(request: request.Request) -> None: str(request) - ''' - ) - ) - == set() - ) + ''')) == set() diff --git a/tests/test_exempt_modules.py b/tests/test_exempt_modules.py index 10d3701..77286c9 100644 --- a/tests/test_exempt_modules.py +++ b/tests/test_exempt_modules.py @@ -10,27 +10,23 @@ def test_exempt_modules_option(): which is meant to passlist certain modules from TC001 and TC002 errors. """ # Check that typing is passlisted when exempted - example = textwrap.dedent( - ''' + example = textwrap.dedent(''' from typing import TYPE_CHECKING from pandas import DataFrame x: DataFrame - ''' - ) + ''') assert _get_error(example, error_code_filter='TC002') == {'3:0 ' + TC002.format(module='pandas.DataFrame')} assert _get_error(example, error_code_filter='TC002', type_checking_exempt_modules=['pandas']) == set() # Check that other basic errors are still caught - example2 = textwrap.dedent( - ''' + example2 = textwrap.dedent(''' from typing import TYPE_CHECKING from pandas import DataFrame from a import B x: Callable[[DataFrame, B], List] - ''' - ) + ''') assert _get_error(example2, error_code_filter='TC002') == { '3:0 ' + TC002.format(module='pandas.DataFrame'), '4:0 ' + TC002.format(module='a.B'), @@ -40,26 +36,22 @@ def test_exempt_modules_option(): } # Check Import - example3 = textwrap.dedent( - ''' + example3 = textwrap.dedent(''' import pandas x: pandas.DataFrame - ''' - ) + ''') assert _get_error(example3, error_code_filter='TC002') == {'2:0 ' + TC002.format(module='pandas')} assert _get_error(example3, error_code_filter='TC002', type_checking_exempt_modules=['pandas']) == set() # Check template Import - example4 = textwrap.dedent( - ''' + example4 = textwrap.dedent(''' from apps.app_1.choices import ExampleChoice from apps.app_2.choices import Example2Choice x: ExampleChoice y: Example2Choice - ''' - ) + ''') assert _get_error(example4, error_code_filter='TC002') == { '2:0 ' + TC002.format(module='apps.app_1.choices.ExampleChoice'), '3:0 ' + TC002.format(module='apps.app_2.choices.Example2Choice'), diff --git a/tests/test_fastapi_decorators.py b/tests/test_fastapi_decorators.py index 5ae158a..5c10610 100644 --- a/tests/test_fastapi_decorators.py +++ b/tests/test_fastapi_decorators.py @@ -17,8 +17,7 @@ @pytest.mark.parametrize('fdef', ['def', 'async def']) def test_api_router_decorated_function(fdef): """Test sync and async function definition, with an arg and a kwarg.""" - example = textwrap.dedent( - f''' + example = textwrap.dedent(f''' from fastapi import APIRouter from app.models import SomeModel @@ -30,8 +29,7 @@ def test_api_router_decorated_function(fdef): @some_router.get('/{{resource_id}}') {fdef} list_something(resource_id: CustomType, some_model: SomeModel = Depends(some_function)): return None - ''' - ) + ''') assert _get_error(example, error_code_filter='TC001,TC002,TC003', **defaults) == set() @@ -41,8 +39,7 @@ def test_api_router_decorated_function_return_type(fdef): We don't care about return types. To my knowledge, these are not evaluated by FastAPI/pydantic. """ - example = textwrap.dedent( - f''' + example = textwrap.dedent(f''' from fastapi import APIRouter from fastapi import Request @@ -53,15 +50,13 @@ def test_api_router_decorated_function_return_type(fdef): @some_router.get('/{{resource_id}}') {fdef} list_something(request: Request) -> CustomType: return None - ''' - ) + ''') assert _get_error(example, error_code_filter='TC001,TC002,TC003', **defaults) == set() @pytest.mark.parametrize('fdef', ['def', 'async def']) def test_api_router_decorated_nested_function(fdef): - example = textwrap.dedent( - f''' + example = textwrap.dedent(f''' import logging from typing import TYPE_CHECKING @@ -81,15 +76,13 @@ def get_auth_router() -> APIRouter: {fdef} login(request: Request) -> "RedirectResponse": ... - ''' - ) + ''') assert _get_error(example, error_code_filter='TC001,TC002,TC003', **defaults) == set() @pytest.mark.parametrize('fdef', ['def', 'async def']) def test_app_decorated_function(fdef): - example = textwrap.dedent( - f''' + example = textwrap.dedent(f''' from app.main import app from app.models import SomeModel from app.types import CustomType @@ -97,6 +90,5 @@ def test_app_decorated_function(fdef): @app.get('/{{resource_id}}') {fdef} list_something(resource_id: CustomType, some_model: SomeModel = Depends(lambda: 1)): return None - ''' - ) + ''') assert _get_error(example, error_code_filter='TC001,TC002,TC003', **defaults) == set() diff --git a/tests/test_fastapi_dependencies.py b/tests/test_fastapi_dependencies.py index 253f28f..412715e 100644 --- a/tests/test_fastapi_dependencies.py +++ b/tests/test_fastapi_dependencies.py @@ -16,8 +16,7 @@ @pytest.mark.parametrize('fdef', ['def', 'async def']) def test_api_router_decorated_function(fdef): """Test sync and async function definition, with an arg and a kwarg.""" - example = textwrap.dedent( - f''' + example = textwrap.dedent(f''' from fastapi import APIRouter from app.models import SomeModel @@ -28,8 +27,7 @@ def test_api_router_decorated_function(fdef): {fdef} list_something(resource_id: CustomType, some_model: SomeModel = Depends(some_function)): return None - ''' - ) + ''') assert _get_error(example, error_code_filter='TC001,TC002,TC003', type_checking_fastapi_enabled=True) == { '4:0 ' + TC002.format(module='app.models.SomeModel'), '6:0 ' + TC002.format(module='app.types.CustomType'), diff --git a/tests/test_force_future_annotation.py b/tests/test_force_future_annotation.py index c28d445..74e18b1 100644 --- a/tests/test_force_future_annotation.py +++ b/tests/test_force_future_annotation.py @@ -6,13 +6,11 @@ def test_force_future_annotation(): """TC100 should be emitted even if there are no forward references to typing-only symbols.""" - example = textwrap.dedent( - ''' + 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 diff --git a/tests/test_import_visitors.py b/tests/test_import_visitors.py index 5be3dce..3fa86e7 100644 --- a/tests/test_import_visitors.py +++ b/tests/test_import_visitors.py @@ -15,6 +15,7 @@ def _visit(example: str) -> ImportVisitor: visitor = ImportVisitor( cwd=REPO_ROOT, + py314plus=False, pydantic_enabled=False, fastapi_enabled=False, fastapi_dependency_support_enabled=False, diff --git a/tests/test_injector.py b/tests/test_injector.py index d9267d0..c4889f9 100644 --- a/tests/test_injector.py +++ b/tests/test_injector.py @@ -17,15 +17,13 @@ ) def test_non_pydantic_model(enabled, expected): """A class does not use injector, so error should be risen in both scenarios.""" - example = textwrap.dedent( - ''' + example = textwrap.dedent(''' from services import Service class X: def __init__(self, service: Service) -> None: self.service = service - ''' - ) + ''') assert _get_error(example, error_code_filter='TC002', type_checking_pydantic_enabled=enabled) == expected @@ -38,16 +36,14 @@ def __init__(self, service: Service) -> None: ) def test_injector_option(enabled, expected): """When an injector option is enabled, injector should be ignored.""" - example = textwrap.dedent( - ''' + example = textwrap.dedent(''' from injector import Inject from services import Service class X: def __init__(self, service: Inject[Service]) -> None: self.service = service - ''' - ) + ''') assert _get_error(example, error_code_filter='TC002', type_checking_injector_enabled=enabled) == expected @@ -67,8 +63,7 @@ def __init__(self, service: Inject[Service]) -> None: ) def test_injector_option_all_annotations_in_function_are_runtime_dependencies(enabled, expected): """Whenever an argument is injected, all the other annotations are runtime required too.""" - example = textwrap.dedent( - ''' + example = textwrap.dedent(''' from injector import Inject from services import Service from other_dependency import OtherDependency @@ -77,15 +72,13 @@ class X: def __init__(self, service: Inject[Service], other: OtherDependency) -> None: self.service = service self.other = other - ''' - ) + ''') assert _get_error(example, error_code_filter='TC002', type_checking_injector_enabled=enabled) == expected def test_injector_option_require_injections_under_unpack(): """Whenever an injector option is enabled, injected dependencies should be ignored, even if unpacked.""" - example = textwrap.dedent( - """ + example = textwrap.dedent(""" from typing import Unpack from injector import Inject from services import ServiceKwargs @@ -93,8 +86,7 @@ class X: def __init__(self, service: Inject[Service], **kwargs: Unpack[ServiceKwargs]) -> None: self.service = service self.args = args - """ - ) + """) assert _get_error(example, error_code_filter='TC002', type_checking_injector_enabled=True) == set() @@ -107,16 +99,14 @@ def __init__(self, service: Inject[Service], **kwargs: Unpack[ServiceKwargs]) -> ) def test_injector_option_allows_injector_as_module(enabled, expected): """Whenever an injector option is enabled, injected dependencies should be ignored, even if import as module.""" - example = textwrap.dedent( - ''' + example = textwrap.dedent(''' import injector from services import Service class X: def __init__(self, service: injector.Inject[Service]) -> None: self.service = service - ''' - ) + ''') assert _get_error(example, error_code_filter='TC002', type_checking_injector_enabled=enabled) == expected @@ -129,8 +119,7 @@ def __init__(self, service: injector.Inject[Service]) -> None: ) def test_injector_option_only_mentioned_second_time(enabled, expected): """Whenever an injector option is enabled, dependency referenced second time is accepted.""" - example = textwrap.dedent( - """ + example = textwrap.dedent(""" from injector import Inject from services import Service @@ -138,6 +127,5 @@ class X: def __init__(self, service: Inject[Service], other_deps: list[Service]) -> None: self.service = service self.other_deps = other_deps - """ - ) + """) assert _get_error(example, error_code_filter='TC002', type_checking_injector_enabled=enabled) == expected diff --git a/tests/test_name_extraction.py b/tests/test_name_extraction.py index 7f898c9..2e20e37 100644 --- a/tests/test_name_extraction.py +++ b/tests/test_name_extraction.py @@ -39,6 +39,7 @@ def test_name_extraction(example, expected): import_visitor = ImportVisitor( cwd='fake cwd', # type: ignore[arg-type] + py314plus=False, pydantic_enabled=False, fastapi_enabled=False, fastapi_dependency_support_enabled=False, diff --git a/tests/test_name_visitor.py b/tests/test_name_visitor.py index 65708ae..36f041d 100644 --- a/tests/test_name_visitor.py +++ b/tests/test_name_visitor.py @@ -12,6 +12,7 @@ def _get_names_and_soft_uses(example: str) -> tuple[set[str], set[str]]: visitor = ImportVisitor( cwd='fake cwd', # type: ignore[arg-type] + py314plus=False, pydantic_enabled=False, fastapi_enabled=False, fastapi_dependency_support_enabled=False, @@ -45,19 +46,16 @@ def _get_names_and_soft_uses(example: str) -> tuple[set[str], set[str]]: # Attribute ('x.y', {'x.y', 'x'}, set()), ( - textwrap.dedent( - """ + textwrap.dedent(""" def example(c): a = 2 b = c * 2 - """ - ), + """), {'a', 'b', 'c'}, set(), ), ( - textwrap.dedent( - """ + textwrap.dedent(""" class Test: x = 13 @@ -66,51 +64,43 @@ def __init__(self, z): a = Test() b = a.y - """ - ), + """), {'self.y', 'z', 'Test', 'self', 'a', 'b', 'x', 'a.y'}, set(), ), ( - textwrap.dedent( - """ + textwrap.dedent(""" import ast ImportType = Union[Import, ImportFrom] - """ - ), # ast should not be a part of this + """), # ast should not be a part of this {'Union', 'Import', 'ImportFrom', 'ImportType'}, set(), ), ( - textwrap.dedent( - """ + textwrap.dedent(""" import ast def _get_usages(example): visitor = UnusedImportVisitor() visitor.visit(parse(example)) return visitor.usage_names - """ - ), + """), {'UnusedImportVisitor', 'example', 'parse', 'visitor', 'visitor.usage_names', 'visitor.visit'}, set(), ), ( - textwrap.dedent( - """ + textwrap.dedent(""" from typing import Annotated from foo import Gt x: Annotated[int, Gt(5)] - """ - ), + """), {'Gt'}, {'int'}, ), ( - textwrap.dedent( - """ + textwrap.dedent(""" from __future__ import annotations from typing import Annotated @@ -118,8 +108,7 @@ def _get_usages(example): from foo import Gt x: Annotated[int, Gt(5)] - """ - ), + """), set(), {'Gt', 'int'}, ), @@ -129,15 +118,13 @@ def _get_usages(example): examples.extend( [ ( - textwrap.dedent( - """ + textwrap.dedent(""" from typing import Annotated from foo import Gt type x = Annotated[int, Gt(5)] - """ - ), + """), set(), {'Gt', 'int'}, ), @@ -152,8 +139,7 @@ def test_basic_annotations_are_removed(example, result, soft_uses): def test_model_declarations_are_included_in_names(): """Class definition arguments need to be included in our "names".""" - example = textwrap.dedent( - """ + example = textwrap.dedent(""" from django.db import models from app.models import SomeModel @@ -162,8 +148,7 @@ class LoanProvider(models.Model): SomeModel, on_delete=models.CASCADE, ) - """ - ) + """) assert _get_names_and_soft_uses(example) == ( {'SomeModel', 'fk', 'models', 'models.CASCADE', 'models.ForeignKey', 'models.Model'}, set(), diff --git a/tests/test_pydantic.py b/tests/test_pydantic.py index ec68fe6..0f8a716 100644 --- a/tests/test_pydantic.py +++ b/tests/test_pydantic.py @@ -25,14 +25,12 @@ def test_non_pydantic_model(enabled, expected): A class cannot be a pydantic model if it doesn't have a base class, so we should raise the same error here in both cases. """ - example = textwrap.dedent( - ''' + example = textwrap.dedent(''' from pandas import DataFrame class X: x: DataFrame - ''' - ) + ''') assert _get_error(example, error_code_filter='TC002', type_checking_pydantic_enabled=enabled) == expected @@ -42,21 +40,18 @@ def test_class_with_base_class(): to assume it might be a pydantic model, for which we need to register annotations as uses. """ - example = textwrap.dedent( - ''' + example = textwrap.dedent(''' from pandas import DataFrame class X(Y): x: DataFrame - ''' - ) + ''') assert _get_error(example, error_code_filter='TC002', type_checking_pydantic_enabled=True) == set() def test_complex_pydantic_model(): """Test actual Pydantic models, with different annotation types.""" - example = textwrap.dedent( - ''' + example = textwrap.dedent(''' from __future__ import annotations from datetime import datetime @@ -100,24 +95,21 @@ class FinalModel(ModelBase): f: NestedModel g: condecimal(ge=Decimal(0)) = Decimal(0) - ''' - ) + ''') assert _get_error(example, error_code_filter='TC002', type_checking_pydantic_enabled=True) == set() @pytest.mark.parametrize('c', ['NamedTuple', 'TypedDict']) def test_type_checking_pydantic_enabled_baseclass_passlist(c): """Test that named tuples are not ignored.""" - example = textwrap.dedent( - f''' + example = textwrap.dedent(f''' from typing import {c} from x import Y, Z class ModelBase({c}): a: Y[str] b: Z[int] - ''' - ) + ''') assert _get_error( example, error_code_filter='TC002', @@ -132,38 +124,33 @@ class ModelBase({c}): @pytest.mark.parametrize('f', ['def', 'async def']) def test_type_checking_pydantic_enabled_validate_arguments_decorator(f): """Test that @validate_argument-decorated functions have their annotations ignored.""" - example = textwrap.dedent( - f''' + example = textwrap.dedent(f''' from pydantic import validate_arguments from x import Y, Z @validate_arguments {f} f(y: Y) -> Z: pass - ''' - ) + ''') assert _get_error(example, error_code_filter='TC002', type_checking_pydantic_enabled=True) == set() @pytest.mark.parametrize('f', ['def', 'async def']) def test_type_checking_pydantic_enabled_validate_arguments_decorator_alias(f): - example = textwrap.dedent( - f''' + example = textwrap.dedent(f''' from pydantic import validate_arguments as va from x import Y, Z @va {f} f(y: Y) -> Z: pass - ''' - ) + ''') assert _get_error(example, error_code_filter='TC002', type_checking_pydantic_enabled=True) == set() @pytest.mark.parametrize('f', ['def', 'async def']) def test_type_checking_pydantic_enabled_validate_arguments_decorator_method(f): - example = textwrap.dedent( - f''' + example = textwrap.dedent(f''' from pydantic import validate_arguments from x import Y, Z @@ -171,6 +158,5 @@ class Test: @validate_arguments {f} f(self, y: Y) -> Z: pass - ''' - ) + ''') assert _get_error(example, error_code_filter='TC002', type_checking_pydantic_enabled=True) == set() diff --git a/tests/test_should_warn.py b/tests/test_should_warn.py index e357503..20d62a3 100644 --- a/tests/test_should_warn.py +++ b/tests/test_should_warn.py @@ -20,15 +20,11 @@ def test_version(flake8_path): def test_tc_is_enabled_with_config(flake8_path): (flake8_path / 'setup.cfg').write_text('[flake8]\nselect = TC') - (flake8_path / 'example.py').write_text( - dedent( - ''' + (flake8_path / 'example.py').write_text(dedent(''' from x import Y x: Y[str, int] = 1 - ''' - ) - ) + ''')) result = flake8_path.run_flake8() assert result.out_lines == [ f".{os.sep}example.py:2:1: TC002 Move third-party import 'x.Y' into a type-checking block" @@ -37,79 +33,59 @@ def test_tc_is_enabled_with_config(flake8_path): def test_tc1_and_tc2_are_disabled_by_default(flake8_path): (flake8_path / 'setup.cfg').write_text('') - (flake8_path / 'example.py').write_text( - dedent( - ''' + (flake8_path / 'example.py').write_text(dedent(''' from typing import TYPE_CHECKING if TYPE_CHECKING: from typing import Union x: Union[str, int] - ''' - ) - ) + ''')) result = flake8_path.run_flake8() assert result.out_lines == [] def test_tc1_and_tc2_are_disabled_by_default_when_tc_is_enabled(flake8_path): (flake8_path / 'setup.cfg').write_text('[flake8]\nselect = TC') - (flake8_path / 'example.py').write_text( - dedent( - ''' + (flake8_path / 'example.py').write_text(dedent(''' from typing import TYPE_CHECKING if TYPE_CHECKING: from typing import Union x: Union[str, int] - ''' - ) - ) + ''')) result = flake8_path.run_flake8() assert result.out_lines == [] def test_tc1_works_when_opted_in(flake8_path): (flake8_path / 'setup.cfg').write_text('[flake8]\nselect = TC1') - (flake8_path / 'example.py').write_text( - dedent( - ''' + (flake8_path / 'example.py').write_text(dedent(''' from typing import TYPE_CHECKING if TYPE_CHECKING: from typing import Union x: Union[str, int] - ''' - ) - ) + ''')) result = flake8_path.run_flake8() assert result.out_lines == [f".{os.sep}example.py:1:1: TC100 Add 'from __future__ import annotations' import"] def test_tc2_works_when_opted_in(flake8_path): - (flake8_path / 'setup.cfg').write_text( - dedent( - """\ + (flake8_path / 'setup.cfg').write_text(dedent("""\ [flake8] select = TC2 - """ - ) - ) - (flake8_path / 'example.py').write_text( - dedent( - ''' + """)) + (flake8_path / 'example.py').write_text(dedent(''' from typing import TYPE_CHECKING if TYPE_CHECKING: from typing import Union x: Union[str, int] - ''' - ) - ) + ''')) result = flake8_path.run_flake8() assert result.out_lines == [ f".{os.sep}example.py:7:4: TC200 Annotation 'Union' needs to be made into a string literal" @@ -117,13 +93,9 @@ def test_tc2_works_when_opted_in(flake8_path): def test_pyi_ignored(flake8_path): - (flake8_path / 'example.pyi').write_text( - dedent( - ''' + (flake8_path / 'example.pyi').write_text(dedent(''' import pandas x: pandas.DataFrame - ''' - ) - ) + ''')) result = flake8_path.run_flake8() assert result.out_lines == [] diff --git a/tests/test_sqlalchemy.py b/tests/test_sqlalchemy.py index 52a27ff..163fbd7 100644 --- a/tests/test_sqlalchemy.py +++ b/tests/test_sqlalchemy.py @@ -25,15 +25,13 @@ def test_simple_mapped_use(enabled, expected): Mapped itself must be available at runtime and the inner type may or may not need to be available at runtime. """ - example = textwrap.dedent( - ''' + example = textwrap.dedent(''' from foo import Bar from sqlalchemy.orm import Mapped class User: x: Mapped[Bar] - ''' - ) + ''') assert _get_error(example, error_code_filter='TC002', type_checking_sqlalchemy_enabled=enabled) == expected @@ -51,15 +49,13 @@ class User: ) def test_default_mapped_names(name, expected): """Check the three default names and a bogus name.""" - example = textwrap.dedent( - f''' + example = textwrap.dedent(f''' from foo import Bar from sqlalchemy.orm import {name} class User: x: {name}[Bar] - ''' - ) + ''') assert _get_error(example, error_code_filter='TC002', type_checking_sqlalchemy_enabled=True) == expected @@ -68,16 +64,14 @@ def test_mapped_with_circular_forward_reference(): Mapped must still be available at runtime even with forward references to a different model. """ - example = textwrap.dedent( - ''' + example = textwrap.dedent(''' from sqlalchemy.orm import Mapped if TYPE_CHECKING: from .address import Address class User: address: Mapped['Address'] - ''' - ) + ''') assert _get_error(example, error_code_filter='TC002', type_checking_sqlalchemy_enabled=True) == set() @@ -87,8 +81,7 @@ def test_mapped_soft_uses(): as such we can't trigger a TC002 here, despite the only uses being inside type annotations. """ - example = textwrap.dedent( - ''' + example = textwrap.dedent(''' from foo import Bar, Gt from sqlalchemy.orm import Mapped from typing import Annotated @@ -97,8 +90,7 @@ class User: number: Mapped[Annotated[Bar, Gt(2)]] bar: Bar validator: Gt - ''' - ) + ''') assert _get_error(example, error_code_filter='TC002', type_checking_sqlalchemy_enabled=True) == set() @@ -107,16 +99,14 @@ def test_mapped_use_without_runtime_import(): Mapped must be available at runtime, so even if it is inside a wrapped annotation we should raise a TC004 for Mapped but not for Bar """ - example = textwrap.dedent( - ''' + example = textwrap.dedent(''' if TYPE_CHECKING: from foo import Bar from sqlalchemy.orm import Mapped class User: created: 'Mapped[Bar]' - ''' - ) + ''') assert _get_error(example, error_code_filter='TC004', type_checking_sqlalchemy_enabled=True) == { '4:0 ' + TC004.format(module='Mapped') } @@ -127,8 +117,7 @@ def test_custom_mapped_dotted_names_unwrapped(): Check a couple of custom dotted names and a bogus one. This also tests the various styles of imports """ - example = textwrap.dedent( - ''' + example = textwrap.dedent(''' import a import a.b as ab from a import b @@ -145,8 +134,7 @@ class User: x: b.MyMapped[Bar] y: a.b.MyMapped[Bar] z: ab.MyMapped[Bar] - ''' - ) + ''') assert _get_error( example, error_code_filter='TC002', @@ -161,8 +149,7 @@ def test_custom_mapped_dotted_names_wrapped(): Same as the unwrapped test but with wrapped annotations. This should generate a bunch of TC004 errors for the uses of mapped that should be available at runtime. """ - example = textwrap.dedent( - ''' + example = textwrap.dedent(''' if TYPE_CHECKING: import a import a.b as ab @@ -180,8 +167,7 @@ class User: x: 'b.MyMapped[Bar]' y: 'a.b.MyMapped[Bar]' z: 'ab.MyMapped[Bar]' - ''' - ) + ''') assert _get_error( example, error_code_filter='TC004', diff --git a/tests/test_strict.py b/tests/test_strict.py index ac00f09..7c50990 100644 --- a/tests/test_strict.py +++ b/tests/test_strict.py @@ -9,14 +9,12 @@ def test_strict_mode(): Assert that imports are flagged for TC00[1-3] on a per-module basis by default, but individually when --type-checking-strict is set to true. """ - example = textwrap.dedent( - ''' + example = textwrap.dedent(''' from x import Y, Z a = Y b: Z - ''' - ) + ''') assert _get_error(example, error_code_filter='TC002', type_checking_strict=False) == set() assert _get_error(example, error_code_filter='TC002', type_checking_strict=True) == { '2:0 ' + TC002.format(module='x.Z') diff --git a/tests/test_tc001_to_tc003.py b/tests/test_tc001_to_tc003.py index ec097b1..dfcebf7 100644 --- a/tests/test_tc001_to_tc003.py +++ b/tests/test_tc001_to_tc003.py @@ -124,8 +124,7 @@ def get_tc_001_to_003_tests(import_: str, ERROR: str) -> L: # Imports used for `functools.singledispatch`. None of these should generate errors. used_for_singledispatch: L = [ ( - textwrap.dedent( - f''' + textwrap.dedent(f''' import functools from {import_} import Dict, Any @@ -133,13 +132,11 @@ def get_tc_001_to_003_tests(import_: str, ERROR: str) -> L: @functools.singledispatch def foo(arg: Dict[str, Any]) -> Any: return 1 - ''' - ), + '''), set(), ), ( - textwrap.dedent( - f''' + textwrap.dedent(f''' from functools import singledispatch from {import_} import Dict, Any @@ -147,13 +144,11 @@ def foo(arg: Dict[str, Any]) -> Any: @singledispatch def foo(arg: Dict[str, Any]) -> Any: return 1 - ''' - ), + '''), set(), ), ( - textwrap.dedent( - f''' + textwrap.dedent(f''' from functools import singledispatchmethod from {import_} import Dict, Any @@ -162,42 +157,36 @@ class Foo: @singledispatchmethod def foo(self, arg: Dict[str, Any]) -> Any: return 1 - ''' - ), + '''), set(), ), ] other_useful_test_cases: L = [ ( - textwrap.dedent( - f''' + textwrap.dedent(f''' from {import_} import Dict, Any def example() -> Any: return 1 x: Dict[int] = 20 - ''' - ), + '''), {'2:0 ' + ERROR.format(module=f'{import_}.Dict'), '2:0 ' + ERROR.format(module=f'{import_}.Any')}, ), ( - textwrap.dedent( - ''' + textwrap.dedent(''' from typing import TYPE_CHECKING if TYPE_CHECKING: from typing import Dict x: Dict[int] = 20 - ''' - ), + '''), set(), ), ( - textwrap.dedent( - ''' + textwrap.dedent(''' from pathlib import Path class ImportVisitor(ast.NodeTransformer): @@ -211,92 +200,78 @@ class ExampleClass: def __init__(self): self.cwd = Path(pandas.getcwd()) - ''' - ), + '''), set(), ), ( - textwrap.dedent( - f''' + textwrap.dedent(f''' import {import_} class Migration: enum={import_} - ''' - ), + '''), set(), ), ( - textwrap.dedent( - f''' + textwrap.dedent(f''' import {import_} class Migration: enum={import_}.EnumClass - ''' - ), + '''), set(), ), ( - textwrap.dedent( - f''' + textwrap.dedent(f''' from {import_} import y if TYPE_CHECKING: _type = x else: _type = y - ''' - ), + '''), set(), ), ( - textwrap.dedent( - f''' + textwrap.dedent(f''' from {import_} import y if TYPE_CHECKING: _type = x elif True: _type = y - ''' - ), + '''), set(), ), # Annotated soft use ( - textwrap.dedent( - f''' + textwrap.dedent(f''' from typing import Annotated from {import_} import Depends x: Annotated[str, Depends] y: Depends - ''' - ), + '''), set(), ), # This is not a soft-use, it's just a plain string ( - textwrap.dedent( - f''' + textwrap.dedent(f''' from typing import Annotated from {import_} import Depends x: Annotated[str, "Depends"] y: Depends - ''' - ), + '''), {'4:0 ' + ERROR.format(module=f'{import_}.Depends')}, ), # global ( - textwrap.dedent( - f''' + textwrap.dedent(f''' from {import_} import Dict def example() -> Any: @@ -304,14 +279,12 @@ def example() -> Any: x = Dict[int] # runtime use of Dict import x: Dict[int] = 20 - ''' - ), + '''), set(), ), # nonlocal ( - textwrap.dedent( - f''' + textwrap.dedent(f''' from {import_} import Dict def example() -> Any: @@ -319,32 +292,27 @@ def example() -> Any: x = Dict[int] # runtime use of Dict import x: Dict[int] = 20 - ''' - ), + '''), set(), ), # Issue #127 ( - textwrap.dedent( - f''' + textwrap.dedent(f''' from {import_} import Foo from typing import Any, cast a = cast('Foo', 1) - ''' - ), + '''), {'2:0 ' + ERROR.format(module=f'{import_}.Foo')}, ), # forward reference in sub-expression of cast type ( - textwrap.dedent( - f''' + textwrap.dedent(f''' from {import_} import Foo from typing import Any, cast a = cast(list['Foo'], 1) - ''' - ), + '''), {'2:0 ' + ERROR.format(module=f'{import_}.Foo')}, ), ] diff --git a/tests/test_tc004.py b/tests/test_tc004.py index bd4a28b..684073c 100644 --- a/tests/test_tc004.py +++ b/tests/test_tc004.py @@ -17,22 +17,19 @@ ('', set()), # Used in file ( - textwrap.dedent( - """ + textwrap.dedent(""" from typing import TYPE_CHECKING if TYPE_CHECKING: from datetime import datetime x = datetime - """ - ), + """), {'5:0 ' + TC004.format(module='datetime')}, ), # Used in function ( - textwrap.dedent( - """ + textwrap.dedent(""" from typing import TYPE_CHECKING if TYPE_CHECKING: @@ -40,26 +37,22 @@ def example(): return date() - """ - ), + """), {'5:0 ' + TC004.format(module='date')}, ), # Used, but only used inside the type checking block ( - textwrap.dedent( - """ + textwrap.dedent(""" if TYPE_CHECKING: from typing import Any CustomType = Any - """ - ), + """), set(), ), # Used for typing only ( - textwrap.dedent( - """ + textwrap.dedent(""" if TYPE_CHECKING: from typing import Any @@ -67,39 +60,33 @@ def example(*args: Any, **kwargs: Any): return my_type: Type[Any] | Any - """ - ), + """), set(), ), ( - textwrap.dedent( - """ + textwrap.dedent(""" if TYPE_CHECKING: from typing import List, Sequence, Set def example(a: List[int], /, b: Sequence[int], *, c: Set[int]): return - """ - ), + """), set(), ), # Used different places, but where each function scope has it's own import ( - textwrap.dedent( - """ + textwrap.dedent(""" if TYPE_CHECKING: from pandas import DataFrame def example(): from pandas import DataFrame x = DataFrame - """ - ), + """), set(), ), ( - textwrap.dedent( - """ + textwrap.dedent(""" from __future__ import annotations from typing import TYPE_CHECKING @@ -112,13 +99,11 @@ class Example: async def example(self) -> AsyncIterator[List[str]]: yield 0 - """ - ), + """), set(), ), ( - textwrap.dedent( - """ + textwrap.dedent(""" from typing import TYPE_CHECKING from weakref import WeakKeyDictionary @@ -127,13 +112,11 @@ async def example(self) -> AsyncIterator[List[str]]: d = WeakKeyDictionary["Any", "Any"]() - """ - ), + """), set(), ), ( - textwrap.dedent( - """ + textwrap.dedent(""" if TYPE_CHECKING: import a import b @@ -142,15 +125,13 @@ async def example(self) -> AsyncIterator[List[str]]: def test_function(a, /, b, *, c, **d): print(a, b, c, d) - """ - ), + """), set(), ), # Regression test for #131 # handle scopes correctly ( - textwrap.dedent( - """ + textwrap.dedent(""" if TYPE_CHECKING: from a import Foo @@ -171,16 +152,14 @@ class X: bar: Foo = Foo() - """ - ), + """), set(), ), # Inverse Regression test for #131 # handle scopes correctly, so we should get an error for the imports # in the inner scopes, but not one for the outer scope. ( - textwrap.dedent( - """ + textwrap.dedent(""" if TYPE_CHECKING: from a import Foo @@ -197,8 +176,7 @@ class X: bar: Foo = Foo() - """ - ), + """), { '7:0 ' + TC004.format(module='Foo'), '14:0 ' + TC004.format(module='Foo'), @@ -215,8 +193,7 @@ class X: # special rules (such as being able to access enclosing class scopes) # so it's either to not treat them as separate scopes for now. ( - textwrap.dedent( - """ + textwrap.dedent(""" if TYPE_CHECKING: from foo import v, w, x, y, z @@ -226,14 +203,12 @@ class X: {{y: baz for y, bar in foo for baz in y}} foo = z if (z := bar) else None - """ - ), + """), set(), ), # Inverse test for complex cases ( - textwrap.dedent( - """ + textwrap.dedent(""" if TYPE_CHECKING: from foo import v, w, x, y, z @@ -243,8 +218,7 @@ class X: {{a: y for a in foo}} x = foo if (foo := z) else None - """ - ), + """), { '3:0 ' + TC004.format(module='v'), '3:0 ' + TC004.format(module='w'), @@ -255,8 +229,7 @@ class X: ), # functools.singledispatch ( - textwrap.dedent( - """ + textwrap.dedent(""" import functools if TYPE_CHECKING: @@ -265,13 +238,11 @@ class X: @functools.singledispatch def foo(arg: FooType) -> int: return 1 - """ - ), + """), {'5:0 ' + TC004.format(module='FooType')}, ), ( - textwrap.dedent( - """ + textwrap.dedent(""" from functools import singledispatch if TYPE_CHECKING: @@ -280,13 +251,11 @@ def foo(arg: FooType) -> int: @functools.singledispatch def foo(arg: FooType) -> int: return 1 - """ - ), + """), {'5:0 ' + TC004.format(module='FooType')}, ), ( - textwrap.dedent( - """ + textwrap.dedent(""" from functools import singledispatchmethod if TYPE_CHECKING: @@ -296,13 +265,17 @@ class Foo: @functools.singledispatch def foo(self, arg: FooType) -> int: return 1 - """ - ), + """), {'5:0 ' + TC004.format(module='FooType')}, ), ] +@pytest.mark.parametrize('py314plus', [False, True]) @pytest.mark.parametrize(('example', 'expected'), examples) -def test_TC004_errors(example, expected): - assert _get_error(example, error_code_filter='TC004') == expected +def test_TC004_errors(example, expected, py314plus): + assert _get_error(example, error_code_filter='TC004', type_checking_py314plus=py314plus) == expected + if py314plus and 'from __future__ import annotations' in example: + # removing the future annotation should not change the outcome + example = example.replace('from __future__ import annotations', '') + assert _get_error(example, error_code_filter='TC004', type_checking_py314plus=py314plus) == expected diff --git a/tests/test_tc005.py b/tests/test_tc005.py index 51f2ac5..885f7ff 100644 --- a/tests/test_tc005.py +++ b/tests/test_tc005.py @@ -19,66 +19,56 @@ ('', set()), # Found in file ( - textwrap.dedent( - """ + textwrap.dedent(""" from typing import TYPE_CHECKING if TYPE_CHECKING: pass - """ - ), + """), {'4:0 ' + TC005}, ), # Found in function ( - textwrap.dedent( - """ + textwrap.dedent(""" from typing import TYPE_CHECKING def example(): if TYPE_CHECKING: pass return - """ - ), + """), {'5:0 ' + TC005}, ), # Found in class ( - textwrap.dedent( - """ + textwrap.dedent(""" from typing import TYPE_CHECKING class Test: if TYPE_CHECKING: pass x = 2 - """ - ), + """), {'5:0 ' + TC005}, ), ( - textwrap.dedent( - """ + textwrap.dedent(""" from typing import TYPE_CHECKING if TYPE_CHECKING: if 2: pass - """ - ), + """), set(), ), ( - textwrap.dedent( - """ + textwrap.dedent(""" from typing import TYPE_CHECKING from typing import List if TYPE_CHECKING: x: List - """ - ), + """), set(), ), ] diff --git a/tests/test_tc006.py b/tests/test_tc006.py index 7cfd92e..59c36f3 100644 --- a/tests/test_tc006.py +++ b/tests/test_tc006.py @@ -21,132 +21,108 @@ ('', set()), # Simple type unquoted ( - textwrap.dedent( - """ + textwrap.dedent(""" from typing import cast cast(int, 3.0) - """ - ), + """), {'4:5 ' + TC006.format(annotation='int')}, ), # Complex type unquoted ( - textwrap.dedent( - """ + textwrap.dedent(""" from typing import cast cast(list[tuple[bool | float | int | str]], 3.0) - """ - ), + """), {'4:5 ' + TC006.format(annotation='list[tuple[bool | float | int | str]]')}, ), # Complex type unquoted using Union ( - textwrap.dedent( - """ + textwrap.dedent(""" from typing import Union, cast cast(list[tuple[Union[bool, float, int, str]]], 3.0) - """ - ), + """), {'4:5 ' + TC006.format(annotation='list[tuple[Union[bool, float, int, str]]]')}, ), # Simple type quoted ( - textwrap.dedent( - """ + textwrap.dedent(""" from typing import cast cast("int", 3.0) - """ - ), + """), set(), ), # Complex type quoted ( - textwrap.dedent( - """ + textwrap.dedent(""" from typing import cast cast("list[tuple[bool | float | int | str]]", 3.0) - """ - ), + """), set(), ), # Complex type quoted using Union ( - textwrap.dedent( - """ + textwrap.dedent(""" from typing import Union, cast cast("list[tuple[Union[bool, float, int, str]]]", 3.0) - """ - ), + """), set(), ), # Call aliased function ( - textwrap.dedent( - """ + textwrap.dedent(""" from typing import cast as typecast typecast(int, 3.0) - """ - ), + """), {'4:9 ' + TC006.format(annotation='int')}, ), # Call function from module ( - textwrap.dedent( - """ + textwrap.dedent(""" import typing typing.cast(int, 3.0) - """ - ), + """), {'4:12 ' + TC006.format(annotation='int')}, ), # Call function from aliased module ( - textwrap.dedent( - """ + textwrap.dedent(""" import typing as t t.cast(int, 3.0) - """ - ), + """), {'4:7 ' + TC006.format(annotation='int')}, ), # re-export of cast using a registered compat module ( - textwrap.dedent( - """ + textwrap.dedent(""" from mylib import compat compat.cast(int, 3.0) - """ - ), + """), {'4:12 ' + TC006.format(annotation='int')}, ), ( - textwrap.dedent( - """ + textwrap.dedent(""" from .compat import cast cast(int, 3.0) - """ - ), + """), {'4:5 ' + TC006.format(annotation='int')}, ), ( - textwrap.dedent( - """ + textwrap.dedent(""" from ..compat import cast cast(int, 3.0) - """ - ), + """), {'4:5 ' + TC006.format(annotation='int')}, ), ] diff --git a/tests/test_tc007.py b/tests/test_tc007.py index 8fca26d..6b736e8 100644 --- a/tests/test_tc007.py +++ b/tests/test_tc007.py @@ -22,8 +22,7 @@ ('if TYPE_CHECKING:\n\tfrom typing import Dict\nx: TypeAlias = Dict[int]', {'3:15 ' + TC007.format(alias='Dict')}), # Regression test for issue #163 ( - textwrap.dedent( - ''' + textwrap.dedent(''' from typing import TYPE_CHECKING if TYPE_CHECKING: @@ -31,14 +30,12 @@ from typing_extensions import TypeAlias Foo: TypeAlias = Sequence[int] - ''' - ), + '''), set(), ), # Inverse regression test for issue #163 ( - textwrap.dedent( - ''' + textwrap.dedent(''' from typing import TYPE_CHECKING from typing_extensions import TypeAlias @@ -46,8 +43,7 @@ from collections.abc import Sequence Foo: TypeAlias = Sequence[int] - ''' - ), + '''), { '8:17 ' + TC007.format(alias='Sequence'), }, @@ -58,14 +54,12 @@ # RHS on an explicit TypeAlias with 3.12 syntax should not emit a TC007 examples.append( ( - textwrap.dedent( - ''' + textwrap.dedent(''' if TYPE_CHECKING: from collections.abc import Sequence type Foo = Sequence[int] - ''' - ), + '''), set(), ) ) diff --git a/tests/test_tc008.py b/tests/test_tc008.py index 84fd52c..c413047 100644 --- a/tests/test_tc008.py +++ b/tests/test_tc008.py @@ -16,8 +16,6 @@ examples = [ ('', set()), ("x: TypeAlias = 'int'", {'1:15 ' + TC008.format(alias='int')}), - # this should emit a TC010 instead - ("x: TypeAlias = 'int' | None", set()), # this used to emit an error before fixing #164 if we wanted to handle # this case once again we could add a whitelist of subscriptable types ("x: TypeAlias = 'Dict[int]'", set()), @@ -33,60 +31,51 @@ {'4:20 ' + TC008.format(alias='int')}, ), ( - textwrap.dedent( - ''' + textwrap.dedent(''' from __future__ import annotations if TYPE_CHECKING: import something x: TypeAlias = "something" - ''' - ), + '''), set(), ), ( # Regression test for Issue #164 - textwrap.dedent( - ''' + textwrap.dedent(''' from wtforms import Field from wtforms.fields.core import UnboundField foo: TypeAlias = 'UnboundField[Field]' - ''' - ), + '''), set(), ), ( # this used to yield false negatives but works now, yay - textwrap.dedent( - ''' + textwrap.dedent(''' class Foo(Protocol): pass x: TypeAlias = 'Foo | None' - ''' - ), + '''), {'5:15 ' + TC008.format(alias='Foo | None')}, ), ( # Regression test for Issue #168 - textwrap.dedent( - ''' + textwrap.dedent(''' if TYPE_CHECKING: Foo: TypeAlias = str | int Bar: TypeAlias = 'Foo' - ''' - ), + '''), set(), ), ( # Regression test for Issue #168 # The runtime declaration are inside a Protocol so they should not # affect the outcome - textwrap.dedent( - ''' + textwrap.dedent(''' if TYPE_CHECKING: Foo: TypeAlias = str | int else: @@ -94,8 +83,7 @@ class X(Protocol): Foo: str | int Bar: TypeAlias = 'Foo' - ''' - ), + '''), set(), ), ] @@ -105,14 +93,12 @@ class X(Protocol): [ ( # new style type alias should never be wrapped - textwrap.dedent( - ''' + textwrap.dedent(''' if TYPE_CHECKING: type Foo = 'str' type Bar = 'Foo' - ''' - ), + '''), { '3:15 ' + TC008.format(alias='str'), '5:11 ' + TC008.format(alias='Foo'), diff --git a/tests/test_tc009.py b/tests/test_tc009.py index 15e985b..b41e5e5 100644 --- a/tests/test_tc009.py +++ b/tests/test_tc009.py @@ -18,22 +18,19 @@ ('', set()), # Used in file ( - textwrap.dedent( - """ + textwrap.dedent(""" from typing import TYPE_CHECKING if TYPE_CHECKING: datetime = Any x = datetime - """ - ), + """), {'5:4 ' + TC009.format(name='datetime')}, ), # Used in function ( - textwrap.dedent( - """ + textwrap.dedent(""" from typing import TYPE_CHECKING if TYPE_CHECKING: @@ -41,26 +38,22 @@ class date: ... def example(): return date() - """ - ), + """), {'5:4 ' + TC009.format(name='date')}, ), # Used, but only used inside the type checking block ( - textwrap.dedent( - """ + textwrap.dedent(""" if TYPE_CHECKING: class date: ... CustomType = date - """ - ), + """), set(), ), # Used for typing only ( - textwrap.dedent( - """ + textwrap.dedent(""" if TYPE_CHECKING: class date: ... @@ -68,13 +61,11 @@ def example(*args: date, **kwargs: date): return my_type: Type[date] | date - """ - ), + """), set(), ), ( - textwrap.dedent( - """ + textwrap.dedent(""" from __future__ import annotations from typing import TYPE_CHECKING @@ -87,13 +78,11 @@ class Example: async def example(self) -> AsyncIterator[list[str]]: yield 0 - """ - ), + """), set(), ), ( - textwrap.dedent( - """ + textwrap.dedent(""" from typing import TYPE_CHECKING from weakref import WeakKeyDictionary @@ -102,13 +91,11 @@ async def example(self) -> AsyncIterator[list[str]]: d = WeakKeyDictionary["Any", "Any"]() - """ - ), + """), set(), ), ( - textwrap.dedent( - """ + textwrap.dedent(""" if TYPE_CHECKING: a = int b: TypeAlias = str @@ -117,15 +104,13 @@ class d(TypedDict): ... def test_function(a, /, b, *, c, **d): print(a, b, c, d) - """ - ), + """), set(), ), # Regression test for #131 # handle scopes correctly ( - textwrap.dedent( - """ + textwrap.dedent(""" if TYPE_CHECKING: Foo: something @@ -147,15 +132,13 @@ class Foo(Protocol): bar: Foo = Foo() - """ - ), + """), set(), ), # regression test for #131 # a common pattern for inheriting from generics that aren't runtime subscriptable ( - textwrap.dedent( - """ + textwrap.dedent(""" from wtforms import Field if TYPE_CHECKING: @@ -165,15 +148,13 @@ class Foo(Protocol): class IntegerField(BaseField): pass - """ - ), + """), set(), ), # inverse regression test for #131 # here we forgot the else so it will complain about BaseField ( - textwrap.dedent( - """ + textwrap.dedent(""" from wtforms import Field if TYPE_CHECKING: @@ -181,8 +162,7 @@ class IntegerField(BaseField): class IntegerField(BaseField): pass - """ - ), + """), {'5:4 ' + TC009.format(name='BaseField')}, ), ] @@ -190,27 +170,23 @@ class IntegerField(BaseField): if sys.version_info >= (3, 12): examples.append( ( - textwrap.dedent( - """ + textwrap.dedent(""" if TYPE_CHECKING: type Foo = int x = Foo - """ - ), + """), {'3:4 ' + TC009.format(name='Foo')}, ) ) examples.append( ( - textwrap.dedent( - """ + textwrap.dedent(""" if TYPE_CHECKING: type Foo = int x: Foo - """ - ), + """), set(), ) ) diff --git a/tests/test_tc010.py b/tests/test_tc010.py index d351a2b..fc2091d 100644 --- a/tests/test_tc010.py +++ b/tests/test_tc010.py @@ -54,8 +54,7 @@ # case at some point and then it might become an error, so it's better # to have cleaned up those annotations by then ( - textwrap.dedent( - """ + textwrap.dedent(""" if TYPE_CHECKING: x: "int" | None y: int | "None" @@ -66,8 +65,7 @@ def foo(x: int | "str" | None) -> int | "None": pass - """ - ), + """), { '3:7 ' + TC010, '4:13 ' + TC010, diff --git a/tests/test_tc100.py b/tests/test_tc100.py index d7a978a..c09f016 100644 --- a/tests/test_tc100.py +++ b/tests/test_tc100.py @@ -46,15 +46,13 @@ ('if TYPE_CHECKING:\n\tfrom typing import Dict\ndef example() -> Dict[str, int]:\n\tpass', {'1:0 ' + TC100}), ( # Regression test for #186 - textwrap.dedent( - ''' + textwrap.dedent(''' if TYPE_CHECKING: from baz import Bar def foo(self) -> None: x: Bar - ''' - ), + '''), set(), ), ] @@ -63,8 +61,7 @@ def foo(self) -> None: # PEP695 tests examples += [ ( - textwrap.dedent( - """ + textwrap.dedent(""" if TYPE_CHECKING: from .types import T @@ -74,8 +71,7 @@ def foo[T](a: T) -> T: ... class Bar[T](Sequence[T]): x: T - """ - ), + """), set(), ) ] @@ -86,6 +82,11 @@ def test_TC100_errors(example, expected): assert _get_error(example, error_code_filter='TC100') == expected +@pytest.mark.parametrize(('example', 'expected'), examples) +def test_TC100_errors_skipped_with_py314plus(example, expected): + assert _get_error(example, error_code_filter='TC100', type_checking_py314plus=True) == set() + + @pytest.mark.parametrize(('example', 'expected'), examples) def test_TC100_errors_skipped_on_stubs(example, expected): assert _get_error(example, error_code_filter='TC100', filename='test.pyi') == set() diff --git a/tests/test_tc101.py b/tests/test_tc101.py index 26fad26..bc6715a 100644 --- a/tests/test_tc101.py +++ b/tests/test_tc101.py @@ -16,11 +16,15 @@ ('', set()), ("x: 'int'", {'1:3 ' + TC101.format(annotation='int')}), ("from __future__ import annotations\nx: 'int'", {'2:3 ' + TC101.format(annotation='int')}), - ("if TYPE_CHECKING:\n\timport y\nx: 'y'", set()), + ("if TYPE_CHECKING:\n\timport y\nx: 'y'", (set(), {'3:3 ' + TC101.format(annotation='y')})), # this used to return an error, but it's prone to false positives - ("x: 'dict[int]'", set()), - ("if TYPE_CHECKING:\n\tfrom typing import Dict\nx: 'Dict[int]'", set()), - ("if TYPE_CHECKING:\n\tFoo: TypeAlias = Any\nx: 'Foo'", set()), + # it is however an error with 3.14+ semantics + ("x: 'dict[int]'", (set(), {'1:3 ' + TC101.format(annotation='dict[int]')})), + ( + "if TYPE_CHECKING:\n\tfrom typing import Dict\nx: 'Dict[int]'", + (set(), {'3:3 ' + TC101.format(annotation='Dict[int]')}), + ), + ("if TYPE_CHECKING:\n\tFoo: TypeAlias = Any\nx: 'Foo'", (set(), {'3:3 ' + TC101.format(annotation='Foo')})), # Basic AnnAssign with type-checking block and exact match ( "from __future__ import annotations\nif TYPE_CHECKING:\n\tfrom typing import Dict\nx: 'Dict'", @@ -37,23 +41,20 @@ ), # ast.AnnAssign from type checking block import with quotes ( - textwrap.dedent( - ''' + textwrap.dedent(''' from __future__ import annotations if TYPE_CHECKING: import something x: "something" - ''' - ), + '''), {'7:3 ' + TC101.format(annotation='something')}, ), # No futures import and no type checking block ("from typing import Dict\nx: 'Dict'", {'2:3 ' + TC101.format(annotation='Dict')}), ( - textwrap.dedent( - ''' + textwrap.dedent(''' from __future__ import annotations if TYPE_CHECKING: @@ -61,13 +62,11 @@ def example(x: "something") -> something: pass - ''' - ), + '''), {'7:15 ' + TC101.format(annotation='something')}, ), ( - textwrap.dedent( - ''' + textwrap.dedent(''' from __future__ import annotations if TYPE_CHECKING: @@ -75,97 +74,80 @@ def example(x: "something") -> something: def example(x: "something") -> "something": pass - ''' - ), + '''), {'7:15 ' + TC101.format(annotation='something'), '7:31 ' + TC101.format(annotation='something')}, ), ( - textwrap.dedent( - ''' + textwrap.dedent(''' from __future__ import annotations def example(x: "something") -> "something": pass - ''' - ), + '''), {'4:15 ' + TC101.format(annotation='something'), '4:31 ' + TC101.format(annotation='something')}, ), ( - textwrap.dedent( - ''' + textwrap.dedent(''' if TYPE_CHECKING: import something def example(x: "something") -> "something": pass - ''' - ), - set(), + '''), + (set(), {'5:15 ' + TC101.format(annotation='something'), '5:31 ' + TC101.format(annotation='something')}), ), ( - textwrap.dedent( - ''' + textwrap.dedent(''' class X: def foo(self) -> 'X': pass - ''' - ), - set(), + '''), + (set(), {'3:21 ' + TC101.format(annotation='X')}), ), ( - textwrap.dedent( - ''' + textwrap.dedent(''' from __future__ import annotations class X: def foo(self) -> 'X': pass - ''' - ), + '''), {'4:21 ' + TC101.format(annotation='X')}, ), ( - textwrap.dedent( - ''' + textwrap.dedent(''' from typing import Annotated x: Annotated[int, 42] - ''' - ), + '''), set(), ), # Make sure we didn't introduce any regressions while solving #167 # since we started to treat the RHS sort of like an annotation for # some of the use-cases ( - textwrap.dedent( - ''' + textwrap.dedent(''' from __future__ import annotations if TYPE_CHECKING: from foo import Foo x: TypeAlias = 'Foo' - ''' - ), + '''), set(), ), ( # Regression test for #186 - textwrap.dedent( - ''' + textwrap.dedent(''' def foo(self) -> None: x: Bar - ''' - ), + '''), set(), ), ( # Reverse regression test for #186 - textwrap.dedent( - ''' + textwrap.dedent(''' def foo(self) -> None: x: 'Bar' - ''' - ), + '''), {'3:7 ' + TC101.format(annotation='Bar')}, ), ] @@ -174,15 +156,13 @@ def foo(self) -> None: # PEP695 tests examples += [ ( - textwrap.dedent( - """ + textwrap.dedent(""" def foo[T](a: 'T') -> 'T': pass class Bar[T](Set['T']): x: 'T' - """ - ), + """), { '2:14 ' + TC101.format(annotation='T'), '2:22 ' + TC101.format(annotation='T'), @@ -192,6 +172,14 @@ class Bar[T](Set['T']): ] +@pytest.mark.parametrize('py314plus', [False, True]) @pytest.mark.parametrize(('example', 'expected'), examples) -def test_TC101_errors(example, expected): - assert _get_error(example, error_code_filter='TC101') == expected +def test_TC101_errors(example, expected, py314plus): + if isinstance(expected, tuple): + expected = expected[py314plus] + + assert _get_error(example, error_code_filter='TC101', type_checking_py314plus=py314plus) == expected + if py314plus and 'from __future__ import annotations' in example: + # removing the future annotation should not change the outcome + example = example.replace('from __future__ import annotations', '') + assert _get_error(example, error_code_filter='TC101', type_checking_py314plus=py314plus) == expected diff --git a/tests/test_tc200.py b/tests/test_tc200.py index 149292e..7d3316e 100644 --- a/tests/test_tc200.py +++ b/tests/test_tc200.py @@ -46,8 +46,7 @@ set(), ), ( - textwrap.dedent( - ''' + textwrap.dedent(''' from typing import Dict, TYPE_CHECKING if TYPE_CHECKING: @@ -55,13 +54,11 @@ def example(x: Dict[something]) -> Dict["something"]: pass - ''' - ), + '''), {'7:20 ' + TC200.format(annotation='something')}, ), ( - textwrap.dedent( - ''' + textwrap.dedent(''' from typing import TYPE_CHECKING if TYPE_CHECKING: @@ -69,14 +66,12 @@ def example(x: Dict[something]) -> Dict["something"]: def example(x: ast.If): pass - ''' - ), + '''), {'7:15 ' + TC200.format(annotation='ast')}, ), # Regression test for issue #163 ( - textwrap.dedent( - ''' + textwrap.dedent(''' from typing import TYPE_CHECKING if TYPE_CHECKING: @@ -94,14 +89,12 @@ class FooProtocol(Protocol): class FooDict(TypedDict): seq: Sequence[int] - ''' - ), + '''), set(), ), # Inverse regression test for issue #163 ( - textwrap.dedent( - ''' + textwrap.dedent(''' from typing import TYPE_CHECKING if TYPE_CHECKING: @@ -125,8 +118,7 @@ class FooDict(TypedDict): Bar: int x: Bar - ''' - ), + '''), { '9:5 ' + TC200.format(annotation='TypeAlias'), '12:9 ' + TC200.format(annotation='Sequence'), @@ -136,15 +128,13 @@ class FooDict(TypedDict): ), ( # Regression test for #186 - textwrap.dedent( - ''' + textwrap.dedent(''' if TYPE_CHECKING: from baz import Bar def foo(self) -> None: x: Bar - ''' - ), + '''), set(), ), ] @@ -153,14 +143,12 @@ def foo(self) -> None: # PEP646 tests examples += [ ( - textwrap.dedent( - """ + textwrap.dedent(""" if TYPE_CHECKING: Ts = TypeVarTuple("Ts") x: tuple[*Ts] - """ - ), + """), {'5:10 ' + TC200.format(annotation='Ts')}, ) ] @@ -169,8 +157,7 @@ def foo(self) -> None: # PEP695 tests examples += [ ( - textwrap.dedent( - """ + textwrap.dedent(""" if TYPE_CHECKING: from .types import T @@ -180,8 +167,7 @@ def foo[T](a: T) -> T: ... class Bar[T](Sequence[T]): x: T - """ - ), + """), set(), ) ] diff --git a/tests/test_tc201.py b/tests/test_tc201.py index 3d205d9..72186d5 100644 --- a/tests/test_tc201.py +++ b/tests/test_tc201.py @@ -32,21 +32,18 @@ {'4:8 ' + TC201.format(annotation='int')}, ), ( - textwrap.dedent( - ''' + textwrap.dedent(''' from __future__ import annotations if TYPE_CHECKING: import something x: "something" - ''' - ), + '''), set(), ), ( - textwrap.dedent( - ''' + textwrap.dedent(''' from __future__ import annotations if TYPE_CHECKING: @@ -54,59 +51,49 @@ def example(x: "something") -> something: pass - ''' - ), + '''), set(), ), ( - textwrap.dedent( - ''' + textwrap.dedent(''' class X: def foo(self) -> 'X': pass - ''' - ), + '''), set(), ), ( - textwrap.dedent( - ''' + textwrap.dedent(''' from __future__ import annotations class X: def foo(self) -> 'X': pass - ''' - ), + '''), set(), ), ( # Regression test for Issue #164 - textwrap.dedent( - ''' + textwrap.dedent(''' from wtforms import Field from wtforms.fields.core import UnboundField foo: 'UnboundField[Field]' - ''' - ), + '''), set(), ), ( # this used to yield false negatives but works now, yay - textwrap.dedent( - ''' + textwrap.dedent(''' class Foo(Protocol): pass x: 'Foo | None' - ''' - ), + '''), {'5:3 ' + TC201.format(annotation='Foo | None')}, ), ( # Regression test for Issue #168 - textwrap.dedent( - ''' + textwrap.dedent(''' if TYPE_CHECKING: Foo = str | int Bar: TypeAlias = Foo | None @@ -123,8 +110,7 @@ def foo(a: 'T', *args: Unpack['Ts']) -> None: def bar(*args: 'P.args', **kwargs: 'P.kwargs') -> None: pass - ''' - ), + '''), set(), ), ( @@ -135,8 +121,7 @@ def bar(*args: 'P.args', **kwargs: 'P.kwargs') -> None: # ideally it still would, but it would require more complex # logic in order to avoid false positives, so for now we # put up with the false negatives here - textwrap.dedent( - ''' + textwrap.dedent(''' if TYPE_CHECKING: Foo = str | int Bar: TypeAlias = Foo | None @@ -160,39 +145,32 @@ def foo(a: 'T', *args: Unpack['Ts']) -> None: def bar(*args: 'P.args', **kwargs: 'P.kwargs') -> None: pass - ''' - ), + '''), set(), ), ( # Regression test for type checking only module attributes - textwrap.dedent( - ''' + textwrap.dedent(''' import lxml.etree foo: 'lxml.etree._Element' - ''' - ), + '''), set(), ), ( # Regression test for #186 - textwrap.dedent( - ''' + textwrap.dedent(''' def foo(self) -> None: x: Bar - ''' - ), + '''), set(), ), ( # Reverse regression test for #186 - textwrap.dedent( - ''' + textwrap.dedent(''' def foo(self) -> None: x: 'Bar' - ''' - ), + '''), {'3:7 ' + TC201.format(annotation='Bar')}, ), ] @@ -201,15 +179,13 @@ def foo(self) -> None: # PEP695 tests examples += [ ( - textwrap.dedent( - """ + textwrap.dedent(""" def foo[T](a: 'T') -> 'T': pass class Bar[T]: x: 'T' - """ - ), + """), { '2:14 ' + TC201.format(annotation='T'), '2:22 ' + TC201.format(annotation='T'),