Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: Drop Python 3.8 support. Add Python 3.13 support. Update CI/dev dependencies. #197

Merged
merged 8 commits into from
Dec 13, 2024
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 15 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,19 @@ 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
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') }}-${{ matrix.python-version }}-9
- name: Install Poetry
uses: snok/install-poetry@v1
with:
Expand All @@ -72,9 +72,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'
16 changes: 8 additions & 8 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,19 +31,19 @@ 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:
Expand Down
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
Loading