def test_api_router_decorated_function(fdef): """Test sync and async function definition, with an arg and a kwarg.""" example = textwrap.dedent( f''' from fastapi import APIRouter from app.models import SomeModel from app.services import some_function from app.types import CustomType some_router = APIRouter(prefix='/some-path') {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'), } assert ( _get_error( example, error_code_filter='TC001,TC002,TC003', type_checking_fastapi_dependency_support_enabled=True ) == set() )
def test_complex_attrs_model_as_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 using an alias. """ example = textwrap.dedent( f''' {imp} from decimals import Decimal from decimal import Context {dec} class X: x: Decimal {dec} class Y: x: Decimal class Z: x: Context ''' ) assert _get_error(example, error_code_filter='TC001,TC002,TC003') == expected
def test_api_router_decorated_nested_function(fdef): example = textwrap.dedent(f''' import logging from typing import TYPE_CHECKING from fastapi import APIRouter, Request if TYPE_CHECKING: from starlette.responses import RedirectResponse logger = logging.getLogger(__name__) def get_auth_router() -> APIRouter: router = APIRouter(tags=['Auth'], include_in_schema=False) @router.get('/login') {fdef} login(request: Request) -> "RedirectResponse": ... ''') assert _get_error(example, error_code_filter='TC001,TC002,TC003', **defaults) == set()
def test_literal(self): example = textwrap.dedent(""" from __future__ import annotations x: Literal['string'] """) assert _get_error(example) == set()
def test_mixed_errors(self): example = textwrap.dedent(f""" import {mod} import pytest from x import y """) assert _get_error(example) == { '2:0 ' + TC001.format(module=f'{mod}'), '3:0 ' + TC002.format(module='pytest'), '4:0 ' + TC002.format(module='x.y'), }
def test_callable_import(self): """__all__ declarations originally generated false positives.""" example = textwrap.dedent(""" from x import y class X: def __init__(self): self.all_sellable_models: list[CostModel] = y( country=self.country ) """) assert _get_error(example) == set()
def test_all_tuple_declaration(self): """__all__ declarations originally generated false positives.""" example = textwrap.dedent(""" from app.models import SomeModel from another_app.models import AnotherModel __all__ = ( 'SomeModel', 'AnotherModel' ) """) assert _get_error(example) == set()
def test_app_decorated_function(fdef): example = textwrap.dedent(f''' from app.main import app from app.models import SomeModel from app.types import CustomType @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()
def test_complex_pydantic_model(): """Test actual Pydantic models, with different annotation types.""" example = textwrap.dedent(''' from __future__ import annotations from datetime import datetime from pandas import DataFrame from typing import TYPE_CHECKING from pydantic import BaseModel, condecimal, validator if TYPE_CHECKING: from datetime import date from typing import Union def format_datetime(value: Union[str, datetime]) -> datetime: if isinstance(value, str): value = datetime.strptime(value, '%Y-%m-%dT%H:%M:%S.%f%z') assert isinstance(value, datetime) return value class ModelBase(BaseModel): id: int created_at: datetime updated_at: datetime _format_datetime = validator('created_at', 'updated_at', pre=True, allow_reuse=True)(format_datetime) class NestedModel(ModelBase): z: DataFrame x: int y: str class FinalModel(ModelBase): a: str b: int c: float d: bool e: date f: NestedModel g: condecimal(ge=Decimal(0)) = Decimal(0) ''') assert _get_error(example, error_code_filter='TC002', type_checking_pydantic_enabled=True) == set()
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(''' from pandas import DataFrame class X: x: DataFrame ''') assert _get_error(example, error_code_filter='TC002', type_checking_pydantic_enabled=enabled) == expected
def test_import_not_flagged_when_existing_import_present(self): """ When an import is made to a module, we treat it as already used. Guarding additional imports to the same module shouldn't present a performance gain of note, so it's probably not worth the effort. """ example = """ from os import x # <-- would otherwise be flagged from os import y # <-- but this should prevent that z = y """ assert _get_error(textwrap.dedent(example)) == set()
def test_conditional_import(self): example = textwrap.dedent(""" version = 2 if version == 2: import x else: 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_class_with_base_class(): """ Whenever a class inherits from anything, we need to assume it might be a pydantic model, for which we need to register annotations as uses. """ 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_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(""" from django.db import models from app.models import SomeModel class LoanProvider(models.Model): fk: SomeModel = models.ForeignKey( SomeModel, on_delete=models.CASCADE, ) """) assert _get_error(example) == set()
def test_api_router_decorated_function(fdef): """Test sync and async function definition, with an arg and a kwarg.""" example = textwrap.dedent(f''' from fastapi import APIRouter from app.models import SomeModel from app.services import some_function from app.types import CustomType some_router = APIRouter(prefix='/some-path') @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()
def test_type_checking_block_imports_dont_generate_errors(self): example = textwrap.dedent(""" import x from y import z if TYPE_CHECKING: import a # arbitrary whitespace from b import c def test(): pass """) assert _get_error(example) == { '2:0 ' + TC002.format(module='x'), '3:0 ' + TC002.format(module='y.z'), }
def test_type_checking_block_formats_detected(self): """ We should detect type-checking blocks, no matter* the format. https://github.com/snok/flake8-type-checking/issues/68 """ type_checking = """ from typing import TYPE_CHECKING if TYPE_CHECKING: from pathlib import Path p: Path """ typing_type_checking = """ import typing if typing.TYPE_CHECKING: from pathlib import Path p: Path """ alias = """ from typing import TYPE_CHECKING as test if test: from pathlib import Path p: Path """ aliased_typing = """ import typing as T if T.TYPE_CHECKING: from pathlib import Path p: Path """ for example in [ type_checking, typing_type_checking, alias, aliased_typing ]: assert _get_error(textwrap.dedent(example)) == set()
def test_exempt_modules_option(): """ The plugin provides an option called `--type-checking-exempt-modules` which is meant to passlist certain modules from TC001 and TC002 errors. """ # Check that typing is passlisted when exempted 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(''' from typing import TYPE_CHECKING from pandas import DataFrame from a import B x: Callable[[], List] ''') assert _get_error(example2, error_code_filter='TC002') == { '3:0 ' + TC002.format(module='pandas.DataFrame'), '4:0 ' + TC002.format(module='a.B'), } assert _get_error(example2, error_code_filter='TC002', type_checking_exempt_modules=['pandas']) == { '4:0 ' + TC002.format(module='a.B') } # Check Import 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()
def test_type_checking_pydantic_enabled_baseclass_passlist(c): """Test that named tuples are not ignored.""" 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', type_checking_pydantic_enabled=True, type_checking_pydantic_enabled_baseclass_passlist=[ 'NamedTuple', 'TypedDict' ], ) == { '3:0 ' + TC002.format(module='x.Y'), '3:0 ' + TC002.format(module='x.Z'), }
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''' from fastapi import APIRouter from fastapi import Request from app.types import CustomType some_router = APIRouter(prefix='/some-path') @some_router.get('/{{resource_id}}') {fdef} list_something(request: Request) -> CustomType: return None ''') assert _get_error(example, error_code_filter='TC001,TC002,TC003', **defaults) == { '5:0 ' + TC002.format(module='app.types.CustomType') }
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''' {imp} from decimal import Decimal {dec} class X: x: Decimal {dec} class Y: x: Decimal class Z: x: Decimal ''' ) assert _get_error(example, error_code_filter='TC001,TC002,TC003') == set()
def test_ellipsis(self): example = textwrap.dedent(""" x: Tuple[str, ...] """) assert _get_error(example) == set()
def test_TC001_errors(example: str, expected: set[str]) -> None: assert _get_error(example, error_code_filter='TC001') == expected
def test_TC100_errors(example, expected): assert _get_error(example, error_code_filter='TC100') == expected