Skip to content

Commit 40c43b5

Browse files
authored
chore: Drop Python 3.8 support. Add Python 3.13 support.
Update CI/dev dependencies.
1 parent 76d4b9c commit 40c43b5

33 files changed

+1109
-757
lines changed

.github/workflows/testing.yml

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -10,25 +10,25 @@ jobs:
1010
linting:
1111
runs-on: ubuntu-latest
1212
steps:
13-
- uses: actions/checkout@v3
14-
- uses: actions/setup-python@v4
13+
- uses: actions/checkout@v4
14+
- uses: actions/setup-python@v5
1515
with:
16-
python-version: "3.10.7"
17-
- uses: actions/cache@v3
16+
python-version: "3.10"
17+
- uses: actions/cache@v4
1818
id: cache-venv
1919
with:
2020
path: .venv
21-
key: venv-5 # increment to reset
21+
key: venv-6 # increment to reset
2222
- run: |
2323
python -m venv .venv --upgrade-deps
2424
source .venv/bin/activate
2525
pip install pre-commit
2626
if: steps.cache-venv.outputs.cache-hit != 'true'
27-
- uses: actions/cache@v3
27+
- uses: actions/cache@v4
2828
id: pre-commit-cache
2929
with:
3030
path: ~/.cache/pre-commit
31-
key: ${{ hashFiles('**/pre-commit-config.yaml') }}-4
31+
key: ${{ hashFiles('**/pre-commit-config.yaml') }}-5
3232
- run: |
3333
source .venv/bin/activate
3434
pre-commit run --all-files
@@ -38,19 +38,20 @@ jobs:
3838
strategy:
3939
fail-fast: false
4040
matrix:
41-
python-version: [ "3.8.18", "3.9.18", "3.10.13", "3.11.6", "3.12.0" ]
41+
python-version: [ "3.9", "3.10", "3.11", "3.12", "3.13" ]
4242
steps:
43-
- uses: actions/checkout@v3
44-
- uses: actions/setup-python@v4
43+
- uses: actions/checkout@v4
44+
- uses: actions/setup-python@v5
45+
id: setup-python
4546
with:
4647
python-version: "${{ matrix.python-version }}"
47-
- uses: actions/cache@v3
48+
- uses: actions/cache@v4
4849
id: poetry-cache
4950
with:
5051
path: |
5152
~/.local
5253
.venv
53-
key: ${{ hashFiles('**/poetry.lock') }}-${{ matrix.python-version }}-8
54+
key: ${{ hashFiles('**/poetry.lock') }}-${{ steps.setup-python.outputs.python-version }}-9
5455
- name: Install Poetry
5556
uses: snok/install-poetry@v1
5657
with:
@@ -72,9 +73,9 @@ jobs:
7273
coverage run -m pytest tests
7374
coverage xml
7475
coverage report
75-
- uses: codecov/codecov-action@v2
76+
- uses: codecov/codecov-action@v5
7677
with:
77-
file: ./coverage.xml
78+
files: ./coverage.xml
7879
fail_ci_if_error: true
7980
token: ${{ secrets.CODECOV_TOKEN }}
80-
if: matrix.python-version == '3.10.7'
81+
if: matrix.python-version == '3.12'

.pre-commit-config.yaml

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
repos:
22
- repo: https://github.com/psf/black
3-
rev: 23.11.0
3+
rev: 24.10.0
44
hooks:
55
- id: black
66
- repo: https://github.com/pre-commit/pre-commit-hooks
7-
rev: v4.5.0
7+
rev: v5.0.0
88
hooks:
99
- id: check-ast
1010
- id: check-added-large-files
@@ -19,7 +19,7 @@ repos:
1919
- id: mixed-line-ending
2020
- id: trailing-whitespace
2121
- repo: https://github.com/pycqa/flake8
22-
rev: 6.1.0
22+
rev: 7.1.1
2323
hooks:
2424
- id: flake8
2525
additional_dependencies: [
@@ -31,23 +31,21 @@ repos:
3131
'flake8-pytest-style',
3232
'flake8-docstrings',
3333
'flake8-printf-formatting',
34-
'flake8-type-checking==2.0.6',
34+
'flake8-type-checking==2.9.1',
3535
]
3636
- repo: https://github.com/asottile/pyupgrade
37-
rev: v3.15.0
37+
rev: v3.19.0
3838
hooks:
3939
- id: pyupgrade
40-
args: [ "--py36-plus", "--py37-plus", "--py38-plus", '--keep-runtime-typing' ]
40+
args: [ "--py39-plus", '--keep-runtime-typing' ]
4141
- repo: https://github.com/pycqa/isort
42-
rev: 5.12.0
42+
rev: 5.13.2
4343
hooks:
4444
- id: isort
4545
- repo: https://github.com/pre-commit/mirrors-mypy
46-
rev: v1.7.1
46+
rev: v1.13.0
4747
hooks:
4848
- id: mypy
49-
args:
50-
- --config-file=setup.cfg
5149
additional_dependencies:
5250
- pytest
5351
- flake8

