diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index cd11809..67609fe 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -20,7 +20,6 @@ repos: - id: check-added-large-files args: [ '--maxkb=1024' ] - id: detect-private-key - - id: end-of-file-fixer - id: mixed-line-ending - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.16.9 @@ -39,24 +38,25 @@ repos: hooks: - id: uv-lock name: Update uv lockfile - args: - - --upgrade - id: uv-sync - repo: https://github.com/pre-commit/mirrors-mypy rev: v2.3.1 hooks: - id: mypy name: Static type checking with mypy + # The hook runs in its own virtualenv, so it needs every package the + # checked code imports, including the optional backends. additional_dependencies: - - types-redis - - types-requests - - types-PyYAML - - types-orjson + - fastapi>=0.110.0 + - starlette>=0.36.0 - pydantic>=2.0.0 - pytest>=8.0.0 - pytest-asyncio>=0.23.0 - - fastapi>=0.110.0 - - starlette>=0.36.0 - - types-aiofiles + - redis>=5.3.0 + - types-redis + - orjson + - types-orjson + - pymemcache>=4.0.0 + - PyJWT>=2.9.0 args: [--strict] exclude: ^(docs/|scripts/) diff --git a/CLAUDE.md b/CLAUDE.md index b833176..83497c3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -107,7 +107,7 @@ Four non-abstract atomic primitives live on the base class with non-atomic fallb ### Code Quality Rules -- Ruff is configured with `extend-select = ['ALL']` with specific ignores (see `pyproject.toml`). Notable: E501 (line length), B008 (function calls in defaults), FBT001/FBT002 (boolean args — intentional for Cache-Control API). +- Ruff is configured with `extend-select = ['ALL']` with specific ignores (see `pyproject.toml`). Notable: E501 (line length), FBT001/FBT002 (boolean args — intentional for Cache-Control API). `fastapi.Depends` is allowed in argument defaults via `flake8-bugbear.extend-immutable-calls` rather than ignoring B008. - mypy runs in strict mode on the package (not tests). - pydocstring convention is Google style. - Forward references are mostly quoted annotations with `TYPE_CHECKING` imports; only a couple of modules use `from __future__ import annotations`. diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index ee85400..3457751 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -149,6 +149,9 @@ uv run pre-commit run --all-files The hooks cover the standard pre-commit-hooks checks, `ruff` (with `--fix`) and `ruff-format`, `typos`, `uv-lock`/`uv-sync`, and strict `mypy` (excluding `docs/` and `scripts/`). They run automatically on `git commit`. If any checks fail, fix the issues and try committing again. +`uv-lock` only re-resolves the lockfile when `pyproject.toml` changed; it does not +upgrade pinned versions. Upgrades come from Renovate or an explicit +`uv lock --upgrade`. pre-commit only sees the files you touched. The **Lint** workflow checks the whole tree; run the same commands before pushing: diff --git a/fastapi_cachex/backends/memcached.py b/fastapi_cachex/backends/memcached.py index f3dbf3c..cfde919 100644 --- a/fastapi_cachex/backends/memcached.py +++ b/fastapi_cachex/backends/memcached.py @@ -100,9 +100,9 @@ def __init__( """ try: from pymemcache import HashClient - except ImportError: + except ImportError as exc: msg = "pymemcache is not installed. Install it with the extra: pip install 'fastapi-cachex[memcached]'" - raise CacheXError(msg) + raise CacheXError(msg) from exc # Pooled connections have no ordering guarantee between each other, so # every write waits for the server's acknowledgement; otherwise a diff --git a/fastapi_cachex/backends/redis.py b/fastapi_cachex/backends/redis.py index 9b029a8..c15da04 100644 --- a/fastapi_cachex/backends/redis.py +++ b/fastapi_cachex/backends/redis.py @@ -146,12 +146,12 @@ def __init__( # builtins.__import__("redis") can simulate absence reliably. import redis # noqa: F401 from redis.asyncio import Redis as AsyncRedis - except ImportError: + except ImportError as exc: msg = ( "redis[hiredis] is not installed. Please install it with " "'pip install \"redis[hiredis]\"' " ) - raise CacheXError(msg) + raise CacheXError(msg) from exc _warn_if_not_utf8(encoding) diff --git a/fastapi_cachex/session/middleware.py b/fastapi_cachex/session/middleware.py index 0f9a879..8e1ae20 100644 --- a/fastapi_cachex/session/middleware.py +++ b/fastapi_cachex/session/middleware.py @@ -426,7 +426,7 @@ async def send_wrapper(message: Message) -> None: await self.app(scope, receive, send_wrapper) - async def _persist( # noqa: PLR0913 + async def _persist( # noqa: PLR0913, PLR0917 self, session: _RequestSession, headers: MutableHeaders, diff --git a/pyproject.toml b/pyproject.toml index db86ce1..1398a1b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -91,18 +91,18 @@ ignore = [ "E501", "ANN401", "COM812", # redundant rule, use formatter to fix - "B008", # do not perform function calls in argument defaults - "B904", # Allow raising exceptions without from e, for HTTPException "FBT001", "FBT002", # Boolean arguments are part of Cache-Control API design - "S105", # false-positive on token_format strings "C901", # allow slightly complex session flow "CPY001", # no copyright header convention in this project (stabilized in ruff 0.16) - "PLR0917", # backend constructors take many positional options; keyword-only would break the API ] preview = true explicit-preview-rules = true extend-select = ['ALL'] +[tool.ruff.lint.flake8-bugbear] +# FastAPI dependency markers are meant to be called in argument defaults. +extend-immutable-calls = ["fastapi.Depends"] + [tool.ruff.lint.isort] force-single-line = true @@ -115,6 +115,7 @@ keep-runtime-typing = true "tests/**" = [ "D", "S101", + "S105", "S106", "ARG", "ANN", @@ -130,10 +131,14 @@ keep-runtime-typing = true "S101", # Internal invariant guard, not a validation shortcut ] "fastapi_cachex/backends/memcached.py" = ["PLC0415"] # Optional dependency -"fastapi_cachex/backends/redis.py" = ["PLR0913", "PLC0415"] # Optional dependency, Redis config -"fastapi_cachex/lock.py" = ["PLR0913"] # Configurable lock parameters -"fastapi_cachex/proxy.py" = ["PLW0603"] # Global backend management by design -"fastapi_cachex/session/manager.py" = ["PLR0915"] # Session/security validation branches needed +# PLR0917: these constructors take many positional options; making them +# keyword-only would break the public API. +"fastapi_cachex/backends/redis.py" = ["PLR0913", "PLR0917", "PLC0415"] # Optional dependency, Redis config +"fastapi_cachex/lock.py" = ["PLR0913", "PLR0917"] # Configurable lock parameters +"fastapi_cachex/session/manager.py" = [ + "PLR0915", # Session/security validation branches needed + "S105", # token_format = "simple" / "jwt" is not a password +] [tool.ruff.format] docstring-code-format = true @@ -158,8 +163,10 @@ exclude_lines = [ "if TYPE_CHECKING:", "raise NotImplementedError", "if __name__ == .__main__.:", - "pass", - "\\.\\.\\.", + # Anchored: a bare "pass" or "..." would also exclude any line (and the + # block it opens) containing "password", "bypass" or "Field(...". + "^\\s*pass\\s*(#.*)?$", + "^\\s*\\.\\.\\.\\s*(#.*)?$", ] fail_under = 90 show_missing = true