Skip to content

Commit 99f4fed

Browse files
WilliamBergaminClaude
andauthored
docs(deps): establish and apply a managing-dependencies convention (#1915)
Co-authored-by: Claude <svc-devxp-claude@slack-corp.com>
1 parent c1e2c10 commit 99f4fed

7 files changed

Lines changed: 198 additions & 21 deletions

File tree

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
---
2+
name: managing-dependencies
3+
description: >-
4+
Use when adding, updating, pinning, or reviewing any dependency in this repo's requirements/*.txt files, including taking a Dependabot bump or writing a requirement line by hand. Also use when a pip install fails on the older Python jobs while newer ones pass, or when CI errors with "Could not find a version that satisfies", "No matching distribution found", "Requires-Python >=3.x", or "ResolutionImpossible". Triggers include: "add <package> to requirements", "bump/update <package>", "pin <package>", "fix this dependabot PR", "CI can't install <package> on Python 3.7/3.8/3.9", "requires-python error in the install step". This skill defines the layout and version-constraint conventions to follow; reach for it before hand-editing any requirements/*.txt file.
5+
---
6+
7+
# Managing dependencies
8+
9+
## Overview
10+
11+
Every `requirements/*.txt` file in this repo follows **one layout convention** and **one version-constraint pattern**. This keeps a single set of pins working across the whole Python matrix and every diff readable. The mechanism for making one line behave differently per interpreter is [PEP 508](https://peps.python.org/pep-0508/) environment markers. That spec is the authority for the marker syntax used throughout this skill.
12+
13+
Two facts about this repo drive everything below:
14+
15+
- It supports **Python `>=3.7`** (`requires-python` and the classifiers in `pyproject.toml`). Its CI matrix in `.github/workflows/ci-build.yml` tests every version from **3.7 through 3.14 plus PyPy** (`pypy3.10`, `pypy3.11`).
16+
- Many popular packages keep raising their minimum Python. A bump that raises a dependency's **lower bound** to a release requiring a newer Python makes pip's resolver find nothing installable on the old interpreters. The install step then fails there before tests even run.
17+
18+
The convention resolves this without dropping old-Python support. Pin each interpreter to the newest release it can actually install, using PEP 508 `python_version` markers.
19+
20+
## File-layout convention
21+
22+
Each file lists **one dependency per section**: Name of the dependency, an optional rationale note (starting with `Note:`, explaining why a version is pinned or split), then the requirement line(s), separated from the next section by a blank line. This makes every pin self-documenting.
23+
24+
```
25+
# pytest
26+
pytest>=7.0.1,<9
27+
28+
# pytest-cov
29+
# Note: pytest-cov 7.1+ requires Python >=3.9; cap older interpreters below it.
30+
pytest-cov>=4,<7.1.0; python_version < "3.9"
31+
pytest-cov>=7.1.0,<8; python_version >= "3.9"
32+
```
33+
34+
Keep this layout when adding or editing dependencies. Never leave an empty trailing `;` (a fossil of a collapsed split; delete it).
35+
36+
## Which files need Python-version markers
37+
38+
A marker split is only needed for requirements files installed across the **full** Python matrix. Which file you are editing decides this. To see where a file is installed, read `.github/workflows/ci-build.yml`. It is the source of truth for which Python versions install which requirements files.
39+
40+
| File | Installed on | Needs markers? |
41+
| -------------------------------- | --------------------------------------------------- | ----------------------------------- |
42+
| `requirements/testing.txt` | full matrix (3.7 → 3.14, pypy) | **Yes, if a bump raises the floor** |
43+
| `requirements/optional.txt` | full matrix (also packaged as the `optional` extra) | **Yes, if a bump raises the floor** |
44+
| `requirements/tools.txt` | latest supported Python only | No, just take the bump |
45+
| `requirements/databases.txt` | latest supported Python only (databases job) | No, just take the bump |
46+
| `requirements/documentation.txt` | not installed in CI (manual docs script only) | No, just take the bump |
47+
48+
If the bump lands in a latest-Python-only file, take it as-is. No markers, no ceiling, just the layout convention above.
49+
50+
**`optional.txt` is special:** it is read into wheel metadata via `[tool.setuptools.dynamic]` in `pyproject.toml` (`optional-dependencies.optional`). Use **full-line comments only** there, never trailing inline comments, so nothing leaks into the packaged metadata.
51+
52+
## The version-constraint pattern
53+
54+
When a dependency's floor rises to a release that requires a newer Python, **do not** just take the bump, and **do not** drop old-Python support to make CI pass. Instead, split the requirement into `python_version`-marked lines that **partition the whole matrix**. Every interpreter matches exactly one line. Old interpreters keep the last compatible release (with an explicit ceiling), and the newest line is open-ended so future Pythons stay covered.
55+
56+
```
57+
# pytest
58+
# Note: pytest 9 requires Python >=3.10; cap older interpreters below it.
59+
pytest>=7.0.1,<9; python_version < "3.10"
60+
pytest>=9.1.1,<10; python_version >= "3.10"
61+
```
62+
63+
## Canonical marker style
64+
65+
Consistency matters because these lines are read and edited often, and a stray style makes diffs noisy. Standardize on this:
66+
67+
- Spaces around every operator: `python_version >= "3.10"`, never `python_version>="3.10"`.
68+
- Double-quoted `major.minor` string: `"3.10"`. (`packaging` compares these version-aware, so
69+
`"3.10" > "3.9"` is correctly true, no lexicographic surprise.)
70+
- Use `>=` / `<` for the Python boundary; avoid `>` / `<=` so the boundary version lands on exactly one side.
71+
- One space after the `;`, none before: `pkg>=1,<2; python_version >= "3.10"`.
72+
- The old-side line always carries an explicit upper bound (the floor-jump version).
73+
- The marker set must be **exhaustive and mutually exclusive** across the matrix. The newest line ends open-ended (`>= "X.Y"`), never a bare `==` that leaves future Pythons unmatched.
74+
75+
## Deriving the versions to pin
76+
77+
You need two numbers: the **floor** (which Python the new release requires) and the old-side **ceiling** (the first release that raised that floor).
78+
79+
1. **Floor.** Read the metadata for the _exact target version_ at `https://pypi.org/pypi/<package>/<target-version>/json`. The `info.requires_python` field gives the new minimum (e.g. `">=3.10"`). A `null` there means the release declares no floor.
80+
81+
2. **Ceiling.** Walk the release history at `https://pypi.org/pypi/<package>/json` and find the **first version that raised the floor above the oldest matrix Python**. The old-side ceiling is `< <that version>`. For example, if `aiodns` jumped to `>=3.10` at version **4.0.0**, the old-side cap is `<4` even if the target is `4.0.4` (pinning `<4.0.4` would wrongly admit 4.0.0–4.0.3, which are also 3.10-only).
82+
83+
Why an explicit ceiling instead of trusting pip to filter by `Requires-Python`? Because that filtering only holds if every future release keeps its metadata correct; a single mis-tagged release would silently float onto an untested interpreter. An explicit ceiling makes the intent self-documenting and robust.
84+
85+
**If you arrived here from a red CI job:** the failing _install_ log is ground truth. It names the interpreter that failed and the versions pip was actually offered, e.g.:
86+
87+
```
88+
ERROR: Ignored the following versions that require a different python version: 4.0.4 Requires-Python >=3.10
89+
ERROR: Could not find a version that satisfies the requirement aiodns>=4.0.4 (from versions: ..., 3.6.0, 3.6.1)
90+
```
91+
92+
Cross-check the PyPI value against that log so you are never guessing. (If instead the failure is a real test failure, or hits _every_ Python version, this pattern does not apply, so investigate the bump normally.)
93+
94+
## Two harder shapes
95+
96+
**Different old Pythons need different ceilings.** When several old interpreters each cap at a different release, emit one line per divergent interpreter with `== "X.Y"`, then a final open-ended line. This is the `aiohttp` pattern already in `testing.txt` / `optional.txt`:
97+
98+
```
99+
# aiohttp
100+
# Note: aiohttp's minimum Python rose in stages; cap each old interpreter at its last compatible release.
101+
aiohttp>=3.7.3,<3.9; python_version == "3.7"
102+
aiohttp>=3.7.3,<3.11; python_version == "3.8"
103+
aiohttp>=3.13.5,<4; python_version >= "3.9"
104+
```
105+
106+
**Only PyPy breaks.** Sometimes a bump drops PyPy _wheels_ while CPython at the same version is fine (the failing jobs are `pypy*`, not `3.7`/`3.8`/`3.9`). A `python_version` split would wrongly downgrade CPython too. Gate on the implementation instead, as in the `cryptography` pattern in `testing.txt`. Note the exact marker name and casing: **`implementation_name == "pypy"`** (lowercase `pypy`), not `platform_python_implementation`. A single gated line is enough. You do **not** need a second negated line for the other interpreters, because an unmarked line already applies everywhere the marked one is skipped.
107+
108+
```
109+
# cryptography
110+
# Note: cryptography 46+ dropped PyPy 3.10 wheels; pin to <46 for PyPy 3.10 only.
111+
cryptography<46; implementation_name == "pypy" and python_version == "3.10"
112+
```
113+
114+
## Collapse when a Python is dropped
115+
116+
Marker splits are maintenance cost, so remove them when they stop earning their keep. When a Python version is dropped from the CI matrix (and from `requires-python` / the classifiers), collapse any split whose only reason was that version back into a single unmarked line, and delete the trailing `;`. A leaner file is easier for both humans and Dependabot to reason about.
117+
118+
## What to leave alone
119+
120+
- **Do not touch `requires-python` or the CI matrix.** Keeping 3.7 working on old dependency versions is the entire point; changing the floor is a separate, deliberate decision.
121+
- **Prefer markers over a Dependabot `ignore`.** An `ignore` rule freezes newer Pythons on the old version too, and hides the version knowledge in config. Reserve `ignore` for the rare dep that must stay pinned everywhere for reproducible output.

AGENTS.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,3 +310,5 @@ The SDK has **zero required runtime dependencies** for the core sync Web API cli
310310
- `aiodns` — faster DNS resolution for async
311311

312312
Version constraints for optional and dev dependencies are pinned in the `requirements/` directory.
313+
314+
Before adding, bumping, pinning, or reviewing any dependency in `requirements/*.txt`, whether a Dependabot PR or a manual edit, follow the `managing-dependencies` skill in `.claude/skills/`. It defines the layout and `python_version` marker conventions that keep one set of pins working across the full Python matrix.

requirements/databases.txt

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
1-
# Database drivers for CI testing
1+
# pip install -r requirements/databases.txt
2+
# Database drivers for CI testing.
23

3-
# PostgreSQL drivers
4+
# psycopg2-binary
5+
# Note: postgreSQL driver (sync).
46
psycopg2-binary>=2.9.12,<3
7+
8+
# asyncpg
9+
# Note: postgreSQL driver (async).
510
asyncpg>=0.31.0,<1

requirements/documentation.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,7 @@
1+
# pip install -r requirements/documentation.txt
2+
3+
# docutils
14
docutils==0.23
5+
6+
# pdoc3
27
pdoc3==0.11.6

requirements/optional.txt

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,32 @@
11
# pip install -r requirements/optional.txt
2-
# async modules depend on aiohttp
2+
# Note: comments here must be full-line only; this file is read into wheel
3+
# metadata via [tool.setuptools.dynamic] in pyproject.toml.
4+
5+
# aiodns
6+
# Note: async modules depend on aiohttp
37
aiodns>1.0
8+
9+
# aiohttp
410
# We recommend using 3.7.1+ for RTMClient
511
# https://github.com/slackapi/python-slack-sdk/issues/912
12+
# Note: aiohttp's minimum Python rose in stages; cap each old interpreter at its last compatible release.
613
aiohttp>=3.7.3,<3.9; python_version == "3.7"
714
aiohttp>=3.7.3,<3.11; python_version == "3.8"
815
aiohttp>=3.13.5,<4; python_version >= "3.9"
9-
# used only under slack_sdk/*_store
16+
17+
# boto3
18+
# Note: used only under slack_sdk/*_store
1019
boto3<=2
20+
21+
# SQLAlchemy
1122
# InstallationStore/OAuthStateStore
1223
# Since v3.20, we no longer support SQLAlchemy 1.3 or older.
1324
# If you need to use a legacy version, please add our v3.19.5 code to your project.
1425
SQLAlchemy>=2.0.49,<3
15-
# Socket Mode
16-
# websockets 9 is not compatible with Python 3.10
26+
27+
# websockets
28+
# Note: websockets 9 is not compatible with Python 3.10; floor at 9.1.
1729
websockets>=9.1,<16
30+
31+
# websocket-client
1832
websocket-client>=1,<2

requirements/testing.txt

Lines changed: 36 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,46 @@
11
# pip install -r requirements/testing.txt
2-
aiohttp<3.9; python_version == "3.7" # used for a WebSocket server mock
3-
aiohttp<3.11; python_version == "3.8" # used for a WebSocket server mock
4-
aiohttp<4; python_version >= "3.9" # used for a WebSocket server mock
2+
3+
# aiohttp
4+
# Note: Used for a WebSocket server mock.
5+
aiohttp<3.9; python_version == "3.7"
6+
aiohttp<3.11; python_version == "3.8"
7+
aiohttp<4; python_version >= "3.9"
8+
9+
# pytest
510
pytest>=7.0.1,<9
6-
pytest-asyncio<2 # for async
11+
12+
# pytest-asyncio
13+
# Note: for async.
14+
pytest-asyncio<2
15+
16+
# pytest-cov
17+
# Note: pytest-cov 7.1+ requires Python >=3.9; cap older interpreters below it.
718
pytest-cov>=4,<7.1.0; python_version < "3.9"
819
pytest-cov>=7.1.0,<8; python_version >= "3.9"
9-
click==8.0.4 # black is affected by https://github.com/pallets/click/issues/2225
20+
21+
# click
22+
# Note: black is affected by https://github.com/pallets/click/issues/2225; pin click to 8.0.4.
23+
click==8.0.4
24+
25+
# psutil
1026
psutil>=7.2.2,<8
11-
# cryptography 46+ dropped PyPy 3.10 wheels; pin to <46 for PyPy 3.10 only
27+
28+
# cryptography
29+
# Note: cryptography 46+ dropped PyPy 3.10 wheels; pin to <46 for PyPy 3.10 only.
1230
cryptography<46; implementation_name == "pypy" and python_version == "3.10"
13-
# used only under slack_sdk/*_store
31+
32+
# boto3
33+
# Note: used only under slack_sdk/*_store.
1434
boto3<=2
15-
# For AWS tests
35+
36+
# moto
37+
# Note: for AWS tests.
1638
moto>=4.2.14,<6
17-
# For AsyncSQLAlchemy tests
39+
40+
# greenlet
41+
# Note: for AsyncSQLAlchemy tests.
1842
greenlet<=4
43+
44+
# aiosqlite
45+
# Note: for AsyncSQLAlchemy tests.
1946
aiosqlite<=1

requirements/tools.txt

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
1-
mypy<=2.1.0;
2-
# while flake8 5.x have issues with Python 3.12, flake8 6.x requires Python >= 3.8.1,
3-
# so 5.x should be kept in order to stay compatible with Python 3.7/3.8
1+
# pip install -r requirements/tools.txt
2+
3+
# mypy
4+
mypy<=2.1.0
5+
6+
# flake8
47
flake8>=7.3.0,<8
5-
# Don't change this version without running CI builds;
6-
# The latest version may not be available for older Python runtime
7-
black==24.3.0;
8+
9+
# black
10+
black==24.3.0

0 commit comments

Comments
 (0)