flake8_type_checking/checker.py

Lines changed: 24 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,13 @@
55
import os
66
import sys
77
from abc import ABC, abstractmethod
8-
from ast import Index, literal_eval
8+
from ast import literal_eval
99
from collections import defaultdict
1010
from contextlib import contextmanager, suppress
1111
from dataclasses import dataclass
1212
from itertools import chain
1313
from pathlib import Path
14-
from typing import TYPE_CHECKING, Literal, NamedTuple, cast
14+
from typing import TYPE_CHECKING, Any, Literal, NamedTuple, cast
1515

1616
from classify_imports import Classified, classify_base
1717

@@ -36,27 +36,13 @@
3636
TC200,
3737
TC201,
3838
builtin_names,
39-
py38,
4039
sqlalchemy_default_mapped_dotted_names,
4140
)
4241

43-
try:
44-
ast_unparse = ast.unparse # type: ignore[attr-defined]
45-
except AttributeError: # pragma: no cover
46-
# Python < 3.9
47-
48-
import astor
49-
50-
def ast_unparse(node: ast.AST) -> str:
51-
"""AST unparsing helper for Python < 3.9."""
52-
return cast('str', astor.to_source(node)).strip()
53-
54-
5542
if TYPE_CHECKING:
5643
from _ast import AsyncFunctionDef, FunctionDef
5744
from argparse import Namespace
5845
from collections.abc import Iterator
59-
from typing import Any, Optional, Union
6046

6147
from flake8_type_checking.types import (
6248
Comprehension,
@@ -104,18 +90,18 @@ def visit(self, node: ast.AST) -> None:
10490
setattr(node.right, BINOP_OPERAND_PROPERTY, True)
10591
self.visit(node.left)
10692
self.visit(node.right)
107-
elif (py38 and isinstance(node, Index)) or isinstance(node, ast.Attribute):
93+
elif isinstance(node, ast.Attribute):
10894
self.visit(node.value)
10995
elif isinstance(node, ast.Subscript):
11096
self.visit(node.value)
11197
if self.is_typing(node.value, 'Literal'):
11298
return
11399
elif self.is_typing(node.value, 'Annotated') and isinstance(
114-
(elts_node := node.slice.value if py38 and isinstance(node.slice, Index) else node.slice),
100+
node.slice,
115101
(ast.Tuple, ast.List),
116102
):
117-
if elts_node.elts:
118-
elts_iter = iter(elts_node.elts)
103+
if node.slice.elts:
104+
elts_iter = iter(node.slice.elts)
119105
# only visit the first element like a type expression
120106
self.visit_annotated_type(next(elts_iter))
121107
for value_node in elts_iter:
@@ -144,9 +130,9 @@ class AttrsMixin:
144130
if TYPE_CHECKING:
145131
third_party_imports: dict[str, Import]
146132

147-
def get_all_attrs_imports(self) -> dict[Optional[str], str]:
133+
def get_all_attrs_imports(self) -> dict[str | None, str]:
148134
"""Return a map of all attrs/attr imports."""
149-
attrs_imports: dict[Optional[str], str] = {} # map of alias to full import name
135+
attrs_imports: dict[str | None, str] = {} # map of alias to full import name
150136

151137
for node in self.third_party_imports.values():
152138
module = getattr(node, 'module', '')
@@ -166,7 +152,7 @@ def is_attrs_class(self, class_node: ast.ClassDef) -> bool:
166152
attrs_imports = self.get_all_attrs_imports()
167153
return any(self.is_attrs_decorator(decorator, attrs_imports) for decorator in class_node.decorator_list)
168154

169-
def is_attrs_decorator(self, decorator: Any, attrs_imports: dict[Optional[str], str]) -> bool:
155+
def is_attrs_decorator(self, decorator: Any, attrs_imports: dict[str | None, str]) -> bool:
170156
"""Check whether a class decorator is an attrs decorator or not."""
171157
if isinstance(decorator, ast.Call):
172158
return self.is_attrs_decorator(decorator.func, attrs_imports)
@@ -185,7 +171,7 @@ def is_attrs_attribute(attribute: ast.Attribute) -> bool:
185171
return any(e for e in actual if e in ATTRS_DECORATORS)
186172

187173
@staticmethod
188-
def is_attrs_str(attribute: Union[str, ast.expr], attrs_imports: dict[Optional[str], str]) -> bool:
174+
def is_attrs_str(attribute: str | ast.expr, attrs_imports: dict[str | None, str]) -> bool:
189175
"""Check whether an ast.expr or string is an attrs string or not."""
190176
actual = attrs_imports.get(str(attribute), '')
191177
return actual in ATTRS_DECORATORS
@@ -211,7 +197,7 @@ class DunderAllMixin:
211197
"""
212198

213199
if TYPE_CHECKING:
214-
uses: dict[str, list[tuple[ast.AST, Scope]]]
200+
uses: dict[str, list[tuple[ast.expr, Scope]]]
215201
current_scope: Scope
216202

217203
def generic_visit(self, node: ast.AST) -> None: # noqa: D102
@@ -285,12 +271,12 @@ class PydanticMixin:
285271

286272
if TYPE_CHECKING:
287273
pydantic_enabled: bool
288-
pydantic_validate_arguments_import_name: Optional[str]
274+
pydantic_validate_arguments_import_name: str | None
289275

290276
def visit(self, node: ast.AST) -> ast.AST: # noqa: D102
291277
...
292278

293-
def _function_is_wrapped_by_validate_arguments(self, node: Union[FunctionDef, AsyncFunctionDef]) -> bool:
279+
def _function_is_wrapped_by_validate_arguments(self, node: FunctionDef | AsyncFunctionDef) -> bool:
294280
if self.pydantic_enabled and node.decorator_list:
295281
for decorator_node in node.decorator_list:
296282
if getattr(decorator_node, 'id', '') == self.pydantic_validate_arguments_import_name:
@@ -360,7 +346,7 @@ class SQLAlchemyMixin:
360346
sqlalchemy_enabled: bool
361347
sqlalchemy_mapped_dotted_names: set[str]
362348
current_scope: Scope
363-
uses: dict[str, list[tuple[ast.AST, Scope]]]
349+
uses: dict[str, list[tuple[ast.expr, Scope]]]
364350
soft_uses: set[str]
365351
in_soft_use_context: bool
366352

@@ -504,7 +490,7 @@ def visit_AsyncFunctionDef(self, node: AsyncFunctionDef) -> None:
504490
if self.injector_enabled:
505491
self.handle_injector_declaration(node)
506492

507-
def handle_injector_declaration(self, node: Union[AsyncFunctionDef, FunctionDef]) -> None:
493+
def handle_injector_declaration(self, node: AsyncFunctionDef | FunctionDef) -> None:
508494
"""
509495
Adjust for injector declaration setting.
510496
@@ -553,7 +539,7 @@ def visit_AsyncFunctionDef(self, node: AsyncFunctionDef) -> None:
553539
if (self.fastapi_enabled and node.decorator_list) or self.fastapi_dependency_support_enabled:
554540
self.handle_fastapi_decorator(node)
555541

556-
def handle_fastapi_decorator(self, node: Union[AsyncFunctionDef, FunctionDef]) -> None:
542+
def handle_fastapi_decorator(self, node: AsyncFunctionDef | FunctionDef) -> None:
557543
"""
558544
Adjust for FastAPI decorator setting.
559545
@@ -648,7 +634,7 @@ class ImportName:
648634

649635
_module: str
650636
_name: str
651-
_alias: Optional[str]
637+
_alias: str | None
652638

653639
#: Whether or not this import is exempt from TC001-004 checks.
654640
exempt: bool
@@ -1034,8 +1020,8 @@ def __init__(
10341020
injector_enabled: bool,
10351021
cattrs_enabled: bool,
10361022
pydantic_enabled_baseclass_passlist: list[str],
1037-
typing_modules: Optional[list[str]] = None,
1038-
exempt_modules: Optional[list[str]] = None,
1023+
typing_modules: list[str] | None = None,
1024+
exempt_modules: list[str] | None = None,
10391025
) -> None:
10401026
super().__init__()
10411027

@@ -1074,7 +1060,7 @@ def __init__(
10741060
self.scopes: list[Scope] = []
10751061

10761062
#: List of all names and ids, except type declarations
1077-
self.uses: dict[str, list[tuple[ast.AST, Scope]]] = defaultdict(list)
1063+
self.uses: dict[str, list[tuple[ast.expr, Scope]]] = defaultdict(list)
10781064

10791065
#: Contains a set of all names to be treated like soft-uses.
10801066
# i.e. we don't know if it will be used at runtime or not, so
@@ -1085,7 +1071,7 @@ def __init__(
10851071
self.annotation_visitor = ImportAnnotationVisitor(self)
10861072

10871073
#: Whether there is a `from __futures__ import annotations` present in the file
1088-
self.futures_annotation: Optional[bool] = None
1074+
self.futures_annotation: bool | None = None
10891075

10901076
#: Where the type checking block exists (line_start, line_end, col_offset)
10911077
# Empty type checking blocks are used for TC005 errors, while the type
@@ -1098,7 +1084,7 @@ def __init__(
10981084
self.unquoted_types_in_casts: list[tuple[int, int, str]] = []
10991085

11001086
#: For tracking which comprehension/IfExp we're currently inside of
1101-
self.active_context: Optional[Comprehension | ast.IfExp] = None
1087+
self.active_context: Comprehension | ast.IfExp | None = None
11021088

11031089
#: Whether or not we're in a context where uses count as soft-uses.
11041090
# E.g. the type expression of `typing.Annotated[type, value]`
@@ -1914,7 +1900,7 @@ def register_unquoted_type_in_typing_cast(self, node: ast.Call) -> None:
19141900
if isinstance(arg, ast.Constant) and isinstance(arg.value, str):
19151901
return # Type argument is already a string literal.
19161902

1917-
self.unquoted_types_in_casts.append((arg.lineno, arg.col_offset, ast_unparse(arg)))
1903+
self.unquoted_types_in_casts.append((arg.lineno, arg.col_offset, ast.unparse(arg)))
19181904

19191905
def visit_Call(self, node: ast.Call) -> None:
19201906
"""Check arguments of calls, e.g. typing.cast()."""
@@ -1937,7 +1923,7 @@ class TypingOnlyImportsChecker:
19371923
'future_option_enabled',
19381924
]
19391925

1940-
def __init__(self, node: ast.Module, options: Optional[Namespace]) -> None:
1926+
def __init__(self, node: ast.Module, options: Namespace | None) -> None:
19411927
self.cwd = Path(os.getcwd())
19421928
self.strict_mode = getattr(options, 'type_checking_strict', False)
19431929

flake8_type_checking/constants.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import builtins
2-
import sys
32

43
import flake8
54

@@ -18,7 +17,6 @@
1817
]
1918
ATTRS_IMPORTS = {'attrs', 'attr'}
2019

21-
py38 = sys.version_info.major == 3 and sys.version_info.minor == 8
2220
flake_version_gt_v4 = tuple(int(i) for i in flake8.__version__.split('.')) >= (4, 0, 0)
2321

2422
# Based off of what pyflakes does

flake8_type_checking/types.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,13 @@
44

55
if TYPE_CHECKING:
66
import ast
7-
from typing import Any, Generator, Optional, Protocol, Tuple, Union
7+
from collections.abc import Generator
8+
from typing import Any, Optional, Protocol, Union
89

910
Function = Union[ast.FunctionDef, ast.AsyncFunctionDef, ast.Lambda]
1011
Comprehension = Union[ast.ListComp, ast.SetComp, ast.DictComp, ast.GeneratorExp]
1112
Import = Union[ast.Import, ast.ImportFrom]
12-
Flake8Generator = Generator[Tuple[int, int, str, Any], None, None]
13+
Flake8Generator = Generator[tuple[int, int, str, Any], None, None]
1314

1415
class Name(Protocol):
1516
asname: Optional[str]

0 commit comments

Comments
 (0)