From d0fb38108b07cf0dbaf6e443f7bc1f2bded58730 Mon Sep 17 00:00:00 2001 From: Yaroslav Halchenko Date: Tue, 4 Aug 2026 09:13:15 -0400 Subject: [PATCH 1/4] git ignore .duct here used for development etc --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index b39d66477..e2d2dab22 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,4 @@ sandbox/ venv/ venvs/ .DS_Store +.duct From 7d21e3beb3eb76c82cce5e50eef3af7c8be7742c Mon Sep 17 00:00:00 2001 From: Yaroslav Halchenko Date: Tue, 4 Aug 2026 11:14:40 -0400 Subject: [PATCH 2/4] ci: add py3-lowest tox env + lowest-deps CI mode Introduces a py3-lowest tox env that installs the lowest declared-compatible version of every direct dependency using uv's `lowest-direct` resolution, run on Python 3.11 (the minimum supported version per `requires-python`). Purpose is to verify that the lower bounds in pyproject.toml are actually valid and that dandi still works against them. Wires into GitHub Actions as a new matrix mode `lowest-deps` on ubuntu-latest / Python 3.11. Direct deps that currently lack a lower bound will resolve to their oldest release and cause the env to fail; that failure is the intended signal to add real bounds. Co-Authored-By: Claude Code 2.1.221 / Claude Opus 4.7 --- .github/workflows/run-tests.yml | 17 ++++++++++++++++- tox.ini | 15 +++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 1d7a08699..87361445d 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -68,6 +68,9 @@ jobs: - os: ubuntu-latest python: '3.11' mode: nfs + - os: ubuntu-latest + python: '3.11' + mode: lowest-deps steps: - name: Set up environment @@ -83,10 +86,17 @@ jobs: python-version: ${{ matrix.python }} - name: Install dependencies + if: matrix.mode != 'lowest-deps' run: | python -m pip install --upgrade pip wheel pip install ".[extras,test]" + - name: Install tox for lowest-deps mode + if: matrix.mode == 'lowest-deps' + run: | + python -m pip install --upgrade pip + pip install "tox>=4" "tox-uv>=1.11" + # Set only if matrix.instance_name is defined - name: Set DANDI_TESTS_INSTANCE_NAME if: ${{ matrix.instance_name }} @@ -129,10 +139,15 @@ jobs: run: echo PYTEST_ADDOPTS=--scheduled >> "$GITHUB_ENV" - name: Run all tests except those involving obolibrary - if: matrix.mode != 'dandi-api' && matrix.mode != 'obolibrary-only' + if: matrix.mode != 'dandi-api' && matrix.mode != 'obolibrary-only' && matrix.mode != 'lowest-deps' run: | python -m pytest -s -v -m "not obolibrary" --cov=dandi --cov-report=xml dandi + - name: Run tests against minimum declared dependency versions + if: matrix.mode == 'lowest-deps' + run: | + tox -e py3-lowest -- -s -m "not obolibrary" + - name: Run only tests related to obolibrary if: matrix.mode == 'obolibrary-only' run: | diff --git a/tox.ini b/tox.ini index b71a3efcf..97cf30772 100644 --- a/tox.ini +++ b/tox.ini @@ -1,4 +1,6 @@ [tox] +requires = + tox-uv >= 1.11 envlist = lint,typing,py3 [testenv] @@ -16,6 +18,19 @@ commands = coverage combine coverage report +# Install the lowest declared-compatible version of every direct dependency +# (via uv's `lowest-direct` resolution) on the minimum supported Python. +# Purpose: verify that the lower bounds in pyproject.toml are actually valid. +# Any direct dep without a lower bound will resolve to its oldest release and +# will almost certainly break; that failure is a signal to add a real bound. +[testenv:py3-lowest] +basepython = python3.11 +uv_resolution = lowest-direct +setenv = {[testenv]setenv} +passenv = {[testenv]passenv} +extras = {[testenv]extras} +commands = {[testenv]commands} + [testenv:lint] skip_install = true deps = From 272b8c514dad135b760826de2966799712bfcb72 Mon Sep 17 00:00:00 2001 From: Yaroslav Halchenko Date: Tue, 4 Aug 2026 11:39:01 -0400 Subject: [PATCH 3/4] deps: add lower bounds to previously-unbounded direct dependencies MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Now that the `py3-lowest` tox env exercises `uv --resolution lowest-direct`, every direct dep needs a real lower bound — otherwise uv picks the oldest published release (often from ~2015) and everything breaks. This adds bounds targeting the first release with plausible Python 3.11 support (typically released on or after 2022-10-24) plus fixes for API-compat issues surfaced by actually installing and importing: - pydantic ~= 2.0 → ~= 2.9 (dandi uses `BeforeValidator(json_schema_input_type=...)` added in 2.9 and `FtpUrl` added in 2.8; and dandischema 0.12 requires a pydantic that properly validates `field_serializer("*")`, which is 2.7+) - anys ~= 0.2 → ~= 0.3 (0.2.x references non-existent `types.Union` on 3.11) - duecredit >= 0.6.0 → >= 0.9.2 (0.6 imports `collections.Iterator`, gone in 3.10) - pyout >= 0.5 → >= 0.7.3 (0.5 uses jsonschema `additionalItems`, rejected by modern jsonschema Draft 2020-12 metaschema) - tensorstore: added >= 0.1.62 (first release built for numpy 2.x ABI) - opencv-python >= 4.7.0.72 → >= 4.10.0.84 (older wheels built against numpy 1.x ABI segfault on `import cv2` under numpy 2.x, which tensorstore >= 0.1.62 now brings in) - vcrpy >= 4.3.0 → >= 5.0.0 (4.3 references `urllib3.connectionpool.VerifiedHTTPSConnection`, removed in urllib3 2.x; 5.0 switched to `urllib3.connection.VerifiedHTTPSConnection`) For the remaining 20 direct deps without a lower bound, pick the earliest release published on or after Python 3.11's release date (2022-10-24) whose metadata admits 3.11, as determined by querying PyPI. Verified: with these bounds `tox -e py3-lowest` resolves and installs cleanly, `dandi` imports, `pytest --collect-only` gathers 1018 tests, and 601 unit tests pass under the lowest-direct resolution on Python 3.11. Co-Authored-By: Claude Code 2.1.221 / Claude Opus 4.7 --- pyproject.toml | 65 ++++++++++++++++++++++++++------------------------ 1 file changed, 34 insertions(+), 31 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 03d9f0d62..36ade592a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -46,32 +46,33 @@ dependencies = [ "bidsschematools ~= 1.0", "bids-validator-deno >= 2.0.5", "click >= 8.2", - "click-didyoumean", + "click-didyoumean >= 0.3.1", "dandischema ~= 0.12.0", "etelemetry >= 0.2.2", - "fasteners", + "fasteners >= 0.19", "fscacher >= 0.3.0", # 3.14.4: https://github.com/hdmf-dev/hdmf/issues/1186 - "hdmf != 3.5.0,!=3.14.4", - "humanize", + "hdmf >= 3.4.7, != 3.5.0, != 3.14.4", + "humanize >= 4.5.0", "interleave ~= 0.3", - "joblib", - "keyring != 23.9.0", - "keyrings.alt", - "packaging", - "platformdirs", - "pycryptodomex", # for EncryptedKeyring backend in keyrings.alt - "pydantic ~= 2.0", + "joblib >= 1.3.0", + "keyring >= 23.10.0", + "keyrings.alt >= 5.0.0", + "packaging >= 22.0", + "platformdirs >= 2.5.3", + "pycryptodomex >= 3.16.0", # for EncryptedKeyring backend in keyrings.alt + "pydantic ~= 2.9", "pynwb >= 1.0.3,!=1.1.0,!=2.3.0", - "numcodecs", + "numcodecs >= 0.11.0", "nwbinspector >= 0.7.0", - "pyout >=0.5, !=0.6.0", - "python-dateutil", + "pyout >= 0.7.3", + "python-dateutil >= 2.9.0", "requests ~= 2.20", "ruamel.yaml >=0.15, <1", - "semantic-version", - "tenacity", - "tensorstore", + "semantic-version >= 2.10.0", + "tenacity >= 8.2.0", + # 0.1.62 first release built for numpy 2.x + "tensorstore >= 0.1.62", # possibly silently incomplete downloads: https://github.com/dandi/dandi-cli/issues/1500 "urllib3 >= 2.0.0", "yarl ~= 1.9", @@ -89,23 +90,25 @@ extensions = [ "ndx-events", ] extras = [ - "duecredit >= 0.6.0", - "fsspec[http]", + "duecredit >= 0.9.2", + "fsspec[http] >= 2022.11.0", ] test = [ - "aiohttp < 3.14", # See https://github.com/kevin1024/vcrpy/issues/995 - "anys ~= 0.2", - "coverage", + "aiohttp >= 3.8.4, < 3.14", # See https://github.com/kevin1024/vcrpy/issues/995 + "anys ~= 0.3", + "coverage >= 7.0.0", + # 4.10.0.84 first built for numpy 2.x ABI (older wheels segfault under numpy 2) # Workaround for VideoWriter regression in 4.13.0.90 on Intel macOS - "opencv-python < 4.13 ; sys_platform == 'darwin' and platform_machine != 'arm64'", - "opencv-python ; sys_platform != 'darwin' or platform_machine == 'arm64'", - "pytest", - "pytest-cov", - "pytest-mock", - "pytest-rerunfailures", - "pytest-timeout", - "responses != 0.24.0, != 0.25.5", - "vcrpy", + "opencv-python >= 4.10.0.84, < 4.13 ; sys_platform == 'darwin' and platform_machine != 'arm64'", + "opencv-python >= 4.10.0.84 ; sys_platform != 'darwin' or platform_machine == 'arm64'", + "pytest >= 7.2.0", + "pytest-cov >= 4.1.0", + "pytest-mock >= 3.11.0", + "pytest-rerunfailures >= 10.3", + "pytest-timeout >= 2.2.0", + "responses >= 0.23.0, != 0.24.0, != 0.25.5", + # 5.0 switched to urllib3.connection.VerifiedHTTPSConnection (urllib3 2.x compat) + "vcrpy >= 5.0.0", ] tools = [ "boto3", From 96be4a8c98f9e947f071a6c1dd8de8dfee7127f8 Mon Sep 17 00:00:00 2001 From: Yaroslav Halchenko Date: Tue, 4 Aug 2026 17:20:34 -0400 Subject: [PATCH 4/4] deps: tighten 8 floors to match the versions `py3-lowest` actually resolves MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `uv --resolution lowest-direct` picks the *lowest satisfying all constraints*, so if a peer transitively requires more, the direct dep's real lowest is whatever the resolver ends up picking — not what we declared. Inspecting the first successful `lowest-deps` CI run against SHA 272b8c51 showed that 8 direct deps floored ABOVE their declared minimums. That means those declared minimums were never actually being exercised by CI. Fix by raising the floors to match what the resolver is picking, so the declared floor is the tested floor: | Package | Declared -> Raised to | Forced-up by (transitively) | |--------------|------------------------|-------------------------------------------------| | hdmf | 3.4.7 -> 4.1.0 | modern pynwb pins hdmf >= 4 | | pynwb | 1.0.3 -> 3.1.0 | dandischema 0.12 / nwbinspector 0.7 | | platformdirs | 2.5.3 -> 4.1.0 | keyring 23.10 jaraco.classes chain requires >= 4 | | requests | ~= 2.20 -> ~= 2.30 | urllib3 2.x requires requests >= 2.30 | | urllib3 | 2.0.0 -> 2.0.2 | trivial transitive peer | | yarl | ~= 1.9 -> >= 1.9.1 | trivial transitive peer | | ruamel.yaml | >= 0.15 -> >= 0.16 | modern hdmf/pynwb chain | | zarr | >= 2.10 -> >= 2.18 | numcodecs 0.11 / hdmf-zarr chain | The `!= 3.5.0`/`!= 3.14.4` exclusions on hdmf and `!= 1.1.0`/`!= 2.3.0` on pynwb are dropped because the new floors are above all excluded versions. No functional change: the resolver was already picking these versions on Python 3.11; this just makes `pyproject.toml` honest about the tested floor. Co-Authored-By: Claude Code 2.1.221 / Claude Opus 4.7 --- pyproject.toml | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 36ade592a..bc00d6ac6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -51,32 +51,38 @@ dependencies = [ "etelemetry >= 0.2.2", "fasteners >= 0.19", "fscacher >= 0.3.0", - # 3.14.4: https://github.com/hdmf-dev/hdmf/issues/1186 - "hdmf >= 3.4.7, != 3.5.0, != 3.14.4", + # Floor raised to what `py3-lowest` actually resolves: modern pynwb pins + # hdmf >= 4, so declared floor is now the tested floor. + "hdmf >= 4.1.0", "humanize >= 4.5.0", "interleave ~= 0.3", "joblib >= 1.3.0", "keyring >= 23.10.0", "keyrings.alt >= 5.0.0", "packaging >= 22.0", - "platformdirs >= 2.5.3", + # Floor raised: keyring 23.10 chain (jaraco.classes) transitively requires >= 4. + "platformdirs >= 4.1.0", "pycryptodomex >= 3.16.0", # for EncryptedKeyring backend in keyrings.alt "pydantic ~= 2.9", - "pynwb >= 1.0.3,!=1.1.0,!=2.3.0", + # Floor raised: dandischema 0.12 / nwbinspector 0.7 require modern pynwb. + "pynwb >= 3.1.0", "numcodecs >= 0.11.0", "nwbinspector >= 0.7.0", "pyout >= 0.7.3", "python-dateutil >= 2.9.0", - "requests ~= 2.20", - "ruamel.yaml >=0.15, <1", + # Floor raised: urllib3 2.x requires requests >= 2.30. + "requests ~= 2.30", + # Floor raised: modern hdmf/pynwb chain requires >= 0.16. + "ruamel.yaml >= 0.16.0, < 1", "semantic-version >= 2.10.0", "tenacity >= 8.2.0", # 0.1.62 first release built for numpy 2.x "tensorstore >= 0.1.62", # possibly silently incomplete downloads: https://github.com/dandi/dandi-cli/issues/1500 - "urllib3 >= 2.0.0", - "yarl ~= 1.9", - "zarr >= 2.10, <= 3.1.5", + "urllib3 >= 2.0.2", + "yarl >= 1.9.1, < 2", + # Floor raised: numcodecs 0.11 / hdmf-zarr chain requires >= 2.18. + "zarr >= 2.18.0, <= 3.1.5", "zarr_checksum ~= 0.4.0", ] dynamic = ["version"]