|
| 1 | +--- |
| 2 | +summary: Lower the supported Python floor from 3.13 to 3.11, rewriting retry.py off PEP 695 and proving 3.11-3.14 in a CI matrix. |
| 3 | +--- |
| 4 | + |
| 5 | +# Design: Add Python 3.11 / 3.12 support |
| 6 | + |
| 7 | +## Summary |
| 8 | + |
| 9 | +Lower the package's supported Python floor from 3.13 to 3.11. The runtime |
| 10 | +dependencies (tenacity, sqlalchemy, asyncpg) already support 3.11+, so the only |
| 11 | +source change is `retry.py`, which uses PEP 695 generic syntax that is a syntax |
| 12 | +error before 3.12. We rewrite those declarations to the pre-695 stdlib |
| 13 | +`ParamSpec`/`TypeVar`/`TypeAlias` form (no new dependency), widen |
| 14 | +`requires-python` and the classifiers, lower the ruff target, and add a CI |
| 15 | +pytest matrix over 3.11-3.14 that proves every supported version at runtime. |
| 16 | +This mirrors the established `faststream-redis-timers` convention in the same |
| 17 | +org. |
| 18 | + |
| 19 | +## Motivation |
| 20 | + |
| 21 | +`postgres_retry` and friends have no runtime feature that requires 3.13 — the |
| 22 | +floor is incidental, set by the PEP 695 syntax in `retry.py`. Widening to 3.11 |
| 23 | +broadens adoption (3.11 and 3.12 are still widely deployed) at the cost of one |
| 24 | +mechanical typing rewrite. The sibling `faststream-redis-timers` package already |
| 25 | +ships `requires-python = ">=3.11,<4"` with a 3.11-3.14 CI matrix; aligning |
| 26 | +`db-retry` keeps the org's packages consistent. |
| 27 | + |
| 28 | +## Non-goals |
| 29 | + |
| 30 | +- No runtime behavior change: the retry/failover/transaction semantics are |
| 31 | + identical on every version. |
| 32 | +- No new dependency: `typing_extensions` is not needed — `ParamSpec`, |
| 33 | + `TypeVar`, and `TypeAlias` are stdlib since 3.10. |
| 34 | +- No new tests and no version-gated code branches: the existing suite runs |
| 35 | + unchanged across the matrix. |
| 36 | + |
| 37 | +## Design |
| 38 | + |
| 39 | +### 1. `retry.py` — rewrite off PEP 695 |
| 40 | + |
| 41 | +The current module uses two 3.12+ constructs that are hard syntax errors on |
| 42 | +3.11: the `type` alias statement and generic-function syntax (`def f[**P, T]`). |
| 43 | + |
| 44 | +Today (3.12+ only): |
| 45 | + |
| 46 | +```python |
| 47 | +type _Func[**P, T] = typing.Callable[P, typing.Coroutine[None, None, T]] |
| 48 | +type _Decorator[**P, T] = typing.Callable[[_Func[P, T]], _Func[P, T]] |
| 49 | + |
| 50 | +@typing.overload |
| 51 | +def postgres_retry[**P, T](func: _Func[P, T], *, retries: int | None = ...) -> _Func[P, T]: ... |
| 52 | +``` |
| 53 | + |
| 54 | +Rewritten (3.11-compatible), keeping the named aliases: |
| 55 | + |
| 56 | +```python |
| 57 | +P = typing.ParamSpec("P") |
| 58 | +T = typing.TypeVar("T") |
| 59 | + |
| 60 | +_Func: typing.TypeAlias = typing.Callable[P, typing.Coroutine[None, None, T]] |
| 61 | +_Decorator: typing.TypeAlias = typing.Callable[[_Func], _Func] |
| 62 | + |
| 63 | +@typing.overload |
| 64 | +def postgres_retry(func: _Func, *, retries: int | None = ...) -> _Func: ... |
| 65 | +@typing.overload |
| 66 | +def postgres_retry(func: None = ..., *, retries: int | None = ...) -> _Decorator: ... |
| 67 | +def postgres_retry(func: _Func | None = None, *, retries: int | None = None) -> _Func | _Decorator: |
| 68 | + ... # body unchanged (lines 39-55) |
| 69 | +``` |
| 70 | + |
| 71 | +`P` and `T` move to module scope; the bare aliases re-bind them per signature, |
| 72 | +which is exactly how pre-695 generic aliases work. The function body — the |
| 73 | +`tenacity.AsyncRetrying` construction and the `decorator`/`wrapped_method` |
| 74 | +nesting — is untouched. The two public call forms (`@postgres_retry` and |
| 75 | +`@postgres_retry(retries=N)`) and their inferred types are preserved. |
| 76 | + |
| 77 | +### 2. `pyproject.toml` |
| 78 | + |
| 79 | +- `requires-python = ">=3.13,<4"` -> `">=3.11,<4"`. |
| 80 | +- Add classifiers `Programming Language :: Python :: 3.11` and `:: 3.12` (the |
| 81 | + 3.13 / 3.14 entries stay). |
| 82 | +- `[tool.ruff] target-version = "py313"` -> `"py311"`, so lint catches any |
| 83 | + 3.11-incompatible syntax introduced later (and stops suggesting 3.12+-only |
| 84 | + upgrades). |
| 85 | + |
| 86 | +### 3. CI — `.github/workflows/_checks.yml` |
| 87 | + |
| 88 | +The `pytest` job gains a version matrix mirroring `faststream-redis-timers`: |
| 89 | + |
| 90 | +```yaml |
| 91 | + pytest: |
| 92 | + runs-on: ubuntu-latest |
| 93 | + strategy: |
| 94 | + fail-fast: false |
| 95 | + matrix: |
| 96 | + python-version: ["3.11", "3.12", "3.13", "3.14"] |
| 97 | + services: |
| 98 | + postgres: |
| 99 | + ... # unchanged |
| 100 | + steps: |
| 101 | + - uses: actions/checkout@v6 |
| 102 | + - uses: astral-sh/[email protected] |
| 103 | + - run: uv python install ${{ matrix.python-version }} |
| 104 | + - run: | |
| 105 | + uv sync --all-extras --no-install-project |
| 106 | + uv run --no-sync pytest . --cov=. --cov-report xml |
| 107 | + env: |
| 108 | + ... # unchanged |
| 109 | +``` |
| 110 | +
|
| 111 | +The `lint` job stays pinned to `uv python install 3.13` (single-version gate). |
| 112 | + |
| 113 | +### 4. Docs |
| 114 | + |
| 115 | +- `README.md:216`: "Python 3.13+" -> "Python 3.11+". The pyversions badge is |
| 116 | + driven by PyPI metadata and updates automatically. |
| 117 | +- Promote into `architecture/retry.md` in the implementing PR (note the |
| 118 | + pre-695 typing form so the capability page stays code-current). |
| 119 | + |
| 120 | +## Testing |
| 121 | + |
| 122 | +- `just lint-ci` (ruff `--check` + `ty check`) passes with `target-version = |
| 123 | + py311`; `ty check` confirms the rewritten overloads still type-check. |
| 124 | +- `uv run pytest` passes locally against `DB_DSN`. |
| 125 | +- CI proves runtime correctness on 3.11, 3.12, 3.13, and 3.14, each holding the |
| 126 | + existing `--cov-fail-under=100` gate. |
| 127 | + |
| 128 | +## Risk |
| 129 | + |
| 130 | +- **Low: the rewritten generic aliases type-check differently than PEP 695.** |
| 131 | + Mitigation: `ty check` in the lint gate plus the unchanged overload tests |
| 132 | + catch any inference regression before merge. |
| 133 | +- **Low: a transitive dependency drops 3.11 wheels.** Mitigation: the 3.11 CI |
| 134 | + leg fails loudly at `uv sync` if so; deps are unpinned and currently all |
| 135 | + publish 3.11 wheels. |
0 commit comments