Skip to content

Commit

Permalink
chore: Drop Python 3.8 support. Add Python 3.13 support.
Browse files Browse the repository at this point in the history
Update CI/dev dependencies.
  • Loading branch information
Daverball authored Dec 13, 2024
1 parent 76d4b9c commit 40c43b5
Show file tree
Hide file tree
Showing 33 changed files with 1,109 additions and 757 deletions.
31 changes: 16 additions & 15 deletions .github/workflows/testing.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,25 +10,25 @@ jobs:
linting:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.10.7"
- uses: actions/cache@v3
python-version: "3.10"
- uses: actions/cache@v4
id: cache-venv
with:
path: .venv
key: venv-5 # increment to reset
key: venv-6 # increment to reset
- run: |
python -m venv .venv --upgrade-deps
source .venv/bin/activate
pip install pre-commit
if: steps.cache-venv.outputs.cache-hit != 'true'
- uses: actions/cache@v3
- uses: actions/cache@v4
id: pre-commit-cache
with:
path: ~/.cache/pre-commit
key: ${{ hashFiles('**/pre-commit-config.yaml') }}-4
key: ${{ hashFiles('**/pre-commit-config.yaml') }}-5
- run: |
source .venv/bin/activate
pre-commit run --all-files
Expand All @@ -38,19 +38,20 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: [ "3.8.18", "3.9.18", "3.10.13", "3.11.6", "3.12.0" ]
python-version: [ "3.9", "3.10", "3.11", "3.12", "3.13" ]
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
id: setup-python
with:
python-version: "${{ matrix.python-version }}"
- uses: actions/cache@v3
- uses: actions/cache@v4
id: poetry-cache
with:
path: |
~/.local
.venv
key: ${{ hashFiles('**/poetry.lock') }}-${{ matrix.python-version }}-8
key: ${{ hashFiles('**/poetry.lock') }}-${{ steps.setup-python.outputs.python-version }}-9
- name: Install Poetry
uses: snok/install-poetry@v1
with:
Expand All @@ -72,9 +73,9 @@ jobs:
coverage run -m pytest tests
coverage xml
coverage report
- uses: codecov/codecov-action@v2
- uses: codecov/codecov-action@v5
with:
file: ./coverage.xml
files: ./coverage.xml
fail_ci_if_error: true
token: ${{ secrets.CODECOV_TOKEN }}
if: matrix.python-version == '3.10.7'
if: matrix.python-version == '3.12'
18 changes: 8 additions & 10 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
repos:
- repo: https://github.com/psf/black
rev: 23.11.0
rev: 24.10.0
hooks:
- id: black
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.5.0
rev: v5.0.0
hooks:
- id: check-ast
- id: check-added-large-files
Expand All @@ -19,7 +19,7 @@ repos:
- id: mixed-line-ending
- id: trailing-whitespace
- repo: https://github.com/pycqa/flake8
rev: 6.1.0
rev: 7.1.1
hooks:
- id: flake8
additional_dependencies: [
Expand All @@ -31,23 +31,21 @@ repos:
'flake8-pytest-style',
'flake8-docstrings',
'flake8-printf-formatting',
'flake8-type-checking==2.0.6',
'flake8-type-checking==2.9.1',
]
- repo: https://github.com/asottile/pyupgrade
rev: v3.15.0
rev: v3.19.0
hooks:
- id: pyupgrade
args: [ "--py36-plus", "--py37-plus", "--py38-plus", '--keep-runtime-typing' ]
args: [ "--py39-plus", '--keep-runtime-typing' ]
- repo: https://github.com/pycqa/isort
rev: 5.12.0
rev: 5.13.2
hooks:
- id: isort
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.7.1
rev: v1.13.0
hooks:
- id: mypy
args:
- --config-file=setup.cfg
additional_dependencies:
- pytest
- flake8
62 changes: 24 additions & 38 deletions flake8_type_checking/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@
import os
import sys
from abc import ABC, abstractmethod
from ast import Index, literal_eval
from ast import literal_eval
from collections import defaultdict
from contextlib import contextmanager, suppress
from dataclasses import dataclass
from itertools import chain
from pathlib import Path
from typing import TYPE_CHECKING, Literal, NamedTuple, cast
from typing import TYPE_CHECKING, Any, Literal, NamedTuple, cast

from classify_imports import Classified, classify_base

Expand All @@ -36,27 +36,13 @@
TC200,
TC201,
builtin_names,
py38,
sqlalchemy_default_mapped_dotted_names,
)

try:
ast_unparse = ast.unparse # type: ignore[attr-defined]
except AttributeError: # pragma: no cover
# Python < 3.9

import astor

def ast_unparse(node: ast.AST) -> str:
"""AST unparsing helper for Python < 3.9."""
return cast('str', astor.to_source(node)).strip()


if TYPE_CHECKING:
from _ast import AsyncFunctionDef, FunctionDef
from argparse import Namespace
from collections.abc import Iterator
from typing import Any, Optional, Union

from flake8_type_checking.types import (
Comprehension,
Expand Down Expand Up @@ -104,18 +90,18 @@ def visit(self, node: ast.AST) -> None:
setattr(node.right, BINOP_OPERAND_PROPERTY, True)
self.visit(node.left)
self.visit(node.right)
elif (py38 and isinstance(node, Index)) or isinstance(node, ast.Attribute):
elif isinstance(node, ast.Attribute):
self.visit(node.value)
elif isinstance(node, ast.Subscript):
self.visit(node.value)
if self.is_typing(node.value, 'Literal'):
return
elif self.is_typing(node.value, 'Annotated') and isinstance(
(elts_node := node.slice.value if py38 and isinstance(node.slice, Index) else node.slice),
node.slice,
(ast.Tuple, ast.List),
):
if elts_node.elts:
elts_iter = iter(elts_node.elts)
if node.slice.elts:
elts_iter = iter(node.slice.elts)
# only visit the first element like a type expression
self.visit_annotated_type(next(elts_iter))
for value_node in elts_iter:
Expand Down Expand Up @@ -144,9 +130,9 @@ class AttrsMixin:
if TYPE_CHECKING:
third_party_imports: dict[str, Import]

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

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

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

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

if TYPE_CHECKING:
uses: dict[str, list[tuple[ast.AST, Scope]]]
uses: dict[str, list[tuple[ast.expr, Scope]]]
current_scope: Scope

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

if TYPE_CHECKING:
pydantic_enabled: bool
pydantic_validate_arguments_import_name: Optional[str]
pydantic_validate_arguments_import_name: str | None

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

def _function_is_wrapped_by_validate_arguments(self, node: Union[FunctionDef, AsyncFunctionDef]) -> bool:
def _function_is_wrapped_by_validate_arguments(self, node: FunctionDef | AsyncFunctionDef) -> bool:
if self.pydantic_enabled and node.decorator_list:
for decorator_node in node.decorator_list:
if getattr(decorator_node, 'id', '') == self.pydantic_validate_arguments_import_name:
Expand Down Expand Up @@ -360,7 +346,7 @@ class SQLAlchemyMixin:
sqlalchemy_enabled: bool
sqlalchemy_mapped_dotted_names: set[str]
current_scope: Scope
uses: dict[str, list[tuple[ast.AST, Scope]]]
uses: dict[str, list[tuple[ast.expr, Scope]]]
soft_uses: set[str]
in_soft_use_context: bool

Expand Down Expand Up @@ -504,7 +490,7 @@ def visit_AsyncFunctionDef(self, node: AsyncFunctionDef) -> None:
if self.injector_enabled:
self.handle_injector_declaration(node)

def handle_injector_declaration(self, node: Union[AsyncFunctionDef, FunctionDef]) -> None:
def handle_injector_declaration(self, node: AsyncFunctionDef | FunctionDef) -> None:
"""
Adjust for injector declaration setting.
Expand Down Expand Up @@ -553,7 +539,7 @@ def visit_AsyncFunctionDef(self, node: AsyncFunctionDef) -> None:
if (self.fastapi_enabled and node.decorator_list) or self.fastapi_dependency_support_enabled:
self.handle_fastapi_decorator(node)

def handle_fastapi_decorator(self, node: Union[AsyncFunctionDef, FunctionDef]) -> None:
def handle_fastapi_decorator(self, node: AsyncFunctionDef | FunctionDef) -> None:
"""
Adjust for FastAPI decorator setting.
Expand Down Expand Up @@ -648,7 +634,7 @@ class ImportName:

_module: str
_name: str
_alias: Optional[str]
_alias: str | None

#: Whether or not this import is exempt from TC001-004 checks.
exempt: bool
Expand Down Expand Up @@ -1034,8 +1020,8 @@ def __init__(
injector_enabled: bool,
cattrs_enabled: bool,
pydantic_enabled_baseclass_passlist: list[str],
typing_modules: Optional[list[str]] = None,
exempt_modules: Optional[list[str]] = None,
typing_modules: list[str] | None = None,
exempt_modules: list[str] | None = None,
) -> None:
super().__init__()

Expand Down Expand Up @@ -1074,7 +1060,7 @@ def __init__(
self.scopes: list[Scope] = []

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

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

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

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

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

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

self.unquoted_types_in_casts.append((arg.lineno, arg.col_offset, ast_unparse(arg)))
self.unquoted_types_in_casts.append((arg.lineno, arg.col_offset, ast.unparse(arg)))

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

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

Expand Down
2 changes: 0 additions & 2 deletions flake8_type_checking/constants.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import builtins
import sys

import flake8

Expand All @@ -18,7 +17,6 @@
]
ATTRS_IMPORTS = {'attrs', 'attr'}

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

# Based off of what pyflakes does
Expand Down
5 changes: 3 additions & 2 deletions flake8_type_checking/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@

if TYPE_CHECKING:
import ast
from typing import Any, Generator, Optional, Protocol, Tuple, Union
from collections.abc import Generator
from typing import Any, Optional, Protocol, Union

Function = Union[ast.FunctionDef, ast.AsyncFunctionDef, ast.Lambda]
Comprehension = Union[ast.ListComp, ast.SetComp, ast.DictComp, ast.GeneratorExp]
Import = Union[ast.Import, ast.ImportFrom]
Flake8Generator = Generator[Tuple[int, int, str, Any], None, None]
Flake8Generator = Generator[tuple[int, int, str, Any], None, None]

class Name(Protocol):
asname: Optional[str]
Expand Down
Loading

0 comments on commit 40c43b5

Please sign in to comment.