diff --git a/.github/dependabot.yaml b/.github/dependabot.yaml index fccacae6..0761553c 100644 --- a/.github/dependabot.yaml +++ b/.github/dependabot.yaml @@ -15,3 +15,13 @@ updates: - "*" cooldown: default-days: 7 + - package-ecosystem: "pre-commit" + directory: "/" + labels: + - dependencies + - skip news + schedule: + interval: "weekly" + day: "tuesday" + cooldown: + default-days: 7 diff --git a/.github/workflows/check.yaml b/.github/workflows/check.yaml index b2f0c9c7..af0eb365 100644 --- a/.github/workflows/check.yaml +++ b/.github/workflows/check.yaml @@ -1,11 +1,11 @@ -name: "๐Ÿ› ๏ธ check" +name: "๐Ÿงช check" on: workflow_dispatch: push: branches: ["main"] tags-ignore: ["**"] pull_request: - types: [opened, synchronize, reopened, labeled, unlabeled] + types: [opened, synchronize, reopened] schedule: - cron: "0 8 * * *" - cron: "0 6 * * 1" @@ -37,27 +37,29 @@ jobs: exclude: - {os: windows-2025, py: pypy3.11} steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - name: "๐Ÿ“ฅ Check out code" + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 persist-credentials: false - - name: "๐Ÿ”„ Install the latest version of uv" - uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0 + - name: "๐Ÿ Install uv" + uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2 with: + version: latest-known python-version: "3.15" enable-cache: false - - name: "๐Ÿงช Install tox" + - name: "๐Ÿ“ฆ Install tox" run: uv tool install --python-preference only-managed --python 3.15 tox --with tox-uv - - name: "๐Ÿ Install Python" + - name: "๐Ÿ Install Python ${{ matrix.py }}" run: uv python install --python-preference only-managed ${{ matrix.py }} - - name: "โš™๏ธ Setup test suite" + - name: "๐Ÿ“ฆ Set up test suite" run: tox run -vv --notest --skip-missing-interpreters false -e ${{ matrix.py }} env: UV_PYTHON_PREFERENCE: only-managed # PyPy runs unmeasured: coverage has no JIT path there, so the suite takes 12m rather than 2m and roughly 60 of # its lock, heartbeat and poll deadlines lapse under the added latency, though they pass measured in isolation. # PyPy is not a coverage source either: the 100% gate stays CPython's. - - name: "โœ… Run test suite" + - name: "๐Ÿงช Run test suite" run: | if [[ "${{ matrix.py }}" == pypy* ]]; then tox run --skip-pkg-install -e ${{ matrix.py }} -- @@ -69,13 +71,13 @@ jobs: PYTEST_ADDOPTS: "-vv --durations=20" UV_PYTHON_PREFERENCE: only-managed DIFF_AGAINST: HEAD - - name: "๐Ÿ“ Rename coverage report file" + - name: "๐Ÿ“Š Rename coverage report file" if: ${{ !startsWith(matrix.py, 'pypy') }} run: | import os; import sys os.rename(f".tox/.coverage.${{ matrix.py }}", f".tox/.coverage.${{ matrix.py }}-{sys.platform}") shell: python - - name: "๐Ÿ“ฆ Upload coverage data" + - name: "๐Ÿ“ค Upload coverage data" if: ${{ !startsWith(matrix.py, 'pypy') }} uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: @@ -84,32 +86,34 @@ jobs: path: ".tox/.coverage.*" retention-days: 3 graalpy: + name: "๐Ÿงช Test graalpy3.13 - ubuntu-24.04 (weekly)" # GraalPy 25.4.4 can freeze every thread at once under the threaded lock tests, so it stays off pull requests and # pushes. A weekly run keeps the capability probes the suite gates on honest on a second runtime. It runs on one # Linux job because those probes are a per-OS answer, and unmeasured, so the 100% gate stays CPython's. - name: "๐Ÿงช Test graalpy3.13 - ubuntu-24.04 (weekly)" if: github.event_name == 'workflow_dispatch' || github.event.schedule == '0 6 * * 1' runs-on: ubuntu-24.04 timeout-minutes: 60 steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - name: "๐Ÿ“ฅ Check out code" + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 persist-credentials: false - - name: "๐Ÿ”„ Install the latest version of uv" - uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0 + - name: "๐Ÿ Install uv" + uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2 with: + version: latest-known python-version: "3.15" enable-cache: false - - name: "๐Ÿงช Install tox" + - name: "๐Ÿ“ฆ Install tox" run: uv tool install --python-preference only-managed --python 3.15 tox --with tox-uv - name: "๐Ÿ Install Python" run: uv python install --python-preference only-managed graalpy3.13 - - name: "โš™๏ธ Setup test suite" + - name: "๐Ÿ“ฆ Set up test suite" run: tox run -vv --notest --skip-missing-interpreters false -e graalpy3.13 env: UV_PYTHON_PREFERENCE: only-managed - - name: "โœ… Run test suite" + - name: "๐Ÿงช Run test suite" run: tox run --skip-pkg-install -e graalpy3.13 -- env: PYTEST_ADDOPTS: "-vv --durations=20" @@ -119,11 +123,12 @@ jobs: runs-on: ubuntu-24.04 needs: test steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - name: "๐Ÿ“ฅ Check out code" + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 persist-credentials: false - - name: "โฌ‡๏ธ Download coverage data" + - name: "๐Ÿ“ฉ Download coverage data" id: coverage-data background: true uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 @@ -131,20 +136,22 @@ jobs: path: .tox pattern: .coverage.* merge-multiple: true - - name: "๐Ÿ”„ Install the latest version of uv" - uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0 + - name: "๐Ÿ Install uv" + uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2 with: + version: latest-known python-version: "3.15" enable-cache: false - - name: "๐Ÿงช Install tox" + - name: "๐Ÿ“ฆ Install tox" run: uv tool install --python-preference only-managed --python 3.15 tox --with tox-uv - - name: "๐Ÿ“ฆ Build package to generate version" + - name: "๐Ÿ—๏ธ Build package to generate version" run: uv build --python 3.15 --python-preference only-managed --wheel . --out-dir dist - - name: "โš™๏ธ Setup coverage tool" + - name: "๐Ÿ“ฆ Set up coverage tool" run: tox -e coverage --notest env: UV_PYTHON_PREFERENCE: only-managed - - wait: coverage-data + - name: "โณ Wait for coverage data" + wait: coverage-data - name: "๐Ÿ“Š Combine and report coverage" run: tox -e coverage --skip-pkg-install env: @@ -155,7 +162,7 @@ jobs: name: html-report path: .tox/htmlcov check: - name: "๐Ÿ”Ž ${{ matrix.tox_env }} - ${{ matrix.os }}" + name: "๐Ÿ” ${{ matrix.tox_env }} - ${{ matrix.os }}" runs-on: ${{ matrix.os }} strategy: fail-fast: false @@ -171,38 +178,42 @@ jobs: exclude: - {os: windows-2025, tox_env: pkg_meta} steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - name: "๐Ÿ“ฅ Check out code" + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 persist-credentials: false - - name: "๐Ÿ”„ Install the latest version of uv" - uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0 + - name: "๐Ÿ Install uv" + uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2 with: + version: latest-known python-version: "3.15" enable-cache: false - - name: "๐Ÿงช Install tox" + - name: "๐Ÿ“ฆ Install tox" run: uv tool install --python-preference only-managed --python 3.15 tox --with tox-uv - - name: "โš™๏ธ Setup test suite" + - name: "๐Ÿ“ฆ Set up check suite" run: tox run -vv --notest --skip-missing-interpreters false -e ${{ matrix.tox_env }} env: UV_PYTHON_PREFERENCE: only-managed - - name: "โœ… Run test suite" + - name: "๐Ÿงช Run check for ${{ matrix.tox_env }}" run: tox run --skip-pkg-install -e ${{ matrix.tox_env }} env: UV_PYTHON_PREFERENCE: only-managed nfs: - name: "๐Ÿ—„๏ธ NFS mutual exclusion" + name: "๐Ÿงช NFS mutual exclusion" runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - name: "๐Ÿ“ฅ Check out code" + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - - name: "๐Ÿ”„ Install the latest version of uv" - uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0 + - name: "๐Ÿ Install uv" + uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2 with: + version: latest-known python-version: "3.15" enable-cache: false - - name: "๐Ÿ—„๏ธ Export and mount loopback NFS shares" + - name: "โš™๏ธ Export and mount loopback NFS shares" run: | sudo apt-get update sudo apt-get install -y nfs-kernel-server nfs-common @@ -224,7 +235,7 @@ jobs: sudo mount -t nfs -o vers=3,nosharecache,noac,lookupcache=none 127.0.0.1:/export/filelock /mnt/nfs3b for m in nfs4a nfs4b nfs3a nfs3b; do sudo chmod 1777 "/mnt/$m"; done mount | grep /mnt/nfs - - name: "๐Ÿ”’ Verify NFSv4 mutual exclusion across two client caches" + - name: "๐Ÿงช Verify NFSv4 mutual exclusion across two client caches" env: # StrictSoftFileLock is the lock the gate enforces on NFS: its claim queue hands off fairly, so it finishes # every hold with no overlap on both versions (the strict claim retries the transient ESTALE that NFSv3's @@ -242,23 +253,25 @@ jobs: done exit 1 fi - - name: "๐Ÿ”’ Verify NFSv3 mutual exclusion across two client caches" + - name: "๐Ÿงช Verify NFSv3 mutual exclusion across two client caches" env: FILELOCK_VERIFY_LOCKS: StrictSoftFileLock run: timeout 420 .venv/bin/python tasks/verify_filesystem.py /mnt/nfs3a /mnt/nfs3b smb: - name: "๐Ÿ—„๏ธ SMB mutual exclusion" + name: "๐Ÿงช SMB mutual exclusion" runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - name: "๐Ÿ“ฅ Check out code" + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - - name: "๐Ÿ”„ Install the latest version of uv" - uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0 + - name: "๐Ÿ Install uv" + uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2 with: + version: latest-known python-version: "3.15" enable-cache: false - - name: "๐Ÿ—„๏ธ Export and mount a loopback SMB share" + - name: "โš™๏ธ Export and mount a loopback SMB share" run: | sudo apt-get update sudo apt-get install -y samba cifs-utils @@ -281,7 +294,7 @@ jobs: -o "guest,uid=$(id -u),gid=$(id -g),file_mode=0777,dir_mode=0777,vers=3.0" done mount | grep /mnt/smb - - name: "๐Ÿ”’ Verify SMB mutual exclusion across two client caches" + - name: "๐Ÿงช Verify SMB mutual exclusion across two client caches" # CIFS byte-range locks are mandatory and server-enforced, so FileLock excludes and finishes every hold: it is # the lock the gate enforces here. SoftFileLock prints ungated (it excludes but its poll-acquire can starve # under contention), and the strict claim needs an atomic no-replace hard link SMB does not reliably provide. @@ -292,7 +305,7 @@ jobs: uv pip install . timeout 240 .venv/bin/python tasks/verify_filesystem.py /mnt/smba /mnt/smbb capability: - name: "๐Ÿงฉ without ${{ matrix.block }}" + name: "๐Ÿงช without ${{ matrix.block }}" runs-on: ubuntu-24.04 strategy: fail-fast: false @@ -301,15 +314,17 @@ jobs: - fcntl - sqlite3 steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - name: "๐Ÿ“ฅ Check out code" + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - - name: "๐Ÿ”„ Install the latest version of uv" - uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0 + - name: "๐Ÿ Install uv" + uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2 with: + version: latest-known python-version: "3.15" enable-cache: false - - name: "๐Ÿงฉ Verify the fallback without ${{ matrix.block }}" + - name: "๐Ÿงช Verify the fallback without ${{ matrix.block }}" env: FILELOCK_BLOCK_MODULE: ${{ matrix.block }} run: | @@ -319,89 +334,77 @@ jobs: .venv/bin/python tasks/capability/assert_fallback.py .venv/bin/python tasks/verify_filesystem.py symlink: - name: "๐Ÿ”— Windows symlinks ${{ matrix.developer_mode == '1' && 'granted' || 'denied' }}" + name: "๐Ÿงช Windows symlinks ${{ matrix.developer_mode == '1' && 'granted' || 'denied' }}" runs-on: windows-2025 strategy: fail-fast: false matrix: developer_mode: ["0", "1"] steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - name: "๐Ÿ“ฅ Check out code" + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - - name: "๐Ÿ”„ Install the latest version of uv" - uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0 + - name: "๐Ÿ Install uv" + uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2 with: + version: latest-known python-version: "3.15" enable-cache: false - - name: "๐Ÿงช Install tox" + - name: "๐Ÿ“ฆ Install tox" run: uv tool install --python-preference only-managed --python 3.15 tox --with tox-uv # Windows grants symlink creation two ways, so both have to be closed to deny it: Developer Mode lets an # unprivileged process create them, and SeCreateSymbolicLinkPrivilege lets any holder create them regardless. # Clearing only the registry value left the runner's privilege intact and both variants probed identical. - - name: "๐Ÿ”— Set Developer Mode to ${{ matrix.developer_mode }}" + - name: "โš™๏ธ Set Developer Mode to ${{ matrix.developer_mode }}" run: > reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\AppModelUnlock" /t REG_DWORD /f /v AllowDevelopmentWithoutDevLicense /d ${{ matrix.developer_mode }} - - name: "โš™๏ธ Setup test suite" + - name: "๐Ÿ“ฆ Set up test suite" run: tox run -vv --notest --skip-missing-interpreters false -e 3.15 - - name: "๐Ÿ”Ž Report the probed capability" + - name: "๐Ÿ“Š Report the probed capability" if: matrix.developer_mode == '1' run: | $env:PYTHONPATH = "$PWD\tasks" uv run --no-project --python 3.15 --with coverage python -c "import coverage_pragmas; print(coverage_pragmas.CAPABILITIES)" - - name: "โœ… Run test suite with symlinks granted" + - name: "๐Ÿงช Run test suite with symlinks granted" if: matrix.developer_mode == '1' run: tox run --skip-pkg-install -e 3.15 -- # deny_symlink reports the token privilege, the Developer Mode value, and whether a symlink still succeeds in # this process and in a child, so the log says which mechanism actually governs rather than assuming either. - - name: "๐Ÿ”Ž Report the probed capability" + - name: "๐Ÿ“Š Report the probed capability" if: matrix.developer_mode == '0' run: | $env:PYTHONPATH = "$PWD\tasks" uv run --no-project --python 3.15 python tasks\deny_symlink.py uv run --no-project --python 3.15 --with coverage python -c "import coverage_pragmas; print(coverage_pragmas.CAPABILITIES)" - - name: "โœ… Run test suite with symlinks denied" + - name: "๐Ÿงช Run test suite with symlinks denied" if: matrix.developer_mode == '0' run: uv run --no-project --python 3.15 python tasks\deny_symlink.py tox run --skip-pkg-install -e 3.15 -- termux: - name: "๐Ÿค– Termux (Android)" + name: "๐Ÿงช Termux (Android)" runs-on: ubuntu-24.04-arm steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - name: "๐Ÿ“ฅ Check out code" + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 persist-credentials: false - name: "๐Ÿงช Run the suite in a real Termux userland via tox" run: docker compose run --rm termux - changelog: - name: "๐Ÿ“ News fragment" - runs-on: ubuntu-24.04 - if: github.event_name == 'pull_request' && !contains(github.event.pull_request.labels.*.name, 'skip news') - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - fetch-depth: 0 - persist-credentials: false - - name: "๐Ÿ”„ Install the latest version of uv" - uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0 - with: - python-version: "3.15" - enable-cache: false - - name: "๐Ÿ“ Require a news fragment" - run: uvx towncrier check --compare-with "origin/${GITHUB_BASE_REF}" gate: - name: "โœ… Required checks pass" - if: always() - needs: [test, graalpy, coverage, check, nfs, smb, capability, symlink, termux, changelog] + name: "๐Ÿšฆ ${{ github.event_name == 'pull_request' && 'Required checks pass' || 'Non-PR checks pass' }}" + if: ${{ !cancelled() }} # codespell:ignore cancelled + needs: [test, graalpy, coverage, check, nfs, smb, capability, symlink, termux] runs-on: ubuntu-24.04 steps: - - env: + - name: "๐Ÿšฆ Check all jobs passed" + env: NEEDS_JSON: ${{ toJSON(needs) }} run: | jq -e ' all( to_entries[]; .value.result == "success" or - ((.key == "graalpy" or .key == "changelog") and .value.result == "skipped") + (.key == "graalpy" and .value.result == "skipped") ) - ' <<< "$NEEDS_JSON" + ' <<<"$NEEDS_JSON" diff --git a/.github/workflows/codeql.yaml b/.github/workflows/codeql.yaml new file mode 100644 index 00000000..6c77c7d2 --- /dev/null +++ b/.github/workflows/codeql.yaml @@ -0,0 +1,41 @@ +name: "๐Ÿ›ก๏ธ CodeQL" +on: + workflow_dispatch: + push: + branches: ["main"] + pull_request: + schedule: + - cron: "0 7 * * 2" +concurrency: + group: codeql-${{ github.ref }} + cancel-in-progress: true +permissions: {} +jobs: + analyze: + name: "๐Ÿ›ก๏ธ Analyze ${{ matrix.language }}" + runs-on: ubuntu-26.04 + permissions: + contents: read + security-events: write + strategy: + fail-fast: false + matrix: + language: [actions, python] + steps: + - name: "๐Ÿ“ฅ Check out code" + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - name: "๐Ÿ›ก๏ธ Initialize CodeQL" + uses: github/codeql-action/init@6f2ac81f5cd81fe351a197ba0b00d91d03cd2aee # v4.39.0 + with: + languages: ${{ matrix.language }} + build-mode: none + queries: security-extended + config: | + paths-ignore: + - tests + - name: "๐Ÿ›ก๏ธ Analyze" + uses: github/codeql-action/analyze@6f2ac81f5cd81fe351a197ba0b00d91d03cd2aee # v4.39.0 + with: + category: "/language:${{ matrix.language }}" diff --git a/.github/workflows/news.yaml b/.github/workflows/news.yaml new file mode 100644 index 00000000..90e5a1ec --- /dev/null +++ b/.github/workflows/news.yaml @@ -0,0 +1,32 @@ +name: "๐Ÿ“ Changelog" +on: + pull_request: + types: [opened, reopened, synchronize, labeled, unlabeled] +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number }} + cancel-in-progress: true +permissions: + contents: read +jobs: + changelog: + name: "๐Ÿ“ News fragment" + runs-on: ubuntu-26.04 + steps: + - name: "๐Ÿ“ฅ Check out code" + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + persist-credentials: false + - name: "๐Ÿ Install uv" + uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2 + with: + version: latest-known + python-version: "3.15" + enable-cache: false + - name: "๐Ÿ“ฆ Install locked release tools" + uses: tox-dev/github-workflows/install-locked-tools@259a3d3fb578d00517ffb73341fdd463259b0b2f # 6.0.1 + with: + groups: release-news + venv: ${{ runner.temp }}/release-tools + - name: "๐Ÿ” Validate news fragments" + uses: tox-dev/github-workflows/validate-news@259a3d3fb578d00517ffb73341fdd463259b0b2f # 6.0.1 diff --git a/.github/workflows/perf.yaml b/.github/workflows/perf.yaml index 8021500a..2bbe59e6 100644 --- a/.github/workflows/perf.yaml +++ b/.github/workflows/perf.yaml @@ -1,4 +1,4 @@ -name: "๐Ÿ“Š performance matrix" +name: "โฑ๏ธ performance matrix" on: workflow_dispatch: concurrency: @@ -8,7 +8,7 @@ permissions: contents: read jobs: bench: - name: "๐Ÿ“Š bench - ${{ matrix.os }}" + name: "โฑ๏ธ bench - ${{ matrix.os }}" runs-on: ${{ matrix.os }} strategy: fail-fast: false @@ -18,17 +18,19 @@ jobs: - windows-2025 - macos-15 steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - name: "๐Ÿ“ฅ Check out code" + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - - name: "๐Ÿ”„ Install the latest version of uv" - uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0 + - name: "๐Ÿ Install uv" + uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2 with: + version: latest-known python-version: "3.15" enable-cache: false - - name: "๐Ÿงช Install tox" + - name: "๐Ÿ“ฆ Install tox" run: uv tool install --python-preference only-managed --python 3.15 tox --with tox-uv - - name: "๐Ÿ“Š Measure the performance matrix" + - name: "โฑ๏ธ Measure the performance matrix" run: tox run -e bench env: UV_PYTHON_PREFERENCE: only-managed diff --git a/.github/workflows/prepare-release.yaml b/.github/workflows/prepare-release.yaml index dd89611a..0d2a2fed 100644 --- a/.github/workflows/prepare-release.yaml +++ b/.github/workflows/prepare-release.yaml @@ -1,133 +1,22 @@ -name: ๐Ÿ› ๏ธ Prepare release +name: "๐Ÿท๏ธ Prepare release" on: workflow_dispatch: inputs: bump: - description: "Version bump" + description: Version bump type required: true - default: auto type: choice options: [auto, major, minor, patch] -permissions: - contents: read + default: auto +permissions: {} jobs: - assemble: - name: ๐Ÿ“ build changelog - if: github.ref == 'refs/heads/main' - runs-on: ubuntu-24.04 - permissions: - contents: read - outputs: - version: ${{ steps.version.outputs.version }} - steps: - - name: ๐Ÿ“ฅ Checkout - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - fetch-depth: 0 # full history so the version calculation sees the latest tag - persist-credentials: false - - name: ๐Ÿ“ฆ Setup uv - uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0 - with: - python-version: "3.15" - enable-cache: false - - name: ๐Ÿ”ข Compute next version - id: version - env: - BUMP: ${{ inputs.bump }} - run: | - current=$(git tag --list --sort=-v:refname | grep -E '^[0-9]+\.[0-9]+\.[0-9]+$' | head -1) - current=${current:-0.0.0} - IFS='.' read -r major minor patch <<< "$current" - bump="$BUMP" - if [ "$bump" = "auto" ]; then - if ls docs/changelog/*.breaking.rst >/dev/null 2>&1; then - bump=major - elif ls docs/changelog/*.feature.rst >/dev/null 2>&1; then - bump=minor - else - bump=patch - fi - fi - case "$bump" in - major) next="$((major + 1)).0.0" ;; - minor) next="$major.$((minor + 1)).0" ;; - patch) next="$major.$minor.$((patch + 1))" ;; - esac - echo "Bumping $current -> $next ($bump)" - echo "version=$next" >> "$GITHUB_OUTPUT" - - name: ๐Ÿ“ Build release patch - env: - VERSION: ${{ steps.version.outputs.version }} - run: | - set -euo pipefail - if git tag --list "$VERSION" | grep -q .; then - echo "Tag $VERSION already exists, nothing to do" - exit 1 - fi - if ! compgen -G 'docs/changelog/*.*.rst' > /dev/null; then - echo "::error::no changelog fragments to release" - exit 1 - fi - uvx towncrier build --yes --version "$VERSION" - git diff HEAD --binary > "$RUNNER_TEMP/release.patch" - test -s "$RUNNER_TEMP/release.patch" - - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 - with: - name: release-patch - path: ${{ runner.temp }}/release.patch - if-no-files-found: error - push: - name: ๐Ÿ”– Commit and tag - needs: assemble - runs-on: ubuntu-24.04 - environment: release-auth + prepare: + name: "๐Ÿท๏ธ Prepare Towncrier release" + uses: tox-dev/github-workflows/.github/workflows/prepare-towncrier-release.yaml@259a3d3fb578d00517ffb73341fdd463259b0b2f # 6.0.1 permissions: contents: read - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - fetch-depth: 0 - persist-credentials: false - - uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 - with: - name: release-patch - path: ${{ runner.temp }}/release-patch - - name: Validate release patch - env: - VERSION: ${{ needs.assemble.outputs.version }} - run: | - set -euo pipefail - git fetch origin main - git reset --hard origin/main - git apply --check "$RUNNER_TEMP/release-patch/release.patch" - git apply --index "$RUNNER_TEMP/release-patch/release.patch" - while IFS= read -r path; do - case "$path" in - docs/changelog.rst|docs/changelog/*.rst) ;; - *) echo "::error::unexpected release path: $path"; exit 1 ;; - esac - done < <(git diff --cached --name-only) - grep -qE "^ ${VERSION//./\\.} \(" docs/changelog.rst - git diff --cached --check - - id: token - uses: actions/create-github-app-token@64ab729a6294000f5aa4a4bc0b73d1f32bc45510 # v3.3.0 - with: - client-id: ${{ vars.RELEASE_APP_CLIENT_ID }} - private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }} - owner: ${{ github.repository_owner }} - repositories: ${{ github.event.repository.name }} - permission-contents: write - - name: Commit and tag the release - env: - VERSION: ${{ needs.assemble.outputs.version }} - GH_TOKEN: ${{ steps.token.outputs.token }} - APP_SLUG: ${{ steps.token.outputs.app-slug }} - run: | - set -euo pipefail - app_user_id=$(gh api "/users/${APP_SLUG}[bot]" --jq .id) - git config user.name "${APP_SLUG}[bot]" - git config user.email "${app_user_id}+${APP_SLUG}[bot]@users.noreply.github.com" - git commit -m "Release $VERSION" - git tag "$VERSION" - gh auth setup-git --hostname github.com --force - git push --atomic origin HEAD:main "refs/tags/$VERSION" + pull-requests: read + with: + bump: ${{ inputs.bump }} + secrets: + RELEASE_APP_PRIVATE_KEY: ${{ secrets.RELEASE_APP_PRIVATE_KEY }} diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index ea82f917..ea59865f 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -1,11 +1,11 @@ -name: ๐Ÿš€ Release +name: "๐Ÿš€ Release to PyPI" on: push: branches: ["main"] - tags: ["[0-9]+.[0-9]+.[0-9]+"] + tags: ["*"] pull_request: concurrency: - group: ${{ github.workflow }}-${{ github.ref }} + group: release-${{ github.ref }} cancel-in-progress: ${{ !startsWith(github.ref, 'refs/tags/') }} permissions: contents: read @@ -13,107 +13,151 @@ env: dists-artifact-name: python-package-distributions jobs: build: - name: ๐Ÿ“ฆ build - runs-on: ubuntu-24.04 + name: "๐Ÿ—๏ธ Build distributions" + if: ${{ !startsWith(github.ref, 'refs/tags/') }} + runs-on: ubuntu-26.04 permissions: contents: read steps: - - name: ๐Ÿ“ฅ Checkout + - name: "๐Ÿ“ฅ Check out code" uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: - fetch-depth: 0 # full history so hatch-vcs derives the version from the tag + fetch-depth: 0 persist-credentials: false - - name: ๐Ÿ“ฆ Setup uv - uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0 + - name: "๐Ÿ Install uv" + uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2 with: + version: latest-known python-version: "3.15" - enable-cache: false # this workflow builds the artifact that gets published; no cache to poison - - name: ๐Ÿ“ฆ Build package - run: uv build --python 3.15 --python-preference only-managed --sdist --wheel . --out-dir dist - - name: ๐Ÿ“ค Store the distribution packages + enable-cache: false + - name: "๐Ÿ—๏ธ Build distributions" + uses: tox-dev/github-workflows/build-python-distribution@259a3d3fb578d00517ffb73341fdd463259b0b2f # 6.0.1 + - name: "๐Ÿ“ค Upload distributions" uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: ${{ env.dists-artifact-name }} path: dist/* - publish: - name: ๐Ÿš€ publish - needs: [build] + if-no-files-found: error + tag-build: + name: ๐Ÿ—๏ธ Build + if: startsWith(github.ref, 'refs/tags/') + uses: tox-dev/github-workflows/.github/workflows/validate-python-release.yaml@259a3d3fb578d00517ffb73341fdd463259b0b2f # 6.0.1 + permissions: + contents: read + github-notes: + name: "๐Ÿ“ Render GitHub Release notes" if: startsWith(github.ref, 'refs/tags/') - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 + permissions: + contents: read + outputs: + artifact-id: ${{ steps.upload.outputs.artifact-id }} + steps: + - name: "๐Ÿ“ฅ Check out code" + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + persist-credentials: false + - name: "๐Ÿ Install uv" + uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2 + with: + version: latest-known + python-version: "3.15" + enable-cache: false + - name: "๐Ÿ“ฆ Install locked release tools" + uses: tox-dev/github-workflows/install-locked-tools@259a3d3fb578d00517ffb73341fdd463259b0b2f # 6.0.1 + with: + groups: release-notes + venv: ${{ runner.temp }}/release-tools + - name: "๐Ÿท๏ธ Select previous tag" + id: previous + uses: tox-dev/github-workflows/select-previous-tag@259a3d3fb578d00517ffb73341fdd463259b0b2f # 6.0.1 + with: + tag: ${{ github.ref_name }} + - name: "๐Ÿ“ Render release notes" + uses: tox-dev/github-workflows/render-release-notes@259a3d3fb578d00517ffb73341fdd463259b0b2f # 6.0.1 + with: + tag: ${{ github.ref_name }} + previous: ${{ steps.previous.outputs.previous }} + changelog: docs/changelog.rst + artifact-url: https://pypi.org/project/filelock/${{ github.ref_name }}/ + - name: "๐Ÿ“ค Upload release notes" + id: upload + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 + with: + name: github-release-notes + path: ${{ runner.temp }}/release-notes.md + if-no-files-found: error + publish: + name: "๐Ÿš€ Publish to PyPI" + needs: + - tag-build + - github-notes + runs-on: ubuntu-26.04 environment: name: release url: https://pypi.org/project/filelock/${{ github.ref_name }} permissions: id-token: write steps: - - name: ๐Ÿ“ฅ Checkout + - name: "๐Ÿ“ฉ Download distributions" + uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 + with: + artifact-ids: ${{ needs.tag-build.outputs.distributions-artifact-id }} + path: dist/ + - name: "๐Ÿš€ Publish to PyPI" + uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2 + with: + packages-dir: dist + verify-pypi: + name: "๐Ÿ” Verify PyPI distributions" + needs: + - tag-build + - publish + runs-on: ubuntu-26.04 + permissions: + contents: read + steps: + - name: "๐Ÿ“ฅ Check out code" uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - - name: ๐Ÿ›ก๏ธ Require a changelog section for this release - env: - VERSION: ${{ github.ref_name }} - # A tag reaches PyPI only through prepare-release, which builds the changelog first. A tag pushed by hand skips - # that, so refuse to publish a version the changelog does not document rather than ship it silently. - run: | - if ! grep -qE "^ ${VERSION//./\\.} \(" docs/changelog.rst; then - echo "::error::docs/changelog.rst has no section for $VERSION; release via the Prepare release workflow" - exit 1 - fi - - name: โฌ‡๏ธ Download all the dists + - name: "๐Ÿ Install uv" + uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2 + with: + version: latest-known + python-version: "3.15" + enable-cache: false + - name: "๐Ÿ“ฉ Download distributions" uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 with: - name: ${{ env.dists-artifact-name }} + artifact-ids: ${{ needs.tag-build.outputs.distributions-artifact-id }} path: dist/ - - name: ๐Ÿš€ Publish to PyPI - uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2 + - name: "๐Ÿ” Verify PyPI artifacts" + uses: tox-dev/github-workflows/reconcile-pypi-artifacts@259a3d3fb578d00517ffb73341fdd463259b0b2f # 6.0.1 with: - skip-existing: true + project: filelock + version: ${{ github.ref_name }} + dist: dist github-release: - needs: publish + name: "๐Ÿš€ Publish GitHub Release" + needs: [verify-pypi, github-notes] if: startsWith(github.ref, 'refs/tags/') - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 permissions: contents: write steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - name: "๐Ÿ“ฉ Download release notes" + uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 with: - fetch-depth: 0 - persist-credentials: false - - name: ๐Ÿ“ข Create GitHub release + artifact-ids: ${{ needs.github-notes.outputs.artifact-id }} + path: ${{ runner.temp }} + - name: "๐Ÿš€ Publish GitHub release" + uses: tox-dev/github-workflows/publish-github-release@259a3d3fb578d00517ffb73341fdd463259b0b2f # 6.0.1 env: GH_TOKEN: ${{ github.token }} - VERSION: ${{ github.ref_name }} - run: | - set -euo pipefail - awk -v version="$VERSION" ' - index($0, " " version " (") == 1 { found = 1; next } - found && /^ [0-9]+\.[0-9]+\.[0-9]+ \(/ { exit } - found && $0 !~ /^\*+$/ { print } - ' docs/changelog.rst > "$RUNNER_TEMP/release-notes.md" - python3 - "$RUNNER_TEMP/release-notes.md" <<'PY' - import re - import sys - from pathlib import Path - - path = Path(sys.argv[1]) - body = path.read_text().strip() - body = re.sub(r":user:`([^`]+)`", r"\1", body) - body = re.sub(r":(?:pr|issue):`(\d+)`", r"#\1", body) - body = re.sub(r":[a-z]+:`~?([^`]+)`", lambda match: f"`{match.group(1).split('.')[-1]}`", body) - if not body: - raise SystemExit("release notes are empty") - path.write_text(body.replace("``", "`") + "\n") - PY - printf '\n[PyPI release](https://pypi.org/project/filelock/%s/)\n' "$VERSION" >> "$RUNNER_TEMP/release-notes.md" - previous=$({ gh api "repos/$GITHUB_REPOSITORY/releases?per_page=100" --paginate --jq '.[].tag_name'; printf '%s\n' "$VERSION"; } | grep -E '^[0-9]+\.[0-9]+\.[0-9]+$' | sort -Vu | awk -v tag="$VERSION" '$0 == tag { print previous; exit } { previous = $0 }') - if [[ -n "$previous" ]]; then - printf '\n**Full Changelog**: https://github.com/%s/compare/%s...%s\n' "$GITHUB_REPOSITORY" "$previous" "$VERSION" >> "$RUNNER_TEMP/release-notes.md" - else - printf '\n**Full Changelog**: https://github.com/%s/commits/%s\n' "$GITHUB_REPOSITORY" "$VERSION" >> "$RUNNER_TEMP/release-notes.md" - fi - if gh release view "$VERSION" --repo "$GITHUB_REPOSITORY" > /dev/null 2>&1; then - gh release edit "$VERSION" --repo "$GITHUB_REPOSITORY" --notes-file "$RUNNER_TEMP/release-notes.md" - else - gh release create "$VERSION" --repo "$GITHUB_REPOSITORY" --title="$VERSION" --verify-tag --notes-file "$RUNNER_TEMP/release-notes.md" - fi + with: + tag: ${{ github.ref_name }} + commit: ${{ github.sha }} + title: ${{ github.ref_name }} + notes-file: ${{ runner.temp }}/release-notes.md diff --git a/.github/workflows/upgrade-release-tools.yaml b/.github/workflows/upgrade-release-tools.yaml new file mode 100644 index 00000000..7dff94c1 --- /dev/null +++ b/.github/workflows/upgrade-release-tools.yaml @@ -0,0 +1,22 @@ +name: "๐Ÿ”’ Upgrade release tools" +on: + workflow_dispatch: + schedule: + - cron: "0 8 * * 2" +permissions: {} +concurrency: + group: upgrade-release-tools + cancel-in-progress: false +jobs: + upgrade: + name: "๐Ÿ”€ Upgrade release tools" + uses: tox-dev/github-workflows/.github/workflows/upgrade-release-tools.yaml@259a3d3fb578d00517ffb73341fdd463259b0b2f # 6.0.1 + permissions: + contents: read + secrets: + RELEASE_APP_PRIVATE_KEY: ${{ secrets.RELEASE_APP_PRIVATE_KEY }} + with: + commit-message: "build(deps): refresh project lock" + pull-request-title: "build(deps): refresh project lock" + pull-request-body: |- + Relock `pylock.toml` after the seven-day cooldown. diff --git a/.gitignore b/.gitignore index f55bbb25..75fed579 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,4 @@ venv* test.lock .coverage.* test.softlock +/uv.lock diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c3a6419f..782a3060 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,56 +1,64 @@ +ci: + autoupdate_schedule: quarterly default_language_version: - python: python3 + python: python3.15 repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v6.0.0 + rev: 3e8a8703264a2f4a69428a0aa4dcb512790b2c8c # frozen: v6.0.0 hooks: - id: end-of-file-fixer - id: trailing-whitespace - repo: https://github.com/python-jsonschema/check-jsonschema - rev: 0.38.2 + rev: 0494aa7fa1314e98913a41227d8f0a515b2c1049 # frozen: 0.38.2 hooks: - id: check-github-workflows args: ["--verbose"] - repo: https://github.com/codespell-project/codespell - rev: v2.4.3 + rev: 57b21406f092110c18776e39b0bda50d37c945c8 # frozen: v2.4.3 hooks: - id: codespell - repo: https://github.com/tox-dev/tox-toml-fmt - rev: "v1.11.1" + rev: "5125f601c3e134347288c3d42f3b34298b7cf1be" # frozen: v1.11.1 hooks: - id: tox-toml-fmt - repo: https://github.com/tox-dev/pyproject-fmt - rev: "v2.30.3" + rev: "50e0f3aee9dc5747a5f173f79a7417403c18dda6" # frozen: v2.30.3 hooks: - id: pyproject-fmt - repo: https://github.com/astral-sh/ruff-pre-commit - rev: "v0.17.1" + rev: "38bfb07c0c64c304f47eb52090f8288150c77730" # frozen: v0.17.1 hooks: - id: ruff-format - id: ruff-check args: ["--fix", "--unsafe-fixes", "--exit-non-zero-on-fix"] - repo: https://github.com/henryiii/flake8-lazy - rev: v0.10.1 + rev: 2db69f40494cc912ede966160695ede82eb6ebac # frozen: v0.10.1 hooks: - id: flake8-lazy args: ["--apply=list"] files: ^src/ - repo: https://github.com/google/yamlfmt - rev: "v0.21.0" + rev: "21ca5323a9c87ee37a434e0ca908efc0a89daa07" # frozen: v0.21.0 hooks: - id: yamlfmt - - repo: local + - repo: https://github.com/executablebooks/mdformat + rev: 82912cdaea4fb830f751504486a7879c70526547 # frozen: 1.0.0 hooks: - id: mdformat - name: mdformat - entry: mdformat --wrap 120 - language: python - types: [markdown] + args: ["--wrap", "120"] additional_dependencies: - - mdformat-gfm>=1 - - mdformat-ruff>=0.1.3 + - mdformat-gfm==1.0.0 + - mdformat-ruff==0.1.3 + - repo: https://github.com/LilSpazJoekp/docstrfmt + rev: c1813841673cdcd6cf602dde1edb78a5d5e45235 # frozen: v2.2.1 + hooks: + - id: docstrfmt + files: ^docs/changelog[.]rst$ + args: ["-l", "120"] + additional_dependencies: + - "sphinx==9.1.0" - repo: https://github.com/zizmorcore/zizmor-pre-commit - rev: v1.31.1 + rev: c92afdf2c6ca336aaa8f39e2bd8d48f62dbd8e1f # frozen: v1.31.1 hooks: - id: zizmor - repo: meta diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0eee137b..0684b627 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -24,6 +24,35 @@ This page lists the steps needed to set up a development environment and contrib tox -e fix ``` +## Release notes + +Add a `docs/changelog/+short-name..rst` fragment for each change that affects library users. The name describes +the change; it does not need an issue or pull request number. Leave out credits and pull request links: the news check +rejects a fragment that carries either. Pending fragments appear under Unreleased in documentation builds. They enter +`docs/changelog.rst` only when a release is prepared. + +| Type | Section | Use for | +| ------------- | ---------------- | ---------------------------------------------------------- | +| `feature` | Added | New capabilities and Python support additions or removals | +| `change` | Changes | Changed behavior that remains available | +| `bugfix` | Fixed | Corrected behavior | +| `doc` | Documentation | Documentation changes users need to know about | +| `breaking` | Breaking changes | Changes that require users to update code or configuration | +| `security` | Security | Security fixes users need to act on | +| `performance` | Performance | User-visible speed or resource improvements | +| `deprecation` | Deprecated | Supported behavior scheduled for removal | +| `removal` | Removed | Removed user-facing capabilities other than Python support | +| `packaging` | Packaging | Install, wheel, or distribution changes | + +Apply `skip news` to a pull request with no user-facing change, including maintenance work. The label may be applied +after the pull request opens. Dependabot, pre-commit.ci, and repository automation receive it when they supply no +fragment. A bot pull request gets no release entry unless it supplies one. + +Credits come from the commits. When a release is prepared, each fragment is linked to its squash-merged pull request and +credited to the pull request author, then every other author and `Co-authored-by` co-author of the merge commit. Bots +are credited only when no human author remains. After the changelog is built, prepare runs +`release-creator contributors`, which adds a New Contributors subsection for first-time pull request authors. + ## Coverage Every environment in the matrix must reach 100%, and so must the combined report. Name the capability a line needs diff --git a/docs/changelog.rst b/docs/changelog.rst index 3384d5f2..9a3a1c6e 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -6,1322 +6,2510 @@ .. towncrier release notes start -******************* +.. _release-4-2-4: + +******************** 4.2.4 (2026-10-19) -******************* +******************** -- Keep GitHub Release notes free of extra blank lines while retaining the PyPI and version comparison links. :pr:`863` +.. _release-4-2-3: -******************* +******************** 4.2.3 (2026-10-19) -******************* +******************** + +.. _release-4-2-3-fixed: + +Fixed +===== - Creating a lock on a Python 3.15 interpreter without ``types.LazyImportType`` no longer triggers ``AttributeError`` at - exit. :pr:`860` + exit. :pr:`861` - by :user:`gaborbernat`. - Handle ``EFTYPE`` when a no-follow open refuses a symlink in a soft reader/writer lock's protocol tree, so heartbeat - reports lost ownership rather than a transient refresh error. + reports lost ownership rather than a transient refresh error. :pr:`859` - by :user:`edsonsims`. + +.. _release-4-2-2: -******************* +******************** 4.2.2 (2026-10-18) -******************* +******************** + +.. _release-4-2-2-fixed: + +Fixed +===== - Fix a lock on a relative path that stayed held after a later ``with lock:`` ended, when an earlier ``acquire()`` had failed because the working directory no longer exists. That call still raises :class:`FileNotFoundError`, and it now - leaves the lock's hold count unchanged. :pr:`856` + leaves the lock's hold count unchanged. :pr:`856` - by :user:`spicysharkco`. -******************* +.. _release-4-2-1: + +******************** 4.2.1 (2026-10-17) -******************* +******************** + +.. _release-4-2-1-fixed: + +Fixed +===== - On a singleton wrapper, :meth:`AsyncReadWriteLock.close() ` and :meth:`AsyncSoftReadWriteLock.close() ` end only the holds that tasks acquired - through that wrapper. A reader that arrives during ``close()`` gets the closed error instead of joining the ending hold. - After synchronous code closes or force-releases the shared lock, the wrappers refuse to admit readers into the ended - hold, and ``close()`` on such a wrapper returns. - Closing one wrapper no longer ends a task's nested hold taken through another wrapper on the same path. :pr:`832` -- :class:`~filelock.ReadWriteLock` locks again after a ``release()`` that an exception from a signal handler interrupted. - The async read-write wrappers stop admitting readers into such a level. The async read-write locks forget a hold whose - backend release failed after the backend let go, and they free the memory of each acquire that times out or is canceled. - :class:`~filelock.AsyncFileLock` returns from ``release()`` after a few hundred acquires gave up behind a waiting one. An exception from a signal handler delivered while an :class:`~filelock.AsyncFileLock` call joins - its queue no longer blocks every later call on that lock. :pr:`833` -- Fix :class:`~filelock.ReadWriteLock` acting on ``-journal``, ``-wal`` or ``-shm`` files another process puts beside its - database: a write could land in another file, a reader could get in beside a writer, and an acquire could fail. SQLite - now opens the database by name only in a directory no other account can write, and through a private hard link + through that wrapper. A reader that arrives during ``close()`` gets the closed error instead of joining the ending + hold. After synchronous code closes or force-releases the shared lock, the wrappers refuse to admit readers into the + ended hold, and ``close()`` on such a wrapper returns. Closing one wrapper no longer ends a task's nested hold taken + through another wrapper on the same path. :pr:`832` - by :user:`gaborbernat`. +- :class:`~filelock.ReadWriteLock` locks again after a ``release()`` that an exception from a signal handler + interrupted. The async read-write wrappers stop admitting readers into such a level. The async read-write locks forget + a hold whose backend release failed after the backend let go, and they free the memory of each acquire that times out + or is canceled. :class:`~filelock.AsyncFileLock` returns from ``release()`` after a few hundred acquires gave up + behind a waiting one. An exception from a signal handler delivered while an :class:`~filelock.AsyncFileLock` call + joins its queue no longer blocks every later call on that lock. :pr:`833` - by :user:`gaborbernat`. +- Fix :class:`~filelock.ReadWriteLock` acting on ``-journal``, ``-wal`` or ``-shm`` files another process puts beside + its database: a write could land in another file, a reader could get in beside a writer, and an acquire could fail. + SQLite now opens the database by name only in a directory no other account can write, and through a private hard link elsewhere. On Windows the directory's access control list decides. Where hard links are unavailable, keep the database in a directory other accounts cannot write, such as one under ``%LOCALAPPDATA%``. The lock removes link directories through a checked descriptor rather than by name, reads a link directory's name only when the locking account owns it, - and leaves alone a directory with other content renamed onto a link directory's name. A database whose schema takes more - than a few kilobytes to load raises :class:`ValueError` instead of running past the acquire timeout. :pr:`834` + and leaves alone a directory with other content renamed onto a link directory's name. A database whose schema takes + more than a few kilobytes to load raises :class:`ValueError` instead of running past the acquire timeout. :pr:`834` - + by :user:`gaborbernat`. - Fix five bugs in opening the lock file. :class:`~filelock.UnixFileLock` no longer truncates a file it did not create, such as one renamed to the lock path, and no longer applies ``mode`` to it. An ``on_acquired`` hook that writes holder - metadata now truncates the file itself. A lease (Linux) or an oplock (Windows) on the lock file or a soft lock's marker - no longer holds ``acquire()`` past its ``timeout`` or ``blocking=False``, and an oplock no longer holds ``release()``; - :class:`~filelock.ReadWriteLock` on Windows waits for its ``timeout`` and raises :class:`~filelock.Timeout`. On Windows, - a ``mode`` without group and other bits, such as ``0o600``, now creates the lock file with an owner-only permission - list; the :class:`~filelock.ReadWriteLock` database gets the same. The soft locks' writability check no longer follows a - symlink swapped in at the lock path. On a filesystem without ``flock``, a contender from another account now waits on a - marker it cannot write instead of raising ``PermissionError``. :pr:`835` -- Fix :class:`~filelock.SoftFileLock`, :class:`~filelock.MarkerSoftFileLock` and :class:`~filelock.SoftFileLease` removing - a live holder's marker in three cases: the holder and the contender run in Linux time namespaces with different boottime - offsets; only one of them can read the boot id; a process other than the owner rewrote the marker. Soft markers no - longer carry group or other write bits. A process name containing a carriage return or another line separator no longer - makes the stale check raise :class:`ValueError`. When ``/proc/self/ns/time`` exists but cannot be read, the stale check - no longer compares start times and keeps the marker. :pr:`836` + metadata now truncates the file itself. A lease (Linux) or an oplock (Windows) on the lock file or a soft lock's + marker no longer holds ``acquire()`` past its ``timeout`` or ``blocking=False``, and an oplock no longer holds + ``release()``; :class:`~filelock.ReadWriteLock` on Windows waits for its ``timeout`` and raises + :class:`~filelock.Timeout`. On Windows, a ``mode`` without group and other bits, such as ``0o600``, now creates the + lock file with an owner-only permission list; the :class:`~filelock.ReadWriteLock` database gets the same. The soft + locks' writability check no longer follows a symlink swapped in at the lock path. On a filesystem without ``flock``, a + contender from another account now waits on a marker it cannot write instead of raising ``PermissionError``. :pr:`835` + - by :user:`gaborbernat`. +- Fix :class:`~filelock.SoftFileLock`, :class:`~filelock.MarkerSoftFileLock` and :class:`~filelock.SoftFileLease` + removing a live holder's marker in three cases: the holder and the contender run in Linux time namespaces with + different boottime offsets; only one of them can read the boot id; a process other than the owner rewrote the marker. + Soft markers no longer carry group or other write bits. A process name containing a carriage return or another line + separator no longer makes the stale check raise :class:`ValueError`. When ``/proc/self/ns/time`` exists but cannot be + read, the stale check no longer compares start times and keeps the marker. :pr:`836` - by :user:`gaborbernat`. - The async read-write locks record their hold under a task owner, so synchronous code on the executor thread that took the hold can no longer re-enter or release it. :class:`~filelock.AsyncSoftReadWriteLock` gives back one level on release, so synchronous readers of a shared singleton keep theirs. :class:`~filelock.SoftReadWriteLock` tracks read levels per thread: ``release()`` from a thread that holds none raises :class:`RuntimeError`. After its heartbeat - recorded a lost hold, it refuses new acquires until that hold is released. :pr:`838` -- :class:`~filelock.StrictSoftFileLock` creates ``.filelock`` and its ``claims`` directory with the lock's ``mode`` - and sets the sticky bit (a directory flag that lets only a file's owner remove it) on both. On the next acquisition it - narrows a directory it owns that an earlier release created wider; it leaves a directory of another account as it is, - so remove such leftovers by hand. It reaches ``claims`` through a descriptor opened without following symlinks; on - Windows it holds both directories open and refuses a junction (a directory that points at another directory). Under a - sticky parent such as ``/tmp`` it refuses a coordination directory of another account, as + recorded a lost hold, it refuses new acquires until that hold is released. :pr:`838` - by :user:`gaborbernat`. +- :class:`~filelock.StrictSoftFileLock` creates ``.filelock`` and its ``claims`` directory with the lock's + ``mode`` and sets the sticky bit (a directory flag that lets only a file's owner remove it) on both. On the next + acquisition it narrows a directory it owns that an earlier release created wider; it leaves a directory of another + account as it is, so remove such leftovers by hand. It reaches ``claims`` through a descriptor opened without + following symlinks; on Windows it holds both directories open and refuses a junction (a directory that points at + another directory). Under a sticky parent such as ``/tmp`` it refuses a coordination directory of another account, as :class:`~filelock.SoftReadWriteLock` does for ``.rw``. One read grace now covers a whole claim scan, a failed claim cleanup no longer leaves the sentinel descriptor open, and a non-regular node at a sentinel private-record name raises :class:`~filelock.SoftFileLockProtocolError`. On Windows, contenders read claims without blocking their owner's - unlink. :pr:`839` + unlink. :pr:`839` - by :user:`gaborbernat`. - Fix a lock reporting a hold the OS had dropped after a signal handler raised during :meth:`~filelock.BaseFileLock.acquire` or :meth:`~filelock.BaseFileLock.release`. A release that a signal handler cuts - short after it has started now completes before the error propagates. A handler that acquires the lock its own thread is - releasing now gets :class:`RuntimeError` instead of a hold that stays held. The singleton cache no longer hands out an - ``is_singleton=True`` lock or a :class:`~filelock.ReadWriteLock` whose finalizer is running, which released the new + short after it has started now completes before the error propagates. A handler that acquires the lock its own thread + is releasing now gets :class:`RuntimeError` instead of a hold that stays held. The singleton cache no longer hands out + an ``is_singleton=True`` lock or a :class:`~filelock.ReadWriteLock` whose finalizer is running, which released the new holder's lock. A signal during ``os.fork()`` no longer leaves the other threads blocked in their next lock call, and a fork from another thread no longer waits for a :class:`~filelock.ReadWriteLock` acquire on a busy database. :pr:`840` -- Fix a lock on a relative path locking a file in the directory the process changed to while the lock waited, and leaving - its marker behind on release after such a change. The path resolved when an acquisition starts stays in use until - the hold ends. An ``is_singleton=True`` lookup no longer returns a cached lock whose relative path now resolves to - another file; a held lock still matches the file it holds. A rename of the lock's directory during a hold no longer + - by :user:`gaborbernat`. +- Fix a lock on a relative path locking a file in the directory the process changed to while the lock waited, and + leaving its marker behind on release after such a change. The path resolved when an acquisition starts stays in use + until the hold ends. An ``is_singleton=True`` lookup no longer returns a cached lock whose relative path now resolves + to another file; a held lock still matches the file it holds. A rename of the lock's directory during a hold no longer leaves the marker behind or reports a false lease compromise, and a waiter no longer creates a second lock file in a - directory that was renamed away. :pr:`841` -- :class:`~filelock.SoftReadWriteLock` reaches its records through descriptors of ``.rw`` and ``gen`` or ``holders`` - opened without following symlinks. On Windows it holds those directories open and refuses a junction (a directory that - points at another directory). A tree swapped in after an earlier acquire therefore cannot redirect them. Its heartbeat - rewrites a holder record only when the name still leads to the file it created, and does not block on a FIFO (a named - pipe). A heartbeat that finds ``.rw`` replaced by a symlink or junction reports the hold lost instead of ending - the heartbeat thread. A live lock object repeats the ownership check whenever the tree it opens is not the one it - prepared. :pr:`842` -- Fix :class:`~filelock.SoftFileLock`, :class:`~filelock.MarkerSoftFileLock` and :class:`~filelock.SoftFileLease` removing - or displacing a live holder's marker in five cases. ext4 gave a freed inode number to the next marker. A stalled - :class:`~filelock.FileLock` removed its ``ENOSYS`` placeholder after a peer replaced it. A failed acquire cleaned up - after a successor took the path. A stale break left the lock path empty while it checked the marker it had moved. A - ``lifetime`` break judged a marker by a reused inode number. On Linux and macOS a stale marker now changes hands in one - atomic exchange. On other platforms without one, except Windows, including NFS and SMB, the marker stays in place and - waits for ``timeout`` or ``break_lock()``. Restoring a marker no longer follows a symlink found under the break name. A - holder that releases while a peer's exchange has its marker aside leaves a record no lock parses, so the put-back lands - a marker that ages out. A lease heartbeat that finds another file at the lock path once, while a peer's break has its - marker aside, waits for the next refresh before it reports a compromise. A contender that finds a dead breaker's marker - puts back the marker that breaker left under its break name. A marker is the breaker's own only when it names the - breaker's process on this host; the same process number on another host is a holder. A contender takes a stranded marker - under a name of its own before it puts the marker back, so two contenders no longer reverse each other's exchange and - remove the live holder's marker. A claim left behind by a failed put-back is retried on the next poll. :pr:`843` + directory that was renamed away. :pr:`841` - by :user:`gaborbernat`. +- :class:`~filelock.SoftReadWriteLock` reaches its records through descriptors of ``.rw`` and ``gen`` or + ``holders`` opened without following symlinks. On Windows it holds those directories open and refuses a junction (a + directory that points at another directory). A tree swapped in after an earlier acquire therefore cannot redirect + them. Its heartbeat rewrites a holder record only when the name still leads to the file it created, and does not block + on a FIFO (a named pipe). A heartbeat that finds ``.rw`` replaced by a symlink or junction reports the hold lost + instead of ending the heartbeat thread. A live lock object repeats the ownership check whenever the tree it opens is + not the one it prepared. :pr:`842` - by :user:`gaborbernat`. +- Fix :class:`~filelock.SoftFileLock`, :class:`~filelock.MarkerSoftFileLock` and :class:`~filelock.SoftFileLease` + removing or displacing a live holder's marker in five cases. ext4 gave a freed inode number to the next marker. A + stalled :class:`~filelock.FileLock` removed its ``ENOSYS`` placeholder after a peer replaced it. A failed acquire + cleaned up after a successor took the path. A stale break left the lock path empty while it checked the marker it had + moved. A ``lifetime`` break judged a marker by a reused inode number. On Linux and macOS a stale marker now changes + hands in one atomic exchange. On other platforms without one, except Windows, including NFS and SMB, the marker stays + in place and waits for ``timeout`` or ``break_lock()``. Restoring a marker no longer follows a symlink found under the + break name. A holder that releases while a peer's exchange has its marker aside leaves a record no lock parses, so the + put-back lands a marker that ages out. A lease heartbeat that finds another file at the lock path once, while a peer's + break has its marker aside, waits for the next refresh before it reports a compromise. A contender that finds a dead + breaker's marker puts back the marker that breaker left under its break name. A marker is the breaker's own only when + it names the breaker's process on this host; the same process number on another host is a holder. A contender takes a + stranded marker under a name of its own before it puts the marker back, so two contenders no longer reverse each + other's exchange and remove the live holder's marker. A claim left behind by a failed put-back is retried on the next + poll. :pr:`843` - by :user:`gaborbernat`. - Canceling an :class:`~filelock.AsyncReadWriteLock` or :class:`~filelock.AsyncSoftReadWriteLock` acquire that waits for another process now stops the wait and raises the cancellation. It used to wait until that process released, or raise :class:`~filelock.Timeout`. A ``thread_local=True`` :class:`~filelock.AsyncFileLock` that threads with their own event - loops share no longer deadlocks when one thread waits for the lock while another holds it. :pr:`844` + loops share no longer deadlocks when one thread waits for the lock while another holds it. :pr:`844` - by + :user:`gaborbernat`. - :class:`~filelock.SoftReadWriteLock` refuses a commit that lands a whole retention window behind the log, so two - committers stalled at adjacent generations can no longer admit a second writer. Its stale check samples the clock before - it reads a holder record, so a pause between the two no longer counts as time the record stayed unchanged. :pr:`845` + committers stalled at adjacent generations can no longer admit a second writer. Its stale check samples the clock + before it reads a holder record, so a pause between the two no longer counts as time the record stayed unchanged. + :pr:`845` - by :user:`gaborbernat`. - Fix a :class:`~filelock.SoftFileLock` contender evicting an empty marker after two seconds while its owner held the - lock. The owner is a ``SoftFileLock`` before 3.22, which writes nothing into its marker, or a current one still writing - its record. An empty marker now reads as held, and ``timeout``, ``lifetime`` or ``break_lock()`` clears it. A contender - that finds an empty marker a dead breaker left aside puts it back instead of removing it. :pr:`846` -- Fix :class:`~filelock.ReadWriteLock` on Windows acting on ``-journal`` and ``-wal`` files another account places beside - its database: an acquire could fail with :class:`sqlite3.OperationalError`, or SQLite could replay the journal and - delete the file it names. The lock now reads the database directory's access control list. Where an account other than - the locking account, ``SYSTEM`` and ``Administrators`` can add or remove names, SQLite opens a hard link inside a - directory only those accounts can write. A filesystem without hard links raises :class:`OSError`. :pr:`854` -- Fix ``SoftReadWriteLock`` subclasses sharing singleton instances with the base class or sibling subclasses. - Each concrete class now keeps its own cache and construction guard, including after fork, while preserving - cooperative class-definition keywords. :pr:`855` -- GraalPy is now tested weekly instead of on every change: GraalPy 25.4.4 can freeze the test suite, which made its job - fail on unrelated changes. :pr:`849` - -******************* + lock. The owner is a ``SoftFileLock`` before 3.22, which writes nothing into its marker, or a current one still + writing its record. An empty marker now reads as held, and ``timeout``, ``lifetime`` or ``break_lock()`` clears it. A + contender that finds an empty marker a dead breaker left aside puts it back instead of removing it. :pr:`846` - by + :user:`gaborbernat`. +- Fix :class:`~filelock.ReadWriteLock` on Windows acting on ``-journal`` and ``-wal`` files another account places + beside its database: an acquire could fail with :class:`sqlite3.OperationalError`, or SQLite could replay the journal + and delete the file it names. The lock now reads the database directory's access control list. Where an account other + than the locking account, ``SYSTEM`` and ``Administrators`` can add or remove names, SQLite opens a hard link inside a + directory only those accounts can write. A filesystem without hard links raises :class:`OSError`. :pr:`854` - by + :user:`gaborbernat`. +- Fix ``SoftReadWriteLock`` subclasses sharing singleton instances with the base class or sibling subclasses. Each + concrete class now keeps its own cache and construction guard, including after fork, while preserving cooperative + class-definition keywords. :pr:`855` - by :user:`AryanRG`. + +.. _release-4-2-0: + +******************** 4.2.0 (2026-10-14) -******************* +******************** + +.. _release-4-2-0-fixed: + +Fixed +===== -- ``import filelock`` is about twice as fast: lock types other than :class:`~filelock.FileLock` load on first use. :pr:`831` - Fix :class:`~filelock.StrictSoftFileLock` raising :class:`~filelock.SoftFileLockProtocolError` on Windows when a - contender read a claim whose owner was still finishing its unlink. :pr:`829` + contender read a claim whose owner was still finishing its unlink. :pr:`829` - by :user:`gaborbernat`. -******************* +.. _release-4-2-0-performance: + +Performance +=========== + +- ``import filelock`` is about twice as fast: lock types other than :class:`~filelock.FileLock` load on first use. + :pr:`831` - by :user:`gaborbernat`. + +.. _release-4-1-2: + +******************** 4.1.2 (2026-10-14) -******************* +******************** + +.. _release-4-1-2-fixed: -- Fix read-lock acquisition on SQLite versions older than 3.33.0 by using ``sqlite_master`` for the schema probe. :pr:`770` +Fixed +===== + +- Fix read-lock acquisition on SQLite versions older than 3.33.0 by using ``sqlite_master`` for the schema probe. + :pr:`827` - by :user:`DawnofGenX`. - Raise ``IsADirectoryError`` (or ``PermissionError`` on Windows) for a directory lock path regardless of its write - permissions. :pr:`822` -- Fix a forked child hanging at exit on PyPy when the parent forked while an - :class:`~filelock.AsyncReadWriteLock` worker thread was between two operations. :pr:`828` + permissions. :pr:`822` - by :user:`feiiiiii5`. +- Fix a forked child hanging at exit on PyPy when the parent forked while an :class:`~filelock.AsyncReadWriteLock` + worker thread was between two operations. :pr:`828` - by :user:`gaborbernat`. - Hash all cache keys in the lock filename recipe so short path-like keys stay in the chosen directory. The example also - accepts path-derived surrogate escapes and works before the cache directory exists. :pr:`824` + accepts path-derived surrogate escapes and works before the cache directory exists. :pr:`824` - by :user:`xiehuanyi`. + +.. _release-4-1-1: -******************* +******************** 4.1.1 (2026-10-11) -******************* +******************** + +.. _release-4-1-1-fixed: + +Fixed +===== + +- Avoid the macOS ``/dev/fd`` lookup implicated in a kernel panic by opening SQLite through a validated hard link. The + database directory must be writable and support hard links; alias creation failure raises ``OSError``. :pr:`820` - by + :user:`gaborbernat`. +- Ignore non-ASCII PID or start-time fields and out-of-range PIDs in ``ReadWriteLock`` hard-link directory names so + forged owners cannot abort acquisition. :pr:`821` - by :user:`dxbjavid`. -- Bound the shared-lock contention test by worker progress so slow CI machines can finish the workload. :pr:`819` -- Avoid the macOS ``/dev/fd`` lookup implicated in a kernel panic by opening SQLite through a validated hard link. - The database directory must be writable and support hard links; alias creation failure raises ``OSError``. :pr:`820` -- Ignore non-ASCII PID or start-time fields and out-of-range PIDs in ``ReadWriteLock`` hard-link directory names so forged - owners cannot abort acquisition. :pr:`821` -- Use Python 3.15 for development and automation. :pr:`816` +.. _release-4-1-0: -******************* +******************** 4.1.0 (2026-10-09) -******************* +******************** + +.. _release-4-1-0-added: + +Added +===== - Import :data:`~filelock.OwnerMode`, :data:`~filelock.CompromiseReason` and :data:`~filelock.has_fcntl` from - ``filelock``. :pr:`790` -- :class:`~filelock.AcquireReturnProxy` is generic in the lock type, so ``with lock.acquire() as held:`` types ``held`` as - the lock's own class. :pr:`803` + ``filelock``. :pr:`790` - by :user:`gaborbernat`. +- :class:`~filelock.AcquireReturnProxy` is generic in the lock type, so ``with lock.acquire() as held:`` types ``held`` + as the lock's own class. :pr:`803` - by :user:`gaborbernat`. - A :class:`~filelock.Timeout` from a soft lock names a holder on another host or in another PID namespace, which no - contender ever reclaims. :pr:`815` + contender ever reclaims. :pr:`815` - by :user:`gaborbernat`. +- Every option given in seconds takes a :class:`~decimal.Decimal` or :class:`~fractions.Fraction`, as a file lock's + ``timeout`` already did, and a read-write lock or lease refuses a ``bool`` or string one. :pr:`815` - by + :user:`gaborbernat`. + +.. _release-4-1-0-fixed: + +Fixed +===== + - :attr:`~filelock.SoftFileLock.is_lock_held_by_us` and :attr:`~filelock.MarkerSoftFileLock.is_lock_held_by_us` return - ``False`` for a marker an earlier process with the same PID left behind. :pr:`774` + ``False`` for a marker an earlier process with the same PID left behind. :pr:`774` - by :user:`lprnmns`. - :class:`~filelock.SoftFileLock` removes its marker when writing it fails and closing the descriptor fails too, so the - lock path no longer stays blocked. :pr:`775` + lock path no longer stays blocked. :pr:`775` - by :user:`harbinresearcher`. - :meth:`~filelock.BaseFileLock.acquire` and :meth:`~filelock.BaseAsyncFileLock.acquire` no longer wait past ``timeout`` - when ``poll_interval`` is longer than the time left. :pr:`777` + when ``poll_interval`` is longer than the time left. :pr:`777` - by :user:`feiiiiii5`. - :class:`~filelock.SoftReadWriteLock` ignores a generation-log entry whose name is not 20 ASCII digits, which used to - stop its heartbeat thread or delete real generations. :pr:`778` + stop its heartbeat thread or delete real generations. :pr:`778` - by :user:`dxbjavid`. - :class:`~filelock.ReadWriteLock`, :class:`~filelock.SoftReadWriteLock` and their async wrappers treat a - :data:`math.inf` timeout like ``-1`` and raise :class:`ValueError` for a blocking ``nan`` one. :pr:`779` + :data:`math.inf` timeout like ``-1`` and raise :class:`ValueError` for a blocking ``nan`` one. :pr:`779` - by + :user:`darrenhuai`. - :class:`~filelock.ReadWriteLock` and :class:`~filelock.SoftReadWriteLock` accept a timeout above - :data:`threading.TIMEOUT_MAX` instead of raising :class:`OverflowError`. :pr:`780` -- :meth:`~filelock.BaseFileLock.acquire` raises :class:`ValueError` for a blocking ``nan`` timeout or a ``poll_interval`` - above :data:`threading.TIMEOUT_MAX`, and treats a :data:`math.inf` timeout like ``-1``. :pr:`781` -- :class:`~filelock.SoftFileLock` and :class:`~filelock.MarkerSoftFileLock` treat a marker cut before its final newline as - malformed, so a crash-truncated record self-heals and a read mid-write no longer breaks a live lock. :pr:`782` + :data:`threading.TIMEOUT_MAX` instead of raising :class:`OverflowError`. :pr:`780` - by :user:`darrenhuai`. +- :meth:`~filelock.BaseFileLock.acquire` raises :class:`ValueError` for a blocking ``nan`` timeout or a + ``poll_interval`` above :data:`threading.TIMEOUT_MAX`, and treats a :data:`math.inf` timeout like ``-1``. :pr:`781` - + by :user:`gaborbernat`. +- :class:`~filelock.SoftFileLock` and :class:`~filelock.MarkerSoftFileLock` treat a marker cut before its final newline + as malformed, so a crash-truncated record self-heals and a read mid-write no longer breaks a live lock. :pr:`782` - by + :user:`gaborbernat`. - :class:`~filelock.UnixFileLock` no longer deletes another process's live marker when it falls back to a soft lock on - ``ENOSYS``, and on macOS a lock path ending in a separator raises :class:`FileNotFoundError` instead of retrying. :pr:`785` + ``ENOSYS``, and on macOS a lock path ending in a separator raises :class:`FileNotFoundError` instead of retrying. + :pr:`785` - by :user:`gaborbernat`. - :class:`~filelock.SoftFileLock` and :class:`~filelock.SoftFileLease` put back a live successor's marker that a racing - stale break moved aside, so a third contender no longer acquires beside the successor. :pr:`786` + stale break moved aside, so a third contender no longer acquires beside the successor. :pr:`786` - by + :user:`gaborbernat`. - :class:`~filelock.StrictSoftFileLock` removes every claim a failed acquire published, counts an ``EEXIST`` link that - landed as published, and no longer reports ``is_locked`` after a failed claim cleanup. :pr:`787` -- On Python 3.10 without the ``exceptiongroup`` backport, a release failure under the default ``context_error_policy`` no - longer turns into :exc:`ModuleNotFoundError`; cleanup failures chain instead of grouping, and ``on_acquired`` is allowed. :pr:`788` + landed as published, and no longer reports ``is_locked`` after a failed claim cleanup. :pr:`787` - by + :user:`gaborbernat`. +- On Python 3.10 without the ``exceptiongroup`` backport, a release failure under the default ``context_error_policy`` + no longer turns into :exc:`ModuleNotFoundError`; cleanup failures chain instead of grouping, and ``on_acquired`` is + allowed. :pr:`788` - by :user:`gaborbernat`. - :class:`~filelock.AsyncReadWriteLock` and :class:`~filelock.AsyncSoftReadWriteLock` drop a task's hold when a caller - cancels it during a release that still completes, instead of blocking every later writer. :pr:`789` + cancels it during a release that still completes, instead of blocking every later writer. :pr:`789` - by + :user:`gaborbernat`. - :class:`~filelock.SoftFileLease` releases a lease dropped while held, at garbage collection or interpreter exit, and - raises :class:`ValueError` for a ``heartbeat_interval`` above :data:`threading.TIMEOUT_MAX`. :pr:`791` + raises :class:`ValueError` for a ``heartbeat_interval`` above :data:`threading.TIMEOUT_MAX`. :pr:`791` - by + :user:`gaborbernat`. - :class:`~filelock.StrictSoftFileLock` judges an abandoned private record's age by the filesystem's clock, so a client - clock ahead of an NFS server no longer aborts live publications. :pr:`792` + clock ahead of an NFS server no longer aborts live publications. :pr:`792` - by :user:`gaborbernat`. - :class:`~filelock.BaseAsyncFileLock` raises :class:`RuntimeError` instead of :class:`asyncio.CancelledError` when its - executor shuts down with the call still queued, and a waiter interrupted by anything else no longer wedges later calls. :pr:`793` -- Constructing or closing a second :class:`~filelock.ReadWriteLock` on a held database no longer drops the holder's lock, - so other processes stay excluded. :pr:`794` + executor shuts down with the call still queued, and a waiter interrupted by anything else no longer wedges later + calls. :pr:`793` - by :user:`gaborbernat`. +- Constructing or closing a second :class:`~filelock.ReadWriteLock` on a held database no longer drops the holder's + lock, so other processes stay excluded. :pr:`794` - by :user:`gaborbernat`. - :class:`~filelock.SoftReadWriteLock` no longer admits a second writer after ``.rw`` is removed under a live - instance, and flushes each snapshot before publishing it so a crash cannot leave an empty one that wedges every peer. :pr:`795` + instance, and flushes each snapshot before publishing it so a crash cannot leave an empty one that wedges every peer. + :pr:`795` - by :user:`gaborbernat`. - :class:`~filelock.SoftFileLease` reclaims a dead :class:`~filelock.MarkerSoftFileLock` owner, and expires a lease or a - malformed soft marker after it stays unchanged on the contender's own monotonic clock, so clock skew cannot break it. :pr:`796` + malformed soft marker after it stays unchanged on the contender's own monotonic clock, so clock skew cannot break it. + :pr:`796` - by :user:`gaborbernat`. - A soft lock marker written in a Linux container records its PID namespace with the hostname, so a sibling container - that shares the hostname and volume no longer breaks a live lock. :pr:`797` + that shares the hostname and volume no longer breaks a live lock. :pr:`797` - by :user:`gaborbernat`. - :meth:`SoftReadWriteLock.close() ` makes an acquire still waiting in another thread - raise :class:`RuntimeError`, where it used to take the lock on the closed instance and hold it until the process exited. :pr:`798` + raise :class:`RuntimeError`, where it used to take the lock on the closed instance and hold it until the process + exited. :pr:`798` - by :user:`gaborbernat`. - :class:`~filelock.SoftReadWriteLock` reads the head from ``gen/HEAD``, so a client whose cached directory listing is - more than 64 generations stale no longer takes an old generation for the head and forks the log. :pr:`799` + more than 64 generations stale no longer takes an old generation for the head and forks the log. :pr:`799` - by + :user:`gaborbernat`. - When the garbage collector finalizes a held :class:`~filelock.BaseAsyncFileLock`, it releases on a ``loop`` running in - another thread, or in place once no loop is left; the ``lifetime`` check and path resolution now run in the executor. :pr:`800` -- :class:`~filelock.SoftReadWriteLock` waits out a holder's own ``stale_threshold`` before evicting it, keys its singleton - on the path its log sits beside, and no longer leaks ledger entries or the holder record of a failed acquire. :pr:`801` + another thread, or in place once no loop is left; the ``lifetime`` check and path resolution now run in the executor. + :pr:`800` - by :user:`gaborbernat`. +- :class:`~filelock.SoftReadWriteLock` waits out a holder's own ``stale_threshold`` before evicting it, keys its + singleton on the path its log sits beside, and no longer leaks ledger entries or the holder record of a failed + acquire. :pr:`801` - by :user:`gaborbernat`. - :func:`~filelock.lock_descriptor` raises :class:`ValueError` for a blocking ``poll_interval`` above :data:`threading.TIMEOUT_MAX` up front, instead of :class:`OverflowError` once the descriptor is contended. :pr:`802` + - by :user:`sricharanreddycheruku`. - :class:`~filelock.BaseFileLock` rejects a ``bool`` or string timeout, an out-of-range ``mode`` and a non-``bool`` - ``blocking`` set at any time, rejects unknown options on a singleton cache hit, and tracks an ``on_acquired`` reacquire. :pr:`803` + ``blocking`` set at any time, rejects unknown options on a singleton cache hit, and tracks an ``on_acquired`` + reacquire. :pr:`803` - by :user:`gaborbernat`. - :class:`~filelock.StrictSoftFileLock` raises :class:`~filelock.SoftFileLockProtocolError` naming a directory, symlink - or exited soft-lock owner's marker at the lock path, and ``lock_path_occupant`` shows what blocks it. :pr:`804` + or exited soft-lock owner's marker at the lock path, and ``lock_path_occupant`` shows what blocks it. :pr:`804` - by + :user:`gaborbernat`. - :class:`~filelock.ReadWriteLock` raises :class:`ValueError` for a WAL-mode database instead of ``disk I/O error``, and - its ``/dev/fd`` fallback links beside the database, so a lock off the TMPDIR filesystem no longer fails with ``EXDEV``. :pr:`805` + its ``/dev/fd`` fallback links beside the database, so a lock off the TMPDIR filesystem no longer fails with + ``EXDEV``. :pr:`805` - by :user:`gaborbernat`. - :meth:`ReadWriteLock.release() ` from a thread that holds no read level raises - :class:`RuntimeError` instead of ending another thread's read lock and letting a writer in. :pr:`806` + :class:`RuntimeError` instead of ending another thread's read lock and letting a writer in. :pr:`806` - by + :user:`gaborbernat`. - :class:`~filelock.SoftFileLockLifetimeWarning` derives from :class:`FutureWarning` instead of - :class:`DeprecationWarning`, so Python shows it by default outside ``__main__``. :pr:`807` -- :class:`~filelock.SoftFileLock` applies an explicit ``mode`` past the umask as :class:`~filelock.UnixFileLock` does, and - reads a marker number spelled other than in ASCII digits, or a lease with an empty token, as malformed. :pr:`807` -- :class:`~filelock.ReadWriteLock` raises :class:`OSError` when its database is unlinked or replaced while an acquisition - waits, instead of taking a lock that no later opener sees. :pr:`808` + :class:`DeprecationWarning`, so Python shows it by default outside ``__main__``. :pr:`807` - by :user:`gaborbernat`. +- :class:`~filelock.SoftFileLock` applies an explicit ``mode`` past the umask as :class:`~filelock.UnixFileLock` does, + and reads a marker number spelled other than in ASCII digits, or a lease with an empty token, as malformed. :pr:`807` + - by :user:`gaborbernat`. +- :class:`~filelock.ReadWriteLock` raises :class:`OSError` when its database is unlinked or replaced while an + acquisition waits, instead of taking a lock that no later opener sees. :pr:`808` - by :user:`gaborbernat`. - On a thread-local synchronous lock, :meth:`~filelock.BaseFileLock.release` with ``force=True`` and garbage collection - release holds exited threads left, and a ``release()`` re-entered from a signal handler closes the descriptor once. :pr:`810` -- :class:`~filelock.SoftFileLease` records a ``lease_duration`` given as a :class:`float` subclass, such as NumPy's, in a - form every contender parses, so a live lease is no longer evicted as malformed. :pr:`815` -- :class:`~filelock.StrictSoftFileLock` raises :class:`~filelock.SoftFileLockProtocolError` for a file at the lock path it - cannot parse once it stays unchanged for two seconds, instead of waiting on it forever. :pr:`815` -- A lock finalized at interpreter exit no longer hangs when a daemon thread stopped inside filelock's fork bookkeeping. :pr:`815` + release holds exited threads left, and a ``release()`` re-entered from a signal handler closes the descriptor once. + :pr:`810` - by :user:`gaborbernat`. +- :class:`~filelock.SoftFileLease` records a ``lease_duration`` given as a :class:`float` subclass, such as NumPy's, in + a form every contender parses, so a live lease is no longer evicted as malformed. :pr:`815` - by :user:`gaborbernat`. +- :class:`~filelock.StrictSoftFileLock` raises :class:`~filelock.SoftFileLockProtocolError` for a file at the lock path + it cannot parse once it stays unchanged for two seconds, instead of waiting on it forever. :pr:`815` - by + :user:`gaborbernat`. +- A lock finalized at interpreter exit no longer hangs when a daemon thread stopped inside filelock's fork bookkeeping. + :pr:`815` - by :user:`gaborbernat`. - Setting :attr:`BaseFileLock.timeout ` from a string emits a :class:`DeprecationWarning` - and from a ``bool`` raises :class:`TypeError`; pass the number of seconds instead. :pr:`815` + and from a ``bool`` raises :class:`TypeError`; pass the number of seconds instead. :pr:`815` - by :user:`gaborbernat`. - Every lock raises :class:`ValueError` for a timeout too large for a :class:`float`, and a singleton accepts a timeout - that waits as long as the cached one, such as ``inf`` for ``-1``. :pr:`815` -- Every option given in seconds takes a :class:`~decimal.Decimal` or :class:`~fractions.Fraction`, as a file lock's - ``timeout`` already did, and a read-write lock or lease refuses a ``bool`` or string one. :pr:`815` + that waits as long as the cached one, such as ``inf`` for ``-1``. :pr:`815` - by :user:`gaborbernat`. - On macOS, :class:`~filelock.ReadWriteLock` connects through a private hard link, so cycling a lock beside a holder no - longer adds an open descriptor per connection. :pr:`815` + longer adds an open descriptor per connection. :pr:`815` - by :user:`gaborbernat`. - :class:`~filelock.SoftFileLock` and :class:`~filelock.SoftFileLease` re-check a stale marker right before breaking it, - so a marker recreated since the check is no longer moved aside for a third contender to take the path. :pr:`815` + so a marker recreated since the check is no longer moved aside for a third contender to take the path. :pr:`815` - by + :user:`gaborbernat`. - An acquire a signal handler starts while its thread is still acquiring the same lock raises :class:`RuntimeError`, - where it used to hang that thread or leave the lock held after the outer release. :pr:`815` -- A :class:`~filelock.BaseAsyncFileLock` singleton the garbage collector finalizes leaves the singleton cache first, so a - new lock on the path waits for its release instead of re-entering a hold that release then drops. :pr:`815` + where it used to hang that thread or leave the lock held after the outer release. :pr:`815` - by :user:`gaborbernat`. +- A :class:`~filelock.BaseAsyncFileLock` singleton the garbage collector finalizes leaves the singleton cache first, so + a new lock on the path waits for its release instead of re-entering a hold that release then drops. :pr:`815` - by + :user:`gaborbernat`. - A finalized thread-local :class:`~filelock.BaseAsyncFileLock` releases every thread's hold, including one on a daemon - thread's event loop at interpreter exit, instead of leaving it held. :pr:`815` + thread's event loop at interpreter exit, instead of leaving it held. :pr:`815` - by :user:`gaborbernat`. - A soft lock in the host's PID namespace no longer breaks a live marker written from a container sharing the hostname - while the kernel shows its owner running in another namespace under the recorded PID and start time. :pr:`815` + while the kernel shows its owner running in another namespace under the recorded PID and start time. :pr:`815` - by + :user:`gaborbernat`. - :class:`~filelock.SoftFileLock` and its marker and lease variants no longer leave their marker behind when a signal - handler interrupts a release; the retried release, or the next acquire on Windows, removes it. :pr:`815` + handler interrupts a release; the retried release, or the next acquire on Windows, removes it. :pr:`815` - by + :user:`gaborbernat`. - :class:`~filelock.SoftReadWriteLock` raises :class:`ValueError` for a ``heartbeat_interval`` above - :data:`threading.TIMEOUT_MAX`, which killed its heartbeat thread, and an interrupted release still removes its record. :pr:`815` -- :class:`~filelock.AsyncReadWriteLock` releases only its own hold on a singleton it shares with synchronous users, where - it used to end their read transaction too. :pr:`815` -- The documentation's canonical link, ``og:url`` and sitemap entries point at the versioned page on Read the Docs instead - of a domain-root URL that returns 404. :pr:`813` -- The source distribution now includes ``docs/``, so ``tox -e docs`` builds the documentation from an unpacked sdist. :pr:`812` + :data:`threading.TIMEOUT_MAX`, which killed its heartbeat thread, and an interrupted release still removes its record. + :pr:`815` - by :user:`gaborbernat`. +- :class:`~filelock.AsyncReadWriteLock` releases only its own hold on a singleton it shares with synchronous users, + where it used to end their read transaction too. :pr:`815` - by :user:`gaborbernat`. -******************** +.. _release-4-1-0-documentation: + +Documentation +============= + +- The documentation's canonical link, ``og:url`` and sitemap entries point at the versioned page on Read the Docs + instead of a domain-root URL that returns 404. :pr:`813` - by :user:`gaborbernat`. + +.. _release-4-0-12: + +********************* 4.0.12 (2026-10-05) -******************** +********************* -- Fix read-lock acquisition on SQLite versions older than 3.33.0 by using ``sqlite_master`` for the schema probe. :pr:`769` +.. _release-4-0-12-fixed: -******************** +Fixed +===== + +- Fix read-lock acquisition on SQLite versions older than 3.33.0 by using ``sqlite_master`` for the schema probe. + :pr:`769` - by :user:`Vikram12301`. + +.. _release-4-0-11: + +********************* 4.0.11 (2026-10-05) -******************** +********************* + +.. _release-4-0-11-fixed: + +Fixed +===== - ``ReadWriteLock`` and ``AsyncReadWriteLock`` use a validated private hard link when ``/dev/fd`` has no entry for the database descriptor, supporting NetBSD's static descriptor directory beyond descriptor 63. The temporary location must - share a filesystem with the database. The symlink refusal test accepts NetBSD's error wording. + share a filesystem with the database. The symlink refusal test accepts NetBSD's error wording - by + :user:`gaborbernat`. -******************** +.. _release-4-0-10: + +********************* 4.0.10 (2026-10-03) -******************** +********************* + +.. _release-4-0-10-fixed: + +Fixed +===== - Reusing a singleton ``AsyncFileLock`` with another ``loop``, ``executor`` or ``run_in_executor``, or a singleton ``SoftFileLease`` with another ``lease_duration``, ``heartbeat_interval`` or ``on_compromise``, now raises - ``ValueError`` instead of returning the lock with its original options. :pr:`765` + ``ValueError`` instead of returning the lock with its original options. :pr:`765` - by :user:`kokotatan`. + +.. _release-4-0-9: -******************* +******************** 4.0.9 (2026-10-01) -******************* +******************** + +.. _release-4-0-9-fixed: + +Fixed +===== - ``ReadWriteLock`` and ``AsyncReadWriteLock`` close the descriptor that checks the database path once SQLite has - connected, so on PyPy a dropped lock leaves no descriptor open until garbage collection runs. :pr:`763` -- ``ReadWriteLock`` and ``AsyncReadWriteLock`` refuse a symlink at the database path instead of following it, so a - user who can create names in a shared lock directory cannot point the lock at another file (GHSA-j8f7-rjxc-mr56). + connected, so on PyPy a dropped lock leaves no descriptor open until garbage collection runs. :pr:`763` - by + :user:`gaborbernat`. + +.. _release-4-0-9-security: + +Security +======== + +- ``ReadWriteLock`` and ``AsyncReadWriteLock`` refuse a symlink at the database path instead of following it, so a user + who can create names in a shared lock directory cannot point the lock at another file (GHSA-j8f7-rjxc-mr56) - by + :user:`gaborbernat`. + +.. _release-4-0-8: -******************* +******************** 4.0.8 (2026-10-01) -******************* +******************** + +.. _release-4-0-8-fixed: + +Fixed +===== - ``ReadWriteLock.release()`` and ``SoftReadWriteLock.release()`` from a thread that does not hold the write lock now - raise ``RuntimeError`` instead of dropping the holder's lock and letting a second writer in. :pr:`761` + raise ``RuntimeError`` instead of dropping the holder's lock and letting a second writer in. :pr:`761` - by + :user:`feiiiiii5`. -******************* +.. _release-4-0-7: + +******************** 4.0.7 (2026-09-29) -******************* +******************** + +.. _release-4-0-7-fixed: + +Fixed +===== - File locks now raise ``ValueError`` at construction when ``mode`` denies the owner read or write, such as - ``mode=0o444``, instead of failing on a later acquire and staying broken until someone deletes the lock file. :pr:`760` + ``mode=0o444``, instead of failing on a later acquire and staying broken until someone deletes the lock file. + :pr:`760` - by :user:`gaborbernat`. -******************* +.. _release-4-0-6: + +******************** 4.0.6 (2026-09-28) -******************* +******************** + +.. _release-4-0-6-fixed: + +Fixed +===== - Reject negative blocking timeouts other than ``-1`` before reentrant ``ReadWriteLock`` and ``SoftReadWriteLock`` - acquisition. Preserve unlimited waits and nonblocking acquisition. :pr:`756` + acquisition. Preserve unlimited waits and nonblocking acquisition. :pr:`756` - by :user:`feiiiiii5`. -******************* +.. _release-4-0-5: + +******************** 4.0.5 (2026-09-28) -******************* +******************** + +.. _release-4-0-5-fixed: + +Fixed +===== - Fix ``MarkerSoftFileLock`` acquisition and prevent contenders from evicting live protocol-2 owners after two seconds. - Reclaim recognized records after owner death; preserve unknown contracts. :pr:`749` + Reclaim recognized records after owner death; preserve unknown contracts. :pr:`751` - by :user:`DawnofGenX`. - Honor instance ``timeout`` and ``blocking`` settings in sync and async ``ReadWriteLock`` acquisition, including waits - between tasks on one instance. Preserve explicit per-call overrides. :pr:`750` + between tasks on one instance. Preserve explicit per-call overrides. :pr:`752` - by :user:`DawnofGenX`. - Skip access-denial checks when the process can read mode-``0o000`` files. Keep mode-bit checks enabled for privileged - processes on filesystems that support POSIX permissions. :pr:`753` -- Skip vanished ``StrictSoftFileLock`` claims after a read-permission retry expires. Recheck the directory before raising - a protocol error so concurrent removal does not turn a stale claim listing into an acquisition failure. :pr:`754` -- Reject negative timeouts other than ``-1`` in blocking ``AsyncReadWriteLock`` and ``AsyncSoftReadWriteLock`` acquisitions. - Keep ``-1`` as an unlimited wait and ignore timeouts when ``blocking=False``. :pr:`755` + processes on filesystems that support POSIX permissions. :pr:`753` - by :user:`pasmud`. +- Skip vanished ``StrictSoftFileLock`` claims after a read-permission retry expires. Recheck the directory before + raising a protocol error so concurrent removal does not turn a stale claim listing into an acquisition failure. + :pr:`754` - by :user:`januththedev`. +- Reject negative timeouts other than ``-1`` in blocking ``AsyncReadWriteLock`` and ``AsyncSoftReadWriteLock`` + acquisitions. Keep ``-1`` as an unlimited wait and ignore timeouts when ``blocking=False``. :pr:`755` - by + :user:`pasmud`. + +.. _release-4-0-4: -******************* +******************** 4.0.4 (2026-09-26) -******************* +******************** + +.. _release-4-0-4-fixed: + +Fixed +===== - Hostnames that still differ after their first 253 escaped characters now publish distinct owners, so a soft lock no - longer takes another such host's live holder for its own and reclaims its marker. :pr:`748` + longer takes another such host's live holder for its own and reclaims its marker. :pr:`748` - by :user:`feiiiiii5`. + +.. _release-4-0-3: -******************* +******************** 4.0.3 (2026-09-23) -******************* +******************** + +.. _release-4-0-3-fixed: -- Importing filelock on CPython 3.10 or 3.11 no longer makes new threads fail with ``RuntimeError: Cannot install a trace - function while another trace function is being installed`` under coverage or a debugger. filelock skips its fork-safety - audit hook there, so forking from inside a thread's own lock-state transition no longer raises immediately on those - versions. :pr:`747` +Fixed +===== -******************* +- Importing filelock on CPython 3.10 or 3.11 no longer makes new threads fail with ``RuntimeError: Cannot install a + trace function while another trace function is being installed`` under coverage or a debugger. filelock skips its + fork-safety audit hook there, so forking from inside a thread's own lock-state transition no longer raises immediately + on those versions. :pr:`747` - by :user:`gaborbernat`. + +.. _release-4-0-2: + +******************** 4.0.2 (2026-09-23) -******************* +******************** + +.. _release-4-0-2-fixed: + +Fixed +===== - Concurrent ``acquire()`` and ``release()`` on a ``thread_local=False`` lock no longer leak the OS lock, close a descriptor twice, drop a lease token, or leave a false deadlock after a cross-thread release (:issue:`744`). :pr:`745` + - by :user:`gaborbernat`. - :class:`~filelock.AsyncReadWriteLock` and :class:`~filelock.AsyncSoftReadWriteLock` now give each ``asyncio`` task its - own hold, so tasks sharing one instance no longer enter the write lock together. :pr:`746` -- Correct the async cache example to create its data directory and clarify automatic creation of lock-file parent directories. :pr:`740` -- Exclude sphinx-llm 1.1.0 from documentation dependencies because its Markdown builder emits unknown-node warnings. :pr:`742` + own hold, so tasks sharing one instance no longer enter the write lock together. :pr:`746` - by :user:`gaborbernat`. +- Correct the async cache example to create its data directory and clarify automatic creation of lock-file parent + directories. :pr:`740` - by :user:`agammann`. + +.. _release-4-0-1: -******************* +******************** 4.0.1 (2026-09-19) -******************* +******************** + +.. _release-4-0-1-fixed: + +Fixed +===== - ``poll_interval`` is now validated at construction, on the setter, and on ``acquire()``: a negative, non-finite, or - non-numeric value raises :class:`ValueError`/:class:`TypeError` immediately instead of failing inside ``time.sleep``. :pr:`739` + non-numeric value raises :class:`ValueError`/:class:`TypeError` immediately instead of failing inside ``time.sleep``. + :pr:`739` - by :user:`Pitchfork-and-Torch`. -******************* +.. _release-4-0-0: + +******************** 4.0.0 (2026-09-17) -******************* +******************** + +.. _release-4-0-0-added: + +Added +===== + +- :class:`~filelock.SoftReadWriteLock` exposes :attr:`~filelock.SoftReadWriteLock.generation` as a fencing token for the + protected resource and reports a lost hold through ``on_compromise`` and + :attr:`~filelock.SoftReadWriteLock.compromise`. :pr:`738` - by :user:`gaborbernat`. + +.. _release-4-0-0-fixed: + +Fixed +===== - The :class:`~filelock.SoftReadWriteLock` on-disk protocol is a generation log under ``.rw``, and a process - running an earlier release does not see it: an old and a new participant on one lock path do not exclude each - other. Stop every participant, upgrade them all, then restart them; the new code ignores leftover ``.state``, - ``.write`` and ``.readers/`` files, and you can delete them. The filesystem must provide no-replace hard links, as - it must for :class:`~filelock.StrictSoftFileLock`, so a runtime without ``os.link`` raises + running an earlier release does not see it: an old and a new participant on one lock path do not exclude each other. + Stop every participant, upgrade them all, then restart them; the new code ignores leftover ``.state``, ``.write`` and + ``.readers/`` files, and you can delete them. The filesystem must provide no-replace hard links, as it must for + :class:`~filelock.StrictSoftFileLock`, so a runtime without ``os.link`` raises :class:`~filelock.SoftFileLockProtocolError` on acquire. Constructing a singleton again with a different - ``on_compromise``, or with ``poll_interval`` at or above ``stale_threshold``, now raises :class:`ValueError`. :pr:`735` -- :class:`~filelock.SoftReadWriteLock` exposes :attr:`~filelock.SoftReadWriteLock.generation` as a fencing token for - the protected resource and reports a lost hold through ``on_compromise`` and - :attr:`~filelock.SoftReadWriteLock.compromise`. :pr:`735` + ``on_compromise``, or with ``poll_interval`` at or above ``stale_threshold``, now raises :class:`ValueError`. + :pr:`738` - by :user:`gaborbernat`. - :class:`~filelock.SoftReadWriteLock` no longer deadlocks when a holder dies on another host mid-transition, and - ``release()`` no longer waits on a mutex a dead host left behind (:pr:`725`, :pr:`735`). The state mutex is gone. - Each transition is one atomic snapshot commit, and liveness is a heartbeat nonce read on the observer's own clock - rather than an ``mtime`` read against another host's. :pr:`735` + ``release()`` no longer waits on a mutex a dead host left behind. The state mutex is gone. Each transition is one + atomic snapshot commit, and liveness is a heartbeat nonce read on the observer's own clock rather than an ``mtime`` + read against another host's. :pr:`738` - by :user:`gaborbernat`. -******************** +.. _release-3-32-7: + +********************* 3.32.7 (2026-09-16) -******************** +********************* + +.. _release-3-32-7-fixed: + +Fixed +===== -- Validate final-symlink refusal by error number so the test works across libc implementations. :pr:`737` - Document that :meth:`~filelock.BaseFileLock.acquire` reads ``blocking=None`` as the lock's ``blocking`` attribute and - raises :class:`~filelock.Timeout` after one attempt when ``blocking=False``. :pr:`733` + raises :class:`~filelock.Timeout` after one attempt when ``blocking=False``. :pr:`733` - by :user:`hxperl`. -******************** +.. _release-3-32-6: + +********************* 3.32.6 (2026-09-08) -******************** +********************* + +.. _release-3-32-6-added: + +Added +===== + +- Allow acquiring existing lock files that grant write access through group permissions or an ACL even when their + owner-write mode bit is unset. :pr:`728` - by :user:`jackwalkerlabs`. + +.. _release-3-32-6-fixed: + +Fixed +===== - ``SoftFileLease`` and ``AsyncSoftFileLease`` now reject a boolean or non-finite ``lease_duration``, which used to - publish an owner record their own ``owner`` property reads back as malformed. :pr:`723` + publish an owner record their own ``owner`` property reads back as malformed. :pr:`723` - by :user:`lprnmns`. - Reject non-finite heartbeat, stale, and polling intervals in ``SoftReadWriteLock`` and ``AsyncSoftReadWriteLock``, - including cached singleton construction and overflow in the default stale threshold. :pr:`724` + including cached singleton construction and overflow in the default stale threshold. :pr:`724` - by :user:`lprnmns`. - Honor acquisition timeouts and ``blocking=False`` during ``SoftReadWriteLock`` state-mutex contention, including - failed writer cleanup. Cross-host recovery of an abandoned ``.state`` marker remains unsupported. :pr:`726` -- Allow acquiring existing lock files that grant write access through group permissions or an ACL even when their - owner-write mode bit is unset. :pr:`728` -- Preserve exception notes and custom attributes when copying or pickling ``Timeout`` and ``SoftFileLockProtocolError``. :pr:`729` + failed writer cleanup. Cross-host recovery of an abandoned ``.state`` marker remains unsupported. :pr:`726` - by + :user:`Sohel2309`. +- Preserve exception notes and custom attributes when copying or pickling ``Timeout`` and ``SoftFileLockProtocolError``. + :pr:`729` - by :user:`jackwalkerlabs`. -******************** +.. _release-3-32-5: + +********************* 3.32.5 (2026-08-31) -******************** +********************* + +.. _release-3-32-5-fixed: + +Fixed +===== - ``SoftFileLease.token`` and ``AsyncSoftFileLease.token`` now read ``None`` after a failed acquisition, so a contender - turned away by a live holder no longer reports a token for a claim it never published. :pr:`721` -- Document that ``mode`` has no setter: unlike ``poll_interval``, ``timeout``, ``blocking`` and ``lifetime``, it is fixed at construction and ``lock.mode = ...`` raises ``AttributeError``. :pr:`716` + turned away by a live holder no longer reports a token for a claim it never published. :pr:`721` - by :user:`lprnmns`. +- Document that ``mode`` has no setter: unlike ``poll_interval``, ``timeout``, ``blocking`` and ``lifetime``, it is + fixed at construction and ``lock.mode = ...`` raises ``AttributeError``. :pr:`716` - by :user:`Gares95`. -******************** +.. _release-3-32-4: + +********************* 3.32.4 (2026-08-23) -******************** +********************* + +.. _release-3-32-4-fixed: + +Fixed +===== - ``StrictSoftFileLock`` always retries a claim read whose first attempt reports the claim as pending, so a first read - that itself outlasts the retry grace no longer fails closed on a claim it could have read. :pr:`705` + that itself outlasts the retry grace no longer fails closed on a claim it could have read. :pr:`705` - by + :user:`gaborbernat`. - ``WindowsFileLock`` waits out a transient ``STATUS_ACCESS_DENIED`` from ``NtCreateFile`` for up to half a second - before raising ``PermissionError``, since a peer unlinking the lock file as it releases can answer that for a moment; a - real denial still fails fast. :pr:`705` + before raising ``PermissionError``, since a peer unlinking the lock file as it releases can answer that for a moment; + a real denial still fails fast. :pr:`705` - by :user:`gaborbernat`. - Every lock class now escapes the hostname it publishes, so a host whose ``socket.gethostname()`` carries a space, a newline or a byte outside UTF-8 no longer writes a marker it reads back as malformed. Such a host used to lose a held - ``SoftReadWriteLock`` read slot to a peer and could not take a write slot or a ``StrictSoftFileLock`` at all. :pr:`709` + ``SoftReadWriteLock`` read slot to a peer and could not take a write slot or a ``StrictSoftFileLock`` at all. + :pr:`709` - by :user:`dxbjavid`. -******************** +.. _release-3-32-3: + +********************* 3.32.3 (2026-08-13) -******************** +********************* + +.. _release-3-32-3-fixed: + +Fixed +===== - The fork-safety audit hook no longer prints ``Exception ignored in audit hook`` with a ``TypeError`` when an audit - event fires during interpreter shutdown, after CPython has already cleared the module globals. :pr:`701` + event fires during interpreter shutdown, after CPython has already cleared the module globals. :pr:`703` - by + :user:`gaborbernat`. -******************** +.. _release-3-32-2: + +********************* 3.32.2 (2026-07-29) -******************** +********************* -- A ``SoftReadWriteLock`` or ``SoftFileLease`` acquire whose heartbeat thread fails to start now unlinks its marker and - hands the claim back, instead of leaving an unrefreshed marker a peer takes while the caller believes it still holds - the lock. :pr:`691` +.. _release-3-32-2-fixed: -******************** - 3.32.1 (2026-07-26) -******************** +Fixed +===== +- A ``SoftReadWriteLock`` or ``SoftFileLease`` acquire whose heartbeat thread fails to start now unlinks its marker and + hands the claim back, instead of leaving an unrefreshed marker a peer takes while the caller believes it still holds + the lock. :pr:`691` - by :user:`dxbjavid`. - Canceling an ``AsyncSoftReadWriteLock`` acquire now releases the claim instead of leaking a marker whose heartbeat - wedges every contender. :pr:`686` + wedges every contender. :pr:`686` - by :user:`dxbjavid`. -******************** +.. _release-3-32-0: + +********************* 3.32.0 (2026-07-21) -******************** +********************* + +.. _release-3-32-0-added: + +Added +===== + +- Declare support for Python 3.15 and run the test suite against it and its free-threaded build, both currently in beta. + :pr:`683` - by :user:`gaborbernat`. + +.. _release-3-32-0-fixed: + +Fixed +===== - ``SoftReadWriteLock`` closes the directory handle it opens to scan for readers as soon as a scan stops early, rather - than holding it until the generator is collected. :pr:`685` -- Declare support for Python 3.15 and run the test suite against it and its free-threaded build, both currently in beta. :pr:`683` -- The source distribution ships the capability probes the tests import, and reading one no longer needs ``coverage`` - installed, so the suite runs from an unpacked sdist instead of failing on a missing ``coverage_pragmas``. :pr:`685` + than holding it until the generator is collected. :pr:`685` - by :user:`gaborbernat`. -******************** +.. _release-3-31-2: + +********************* 3.31.2 (2026-07-21) -******************** +********************* + +.. _release-3-31-2-fixed: + +Fixed +===== - ``filelock`` imports again on runtimes whose ``errno`` omits ``ENOTSUP``, such as GraalPy, where importing the package raised ``ImportError``. It probes the code instead, preferring ``ENOTSUP``, falling back to ``EOPNOTSUPP`` where that name is absent, and dropping to ``ENOSYS``/``EXDEV`` where neither exists. Platforms defining ``ENOTSUP`` keep their - behavior. :pr:`681` + behavior. :pr:`681` - by :user:`gaborbernat`. -******************** +.. _release-3-31-1: + +********************* 3.31.1 (2026-07-20) -******************** +********************* + +.. _release-3-31-1-fixed: + +Fixed +===== - A ``SoftFileLease`` acquired on one thread keeps its claim when another thread fails to acquire the same lease object, - so its heartbeat carries on refreshing the marker instead of being torn down and letting a peer take the live claim. :pr:`680` + so its heartbeat carries on refreshing the marker instead of being torn down and letting a peer take the live claim. + :pr:`680` - by :user:`dxbjavid`. -******************** +.. _release-3-31-0: + +********************* 3.31.0 (2026-07-18) -******************** +********************* + +.. _release-3-31-0-added: + +Added +===== - Support Termux/Android, whose CPython ships without ``os.link`` and reports ``sys.platform == "android"``. ``import filelock`` and both ``FileLock`` and ``SoftFileLock`` now work there, ``StrictSoftFileLock`` reports its missing - hard-link support only when acquired, and process liveness reads ``/proc`` on Android instead of PID-only checks. :pr:`678` + hard-link support only when acquired, and process liveness reads ``/proc`` on Android instead of PID-only checks. + :pr:`678` - by :user:`gaborbernat`. + +.. _release-3-31-0-fixed: + +Fixed +===== + - ``StrictSoftFileLock`` no longer lets two processes hold the lock at once under heavy contention. A holder now keeps - its intent claim for the whole hold, so a contender whose directory scan races the holder's freshly linked claim can no - longer miss it and win alongside it. A held lock now exposes both an ``intent`` and a ``held`` claim. :pr:`678` + its intent claim for the whole hold, so a contender whose directory scan races the holder's freshly linked claim can + no longer miss it and win alongside it. A held lock now exposes both an ``intent`` and a ``held`` claim. :pr:`678` - + by :user:`gaborbernat`. -******************** +.. _release-3-30-3: + +********************* 3.30.3 (2026-07-17) -******************** +********************* + +.. _release-3-30-3-fixed: + +Fixed +===== - ``AsyncFileLock`` and ``AsyncSoftFileLock`` no longer raise a deadlock ``RuntimeError`` when a different asyncio task - waits for a lock they hold; the check now scopes holders to the owning task, so only a same-task reacquire is refused. :pr:`676` -- Keep both tables of contents on screen at any browser font size. The widened body column sized itself in ``em`` against the reader's font size while the breakpoints that fold the layout resolve ``em`` against a fixed 16px, so a reader whose default font is larger than 16px had the right-hand table of contents clipped off the edge. The body column now flexes instead, and the right-hand table of contents hides only at the mobile breakpoint. :pr:`673` + waits for a lock they hold; the check now scopes holders to the owning task, so only a same-task reacquire is refused. + :pr:`676` - by :user:`xt-yin`. + +.. _release-3-30-3-documentation: + +Documentation +============= + +- Keep both tables of contents on screen at any browser font size. The widened body column sized itself in ``em`` + against the reader's font size while the breakpoints that fold the layout resolve ``em`` against a fixed 16px, so a + reader whose default font is larger than 16px had the right-hand table of contents clipped off the edge. The body + column now flexes instead, and the right-hand table of contents hides only at the mobile breakpoint. :pr:`673` - by + :user:`gaborbernat`. - Color the mermaid diagrams from whichever theme is active. They previously carried a light palette baked into each - diagram's source, which stayed light on a dark page. :pr:`674` + diagram's source, which stayed light on a dark page. :pr:`674` - by :user:`gaborbernat`. -******************** +.. _release-3-30-2: + +********************* 3.30.2 (2026-07-16) -******************** +********************* -- Stop :class:`~filelock.SoftFileLease` deleting a live marker whose ``mode`` it does not implement. An unrecognized mode now names its owner instead of reading as malformed, so a record written by a newer filelock survives the grace window rather than being evicted after two seconds. :pr:`672` -- Stop :class:`~filelock.StrictSoftFileLock` and :class:`~filelock.AsyncStrictSoftFileLock` calling themselves a native OS lock when they warn that they ignore ``lifetime``; they now say a strict claim is only ever cleared by ``force_break()``. :pr:`672` -- Cover every lock type in the tutorials and how-to guides, with examples drawn from projects that use filelock, and color the mermaid diagrams. - Correct the claims that ``StrictSoftFileLock`` exposes ``owner``, that :class:`~filelock.SoftFileLock` evicts a strict sentinel, that :class:`~filelock.ReadWriteLock` requires a ``.db`` extension, and that every log record is ``DEBUG``. :pr:`672` +.. _release-3-30-2-fixed: -******************** +Fixed +===== + +- Stop :class:`~filelock.SoftFileLease` deleting a live marker whose ``mode`` it does not implement. An unrecognized + mode now names its owner instead of reading as malformed, so a record written by a newer filelock survives the grace + window rather than being evicted after two seconds. :pr:`672` - by :user:`gaborbernat`. +- Stop :class:`~filelock.StrictSoftFileLock` and :class:`~filelock.AsyncStrictSoftFileLock` calling themselves a native + OS lock when they warn that they ignore ``lifetime``; they now say a strict claim is only ever cleared by + ``force_break()``. :pr:`672` - by :user:`gaborbernat`. + +.. _release-3-30-2-documentation: + +Documentation +============= + +- Cover every lock type in the tutorials and how-to guides, with examples drawn from projects that use filelock, and + color the mermaid diagrams. Correct the claims that ``StrictSoftFileLock`` exposes ``owner``, that + :class:`~filelock.SoftFileLock` evicts a strict sentinel, that :class:`~filelock.ReadWriteLock` requires a ``.db`` + extension, and that every log record is ``DEBUG``. :pr:`672` - by :user:`gaborbernat`. + +.. _release-3-30-1: + +********************* 3.30.1 (2026-07-16) -******************** +********************* + +.. _release-3-30-1-fixed: + +Fixed +===== - ``StrictSoftFileLock`` and ``AsyncStrictSoftFileLock`` no longer abort acquisition when a peer's claim vanishes as an NFSv3 stale filehandle instead of a clean removal; the reader revalidates and skips it, so strict locks hold mutual - exclusion across independent NFSv3 client caches. :pr:`669` + exclusion across independent NFSv3 client caches. :pr:`669` - by :user:`gaborbernat`. - ``SoftFileLease`` reads a marker whose lease duration is ``nan`` or ``inf`` as malformed rather than as a valid lease, - so such a marker ages out through the grace window instead of raising ``LeaseSettingsMismatch`` on every contender. :pr:`670` + so such a marker ages out through the grace window instead of raising ``LeaseSettingsMismatch`` on every contender. + :pr:`670` - by :user:`dxbjavid`. -******************** +.. _release-3-30-0: + +********************* 3.30.0 (2026-07-16) -******************** +********************* + +.. _release-3-30-0-added: -- Add ``context_error_policy`` to surface body and release failures as a :exc:`BaseExceptionGroup`. :pr:`618` -- Add ``close_error_policy`` to control an :func:`os.close` failure after the OS unlock. :pr:`619` -- Add :func:`~filelock.lock_descriptor` and :func:`~filelock.unlock_descriptor` to lock a caller-owned file descriptor. :pr:`620` -- Add ``fallback_to_soft`` to fail closed instead of downgrading to :class:`~filelock.SoftFileLock` on ``ENOSYS``. :pr:`622` -- Add ``preserve_lock_file`` so native locks keep the lock pathname on release. :pr:`624` -- Add an ``on_acquired`` hook that runs a callback on the locked descriptor once the lock is held. :pr:`625` +Added +===== + +- Add ``context_error_policy`` to surface body and release failures as a :exc:`BaseExceptionGroup`. :pr:`618` - by + :user:`gaborbernat`. +- Add ``close_error_policy`` to control an :func:`os.close` failure after the OS unlock. :pr:`619` - by + :user:`gaborbernat`. +- Add :func:`~filelock.lock_descriptor` and :func:`~filelock.unlock_descriptor` to lock a caller-owned file descriptor. + :pr:`620` - by :user:`gaborbernat`. +- Add ``fallback_to_soft`` to fail closed instead of downgrading to :class:`~filelock.SoftFileLock` on ``ENOSYS``. + :pr:`622` - by :user:`gaborbernat`. +- Add ``preserve_lock_file`` so native locks keep the lock pathname on release. :pr:`624` - by :user:`gaborbernat`. +- Add an ``on_acquired`` hook that runs a callback on the locked descriptor once the lock is held. :pr:`625` - by + :user:`gaborbernat`. - Add ``StrictSoftFileLock``, which treats every marker it did not publish as contention, and ``SoftFileLease``, whose - claim expires and whose holder learns through ``on_compromise`` when it is lost. ``SoftFileLock(lifetime=...)`` now warns - and names them. Both publish a record ``SoftFileLock`` evicts, so do not mix contracts on one path. :pr:`636` + claim expires and whose holder learns through ``on_compromise`` when it is lost. ``SoftFileLock(lifetime=...)`` now + warns and names them. Both publish a record ``SoftFileLock`` evicts, so do not mix contracts on one path. :pr:`658` - + by :user:`gaborbernat`. - Add ``StrictSoftFileLock`` and ``AsyncStrictSoftFileLock`` with owner-specific hard-linked claims. Strict mode fails - closed on claim damage, exposes each recovery token, and requires an exact claim name for a force break. :pr:`637` -- Ignore ``lifetime`` on native locks with a warning; only :class:`~filelock.SoftFileLock` honors it. :pr:`593` -- Stop mutating the Unix lock file before :func:`fcntl.flock` is held. :pr:`594` -- Bind the Windows reparse-point check to the locked handle to close a symlink-swap race. :pr:`596` -- Evict a non-regular soft lock file without reading it. :pr:`597` -- Write a soft lock's holder record atomically so a short write cannot create overlapping holders. :pr:`614` -- Make native lock release transactional so a failed unlock is retried, not dropped. :pr:`615` -- Open the Windows lock file via ``NtCreateFile`` so a real access denial is raised, not reported as a timeout. :pr:`617` -- Canonicalize singleton keys so equivalent path spellings share an instance, without following a final symlink. :pr:`621` + closed on claim damage, exposes each recovery token, and requires an exact claim name for a force break. :pr:`659` - + by :user:`gaborbernat`. + +.. _release-3-30-0-changes: + +Changes +======= + +- Make native lock release transactional so a failed unlock is retried, not dropped. :pr:`615` - by :user:`gaborbernat`. + +.. _release-3-30-0-fixed: + +Fixed +===== + +- Ignore ``lifetime`` on native locks with a warning; only :class:`~filelock.SoftFileLock` honors it. :pr:`593` - by + :user:`gaborbernat`. +- Stop mutating the Unix lock file before :func:`fcntl.flock` is held. :pr:`594` - by :user:`gaborbernat`. +- Evict a non-regular soft lock file without reading it. :pr:`597` - by :user:`dxbjavid`. +- Write a soft lock's holder record atomically so a short write cannot create overlapping holders. :pr:`614` - by + :user:`gaborbernat`. +- Open the Windows lock file via ``NtCreateFile`` so a real access denial is raised, not reported as a timeout. + :pr:`617` - by :user:`gaborbernat`. +- Canonicalize singleton keys so equivalent path spellings share an instance, without following a final symlink. + :pr:`621` - by :user:`gaborbernat`. - Preserve parent lock ownership across ``fork()``. A child closes an inherited descriptor only while its identity still - matches, so it neither unlocks nor unlinks the parent's lock. It clears inherited ownership state and singleton caches, - then requires a new lock instance before acquiring. Fork-time construction replaces inherited cache mutexes and omits - an instance whose construction crossed into the child from the cache. If ``fstat()`` cannot establish the identity, - child cleanup skips the descriptor number because an earlier fork callback may have reused it. Descriptor tracking - also covers third-party backends that fail after assigning a descriptor. :pr:`634` -- Open a SQLite connection per outer acquisition and close it on final release, so a forked child can no longer close its - parent's handle. A child rejects inherited databases until ``exec()``, and a PyPy child rejects read-write locks once the - parent has used SQLite. :pr:`635` + matches, so it neither unlocks nor unlinks the parent's lock. It clears inherited ownership state and singleton + caches, then requires a new lock instance before acquiring. Fork-time construction replaces inherited cache mutexes + and omits an instance whose construction crossed into the child from the cache. If ``fstat()`` cannot establish the + identity, child cleanup skips the descriptor number because an earlier fork callback may have reused it. Descriptor + tracking also covers third-party backends that fail after assigning a descriptor. :pr:`652` - by :user:`gaborbernat`. +- Open a SQLite connection per outer acquisition and close it on final release, so a forked child can no longer close + its parent's handle. A child rejects inherited databases until ``exec()``, and a PyPy child rejects read-write locks + once the parent has used SQLite. :pr:`657` - by :user:`gaborbernat`. - Keep executor-backed lock operations alive until they finish when a caller cancels. Serialize acquisitions on the same async lock so cancellation rollback cannot release a later caller's hold. Roll back acquisitions that finish after - cancellation and surface release failures. SQLite read-write locks keep their state until rollback ends the transaction; - callers can retry cleanup with ``release(force=True)`` or another acquisition. :pr:`640` -- Reject negative and non-finite ``lifetime`` values during lock construction and assignment. :pr:`644` -- Read the saved Windows process-probe error before deciding that a soft-lock holder has exited. :pr:`645` + cancellation and surface release failures. SQLite read-write locks keep their state until rollback ends the + transaction; callers can retry cleanup with ``release(force=True)`` or another acquisition. :pr:`652` - by + :user:`gaborbernat`. +- Reject negative and non-finite ``lifetime`` values during lock construction and assignment. :pr:`644` - by + :user:`gaborbernat`. +- Read the saved Windows process-probe error before deciding that a soft-lock holder has exited. :pr:`645` - by + :user:`gaborbernat`. - Reject non-default lock options that a narrow subclass constructor cannot honor, and forward all options through - subclasses that accept arbitrary keyword arguments. :pr:`646` + subclasses that accept arbitrary keyword arguments. :pr:`646` - by :user:`gaborbernat`. - Release now removes the path identity that acquisition recorded instead of resolving a symlinked parent again. When - acquisition raises, its rollback no longer deletes the holder's deadlock record. :pr:`647` -- Remove the body error from implicit context on the release error before adding both errors to an exception group. :pr:`648` -- Relinquish a soft lock's descriptor before one close attempt so a later release cannot close a reused descriptor number, - and honor ``close_error_policy`` while still attempting marker cleanup. :pr:`649` + acquisition raises, its rollback no longer deletes the holder's deadlock record. :pr:`647` - by :user:`gaborbernat`. +- Remove the body error from implicit context on the release error before adding both errors to an exception group. + :pr:`648` - by :user:`gaborbernat`. +- Relinquish a soft lock's descriptor before one close attempt so a later release cannot close a reused descriptor + number, and honor ``close_error_policy`` while still attempting marker cleanup. :pr:`649` - by :user:`gaborbernat`. - Raise ``OSError(errno.ENOSYS)`` from descriptor locks when ``fcntl`` is unavailable, and reject invalid blocking poll - intervals before attempting a lock. :pr:`650` -- ``SoftFileLock`` and ``SoftFileLease`` record the holder's process start time on every platform and reclaim a stale marker only when the recorded owner is provably gone. - A reused PID on Unix no longer breaks a live lock or wedges acquisition, matching the recycled-PID detection Windows already had. :pr:`660` -- ``SoftReadWriteLock`` and ``SoftFileLease`` ride out a transient filesystem error during a heartbeat instead of dropping the lock or reporting a false compromise, and report a lost claim only once refresh cannot recover before the lease would lapse. - ``SoftFileLease`` also reclaims a malformed or partial marker that used to block every contender, and a Linux marker distinguishes a PID reused across a reboot. :pr:`661` -- ``BaseFileLock.__del__`` suppresses a release error rather than raising it during garbage collection, where it surfaced as an unraisable-exception warning attributed to unrelated code. :pr:`665` -- Correct the Unix lock-file cleanup and :func:`fcntl.flock` documentation. :pr:`623` -- Drop automated bot entries from the changelog and link its code references to the API docs and the Python standard library. :pr:`638` -- Refresh the documentation to the current code: a new trust-boundaries and ownership-scope section (same-UID boundary, advisory native locks, strict claims and lease fencing, a filesystem support matrix, migration off timed stale breaking), the lock-selection flowchart and comparison tables extended with ``StrictSoftFileLock`` and ``SoftFileLease``, and the cross-platform process start token replacing the Windows-only description throughout. :pr:`663` -- The filesystem support matrix records mutual exclusion measured across two independent client caches in CI: NFS (v4 and v3) for the native, soft, and strict locks, and SMB for the native and soft locks. :pr:`665` -- Build the changelog from towncrier news fragments, render pending fragments as an ``Unreleased`` docs section, and refuse to publish a tag the changelog does not document. :pr:`626` + intervals before attempting a lock. :pr:`650` - by :user:`gaborbernat`. +- ``SoftFileLock`` and ``SoftFileLease`` record the holder's process start time on every platform and reclaim a stale + marker only when the recorded owner is provably gone. A reused PID on Unix no longer breaks a live lock or wedges + acquisition, matching the recycled-PID detection Windows already had. :pr:`660` - by :user:`gaborbernat`. +- ``SoftReadWriteLock`` and ``SoftFileLease`` ride out a transient filesystem error during a heartbeat instead of + dropping the lock or reporting a false compromise, and report a lost claim only once refresh cannot recover before the + lease would lapse. ``SoftFileLease`` also reclaims a malformed or partial marker that used to block every contender, + and a Linux marker distinguishes a PID reused across a reboot. :pr:`661` - by :user:`gaborbernat`. +- ``BaseFileLock.__del__`` suppresses a release error rather than raising it during garbage collection, where it + surfaced as an unraisable-exception warning attributed to unrelated code. :pr:`665` - by :user:`gaborbernat`. -******************** +.. _release-3-30-0-documentation: + +Documentation +============= + +- Correct the Unix lock-file cleanup and :func:`fcntl.flock` documentation. :pr:`623` - by :user:`gaborbernat`. +- Refresh the documentation to the current code: a new trust-boundaries and ownership-scope section (same-UID boundary, + advisory native locks, strict claims and lease fencing, a filesystem support matrix, migration off timed stale + breaking), the lock-selection flowchart and comparison tables extended with ``StrictSoftFileLock`` and + ``SoftFileLease``, and the cross-platform process start token replacing the Windows-only description throughout. + :pr:`663` - by :user:`gaborbernat`. +- The filesystem support matrix records mutual exclusion measured across two independent client caches in CI: NFS (v4 + and v3) for the native, soft, and strict locks, and SMB for the native and soft locks. :pr:`665` - by + :user:`gaborbernat`. + +.. _release-3-30-0-security: + +Security +======== + +- Bind the Windows reparse-point check to the locked handle to close a symlink-swap race. :pr:`596` - by + :user:`gaborbernat`. + +.. _release-3-29-7: + +********************* 3.29.7 (2026-07-07) -******************** +********************* -- _util: drop the dead ``st_mtime=0`` short-circuit in ``raise_on_not_writable_file`` :pr:`589` - by :user:`HrachShah` -- soft_rw: evict a non-regular write marker without reading it :pr:`588` -- asyncio: detect cross-instance reentrant deadlocks before the poll loop :pr:`586` - by :user:`HrachShah` +.. _release-3-29-7-fixed: -******************** +Fixed +===== + +- Evict a non-regular write marker without reading it :pr:`588` - by :user:`gaborbernat`. +- Detect cross-instance reentrant deadlocks before the poll loop + https://github.com/tox-dev/filelock/commit/616400e382cdc9cc8c95823014e4d2d61a70095c - by :user:`HrachShah` + +.. _release-3-29-6: + +********************* 3.29.6 (2026-07-06) -******************** +********************* -- test: silence fork DeprecationWarning on 3.15 :pr:`585` -- _util: drop the dead ``st_mtime=0`` short-circuit in ``raise_on_not_writable_file`` :pr:`582` - by :user:`HrachShah` -- serialize singleton construction in ``FileLockMeta`` :pr:`581` - by :user:`dxbjavid` -- surface GitHub Sponsors and thanks.dev +.. _release-3-29-6-fixed: -******************** +Fixed +===== + +- Serialize singleton construction in ``FileLockMeta`` :pr:`581` - by :user:`dxbjavid` + +.. _release-3-29-5: + +********************* 3.29.5 (2026-07-02) -******************** +********************* -- lifetime: reject negative, non-numeric, and bool values at the setter :pr:`573` - by :user:`HrachShah` -- roll back a read acquire's open transaction when its SELECT fails :pr:`575` - by :user:`dxbjavid` -- keep Unix lock files after release :pr:`577` - by :user:`itscloud0` -- use a private break name in ``break_lock_file`` :pr:`576` - by :user:`dxbjavid` -- don't complete a writer acquire on a peer's reclaimed marker :pr:`571` - by :user:`dxbjavid` -- don't follow symlinks in ``raise_on_not_writable_file`` :pr:`567` - by :user:`dxbjavid` -- only unlink the writer marker on release if it is still ours :pr:`566` - by :user:`dxbjavid` -- don't follow symlinks when refreshing soft read/write lock markers :pr:`565` - by :user:`dxbjavid` -- serialize read/write release rollback against a concurrent acquire :pr:`563` - by :user:`dxbjavid` +.. _release-3-29-5-changes: -******************** +Changes +======= + +- Use a private break name in ``break_lock_file`` :pr:`576` - by :user:`dxbjavid` + +.. _release-3-29-5-fixed: + +Fixed +===== + +- Reject negative, non-numeric, and bool values at the setter + https://github.com/tox-dev/filelock/commit/be562277636140c26578a98a2a487fdb6f446ee6 - by :user:`HrachShah` +- Roll back a read acquire's open transaction when its SELECT fails :pr:`575` - by :user:`dxbjavid` +- Keep Unix lock files after release :pr:`577` - by :user:`itscloud0` +- Don't complete a writer acquire on a peer's reclaimed marker :pr:`571` - by :user:`dxbjavid` +- Don't follow symlinks in ``raise_on_not_writable_file`` :pr:`567` - by :user:`dxbjavid` +- Only unlink the writer marker on release if it is still ours :pr:`566` - by :user:`dxbjavid` +- Don't follow symlinks when refreshing soft read/write lock markers :pr:`565` - by :user:`dxbjavid` +- Serialize read/write release rollback against a concurrent acquire :pr:`563` - by :user:`dxbjavid` + +.. _release-3-29-4: + +********************* 3.29.4 (2026-06-13) -******************** +********************* -- keep the read/write heartbeat alive on a transient touch error :pr:`562` - by :user:`dxbjavid` -- verify inode in ``break_lock_file`` before unlinking a stale lock :pr:`561` - by :user:`dxbjavid` +.. _release-3-29-4-fixed: -******************** +Fixed +===== + +- Keep the read/write heartbeat alive on a transient touch error :pr:`562` - by :user:`dxbjavid` +- Verify inode in ``break_lock_file`` before unlinking a stale lock :pr:`561` - by :user:`dxbjavid` + +.. _release-3-29-3: + +********************* 3.29.3 (2026-06-10) -******************** +********************* -- ๐Ÿ› fix(ci): restore release environment on tag job :pr:`559` -- validate pid range in ``_parse_lock_holder`` :pr:`556` - by :user:`dxbjavid` -- ๐Ÿ”ง ci(release): publish to PyPI on tag push :pr:`557` +.. _release-3-29-3-fixed: -******************** +Fixed +===== + +- Validate pid range in ``_parse_lock_holder`` :pr:`556` - by :user:`dxbjavid` + +.. _release-3-29-2: + +********************* 3.29.2 (2026-06-10) -******************** +********************* -- check hostname in ``is_lock_held_by_us`` :pr:`553` - by :user:`dxbjavid` -- ๐Ÿ”’ fix(soft): harden stale-lock breaking and self-heal malformed locks :pr:`551` -- open marker reads non-blocking to refuse attacker-placed fifo :pr:`549` - by :user:`dxbjavid` +.. _release-3-29-2-fixed: -******************** +Fixed +===== + +- Check hostname in ``is_lock_held_by_us`` :pr:`553` - by :user:`dxbjavid` +- Recover malformed soft-lock markers without breaking a live lock. :pr:`551` - by :user:`gaborbernat`. + +.. _release-3-29-2-security: + +Security +======== + +- Open marker reads non-blocking to refuse attacker-placed fifo :pr:`549` - by :user:`dxbjavid` + +.. _release-3-29-1: + +********************* 3.29.1 (2026-06-03) -******************** +********************* -- ๐Ÿ› fix(soft): refuse to follow symlinks when reading the lock file :pr:`548` - by :user:`dxbjavid` -- chore: improve filelock maintenance path :pr:`545` - by :user:`lphuc2250gma` -- chore: improve filelock maintenance path :pr:`544` - by :user:`lphuc2250gma` -- chore: improve filelock maintenance path :pr:`542` - by :user:`lphuc2250gma` -- docs: clarify per-thread scope of :class:`~filelock.FileLock` configuration :pr:`543` - by :user:`Gares95` -- docs: fix API docs of :meth:`~filelock.BaseFileLock.release` :pr:`540` - by :user:`MrAnno` +.. _release-3-29-1-documentation: -******************** +Documentation +============= + +- Clarify per-thread scope of :class:`~filelock.FileLock` configuration :pr:`543` - by :user:`Gares95` +- Fix API docs of :meth:`~filelock.BaseFileLock.release` :pr:`540` - by :user:`MrAnno` + +.. _release-3-29-1-security: + +Security +======== + +- Refuse symlinks when reading a soft lock file. :pr:`548` - by :user:`dxbjavid`. + +.. _release-3-29-0: + +********************* 3.29.0 (2026-04-19) -******************** +********************* -- โœจ feat(soft): enable stale lock detection on Windows :pr:`534` -- ๐Ÿ› fix(async): use single-thread executor for lock consistency :pr:`533` +.. _release-3-29-0-added: -******************** +Added +===== + +- Detect stale soft locks on Windows. :pr:`534` - by :user:`gaborbernat`. + +.. _release-3-29-0-fixed: + +Fixed +===== + +- Use one executor thread for async lock operations to keep acquisition and release ordered. :pr:`533` - by + :user:`gaborbernat`. + +.. _release-3-28-0: + +********************* 3.28.0 (2026-04-14) -******************** +********************* -- ๐Ÿ› fix(ci): unbreak release workflow, publish to PyPI again :pr:`529` +.. _release-3-28-0-added: -******************** - 3.26.1 (2026-04-09) -******************** +Added +===== -- ๐Ÿ› fix(asyncio): add ``__exit__`` to :class:`~filelock.BaseAsyncFileLock` and fix ``__del__`` loop handling :pr:`518` - by :user:`naarob` +- Inspect a soft lock's owner PID and break stale locks. :pr:`524` - by :user:`gaborbernat`. +- Add :class:`~filelock.SoftReadWriteLock` for NFS and HPC clusters. :pr:`528` - by :user:`gaborbernat`. -******************** - 3.26.0 (2026-04-06) -******************** +.. _release-3-28-0-fixed: -- โœจ feat(soft): add PID inspection and lock breaking :pr:`524` -- Remove persist-credentials: false from release job :pr:`520` -- ๐Ÿ”’ ci(workflows): add zizmor security auditing :pr:`517` +Fixed +===== -******************** +- Handle ``__exit__`` and event-loop shutdown for :class:`~filelock.BaseAsyncFileLock`. :pr:`518` - by :user:`naarob`. + +.. _release-3-25-2: + +********************* 3.25.2 (2026-03-11) -******************** +********************* -- ๐Ÿ› fix(unix): suppress ``EIO`` on close in Docker bind mounts :pr:`513` +.. _release-3-25-2-fixed: -******************** +Fixed +===== + +- Suppress ``EIO`` when closing a Unix lock on a Docker bind mount. :pr:`513` - by :user:`gaborbernat`. + +.. _release-3-25-1: + +********************* 3.25.1 (2026-03-09) -******************** +********************* -- ๐Ÿ› fix(win): restore best-effort lock file cleanup on release :pr:`511` -- ๐Ÿ“ docs(logo): add branded project logo :pr:`507` +.. _release-3-25-1-fixed: -******************** +Fixed +===== + +- Restore best-effort lock file cleanup on Windows. :pr:`511` - by :user:`gaborbernat`. + +.. _release-3-25-1-documentation: + +Documentation +============= + +- Add the project logo to the documentation. :pr:`507` - by :user:`gaborbernat`. + +.. _release-3-25-0: + +********************* 3.25.0 (2026-03-01) -******************** +********************* -- โœจ feat(async): add :class:`~filelock.AsyncReadWriteLock` :pr:`506` -- Standardize .github files to .yaml suffix -- Move SECURITY.md to .github/SECURITY.md -- Add security policy -- Add permissions to check workflow :pr:`500` +.. _release-3-25-0-added: -******************** +Added +===== + +- Add :class:`~filelock.AsyncReadWriteLock`. :pr:`506` - by :user:`gaborbernat`. + +.. _release-3-25-0-fixed: + +Fixed +===== + +- Suppress :class:`ValueError` while checking a corrupt stale-lock marker. :pr:`496` - by :user:`bysiber` +- Fix nonblocking transaction-lock acquisition with a timeout. :pr:`498` - by :user:`gaborbernat` + +.. _release-3-24-3: + +********************* 3.24.3 (2026-02-19) -******************** +********************* -- ๐Ÿ› fix(unix): handle ``ENOENT`` race on FUSE/NFS during acquire :pr:`495` -- ๐Ÿ› fix(ci): add trailing blank line after changelog entries :pr:`492` +.. _release-3-24-3-fixed: -******************** +Fixed +===== + +- Handle an ``ENOENT`` race while acquiring a lock on FUSE or NFS. :pr:`495` - by :user:`gaborbernat`. + +.. _release-3-24-2: + +********************* 3.24.2 (2026-02-16) -******************** +********************* + +.. _release-3-24-2-fixed: + +Fixed +===== -- ๐Ÿ› fix(rw): close :mod:`sqlite3` cursors and skip :class:`~filelock.SoftFileLock` Windows race :pr:`491` -- ๐Ÿ› fix(test): resolve flaky write non-starvation test :pr:`490` -- ๐Ÿ“ docs: restructure using Diataxis framework :pr:`489` +- Close SQLite cursors and avoid a :class:`~filelock.SoftFileLock` race on Windows. :pr:`491` - by :user:`gaborbernat`. + +.. _release-3-24-2-documentation: + +Documentation +============= + +- Organize the documentation into tutorials, how-to guides, reference, and explanations. :pr:`489` - by + :user:`gaborbernat`. + +.. _release-3-24-1: ********************* 3.24.1 (2026-02-15) ********************* -- ๐Ÿ› fix(soft): resolve Windows deadlock and test race condition :pr:`488` +.. _release-3-24-1-fixed: + +Fixed +===== + +- Fix a soft-lock deadlock on Windows. :pr:`488` - by :user:`gaborbernat`. + +.. _release-3-24-0: ********************* 3.24.0 (2026-02-14) ********************* -- โœจ feat(lock): add ``lifetime`` parameter for lock expiration (#68) :pr:`486` -- โœจ feat(lock): add ``cancel_check`` to :meth:`~filelock.BaseFileLock.acquire` (#309) :pr:`487` -- ๐Ÿ› fix(api): detect same-thread self-deadlock :pr:`481` -- โœจ feat(mode): respect POSIX default ACLs (#378) :pr:`483` -- ๐Ÿ› fix(win): eliminate lock file race in threaded usage :pr:`484` -- โœจ feat(lock): add ``poll_interval`` to constructor :pr:`482` -- ๐Ÿ› fix(unix): auto-fallback to :class:`~filelock.SoftFileLock` on ``ENOSYS`` :pr:`480` +.. _release-3-24-0-added: + +Added +===== + +- Add a ``lifetime`` parameter for lock expiration (:issue:`68`). :pr:`486` - by :user:`gaborbernat`. +- Add ``cancel_check`` to :meth:`~filelock.BaseFileLock.acquire` (:issue:`309`). :pr:`487` - by :user:`gaborbernat`. +- Honor POSIX default ACLs when creating a lock file (:issue:`378`). :pr:`483` - by :user:`gaborbernat`. +- Add ``poll_interval`` to the lock constructor. :pr:`482` - by :user:`gaborbernat`. + +.. _release-3-24-0-fixed: + +Fixed +===== + +- Detect a same-thread self-deadlock. :pr:`481` - by :user:`gaborbernat`. +- Fix a lock file race between threads on Windows. :pr:`484` - by :user:`gaborbernat`. +- Fall back to :class:`~filelock.SoftFileLock` when native locking fails with ``ENOSYS``. :pr:`480` - by + :user:`gaborbernat`. + +.. _release-3-23-0: ********************* 3.23.0 (2026-02-14) ********************* -- ๐Ÿ“ docs: move from Unlicense to MIT :pr:`479` -- ๐Ÿ“ docs: add fasteners to similar libraries :pr:`478` +.. _release-3-23-0-changes: + +Changes +======= + +- Change the project license from Unlicense to MIT. :pr:`479` - by :user:`gaborbernat`. + +.. _release-3-23-0-documentation: + +Documentation +============= + +- List fasteners among the similar libraries in the documentation. :pr:`478` - by :user:`gaborbernat`. + +.. _release-3-22-0: ********************* 3.22.0 (2026-02-14) ********************* -- ๐Ÿ› fix(soft): skip stale detection on Windows :pr:`477` -- โœจ feat(soft): detect and break stale locks :pr:`476` +.. _release-3-22-0-added: + +Added +===== + +- Detect and break stale soft locks. :pr:`476` - by :user:`gaborbernat`. + +.. _release-3-22-0-fixed: + +Fixed +===== + +- Disable stale-lock detection on Windows until its implementation is safe. :pr:`477` - by :user:`gaborbernat`. + +.. _release-3-21-2: ********************* 3.21.2 (2026-02-13) ********************* -- ๐Ÿ› fix: catch :exc:`ImportError` for missing :mod:`sqlite3` C library :pr:`475` +.. _release-3-21-2-fixed: + +Fixed +===== + +- Handle :exc:`ImportError` when the :mod:`sqlite3` C library is unavailable. :pr:`475` - by :user:`gaborbernat`. + +.. _release-3-21-1: ********************* 3.21.1 (2026-02-12) ********************* -- ๐Ÿ› fix: gracefully handle missing :mod:`sqlite3` when importing :class:`~filelock.ReadWriteLock` :pr:`473` - by :user:`bayandin` -- ๐Ÿ› fix(ci): make release workflow robust +.. _release-3-21-1-fixed: + +Fixed +===== + +- Import :class:`~filelock.ReadWriteLock` when :mod:`sqlite3` is unavailable. :pr:`473` - by :user:`bayandin`. + +.. _release-3-21-0: ********************* 3.21.0 (2026-02-12) ********************* -- ๐Ÿ› fix(ci): make release workflow robust -- ๐Ÿ‘ท ci(release): commit changelog and use release config :pr:`472` -- ๐Ÿ‘ท ci(release): consolidate to two jobs :pr:`471` -- โœจ feat(unix): delete lock file on release :pr:`408` - by :user:`sbc100` -- โœจ feat(lock): add SQLite-based :class:`~filelock.ReadWriteLock` :pr:`399` - by :user:`leventov` -- ๐Ÿ”ง chore: modernize tooling and bump deps :pr:`470` +.. _release-3-20-4: + +********************** + v3.20.4 (2026-03-23) +********************** + +.. _release-3-20-4-added: + +Added +===== + +- Add a SQLite-backed :class:`~filelock.ReadWriteLock`. :pr:`399` - by :user:`leventov` +- Allow Unix lock files to be removed after release. :pr:`408` - by :user:`sbc100` + +.. _release-3-20-3: ********************** v3.20.3 (2026-01-09) ********************** -- Fix TOCTOU symlink vulnerability in :class:`~filelock.SoftFileLock` :pr:`465`. +.. _release-3-20-3-security: + +Security +======== + +- Fix TOCTOU symlink vulnerability in :class:`~filelock.SoftFileLock` :pr:`465` - by :user:`gaborbernat`. + +.. _release-3-20-2: ********************** v3.20.2 (2026-01-02) ********************** -- Support Unix systems without :data:`os.O_NOFOLLOW` :pr:`463`. +.. _release-3-20-2-added: + +Added +===== + +- Support Unix systems without :data:`os.O_NOFOLLOW` :pr:`463` - by :user:`mwilliamson`. + +.. _release-3-20-1: ********************** v3.20.1 (2025-12-15) ********************** -- Fix TOCTOU symlink vulnerability in lock file creation :pr:`461`. +.. _release-3-20-1-security: + +Security +======== + +- Fix TOCTOU symlink vulnerability in lock file creation :pr:`461` - by :user:`gaborbernat`. + +.. _release-3-20-0: ********************** v3.20.0 (2025-10-08) ********************** -- Add Python 3.14 support, drop 3.9 :pr:`448`. -- Add ``tox.toml`` to sdist :pr:`436`. +.. _release-3-20-0-added: + +Added +===== + +- Add Python 3.14 support, drop 3.9 :pr:`448` - by :user:`gaborbernat`. + +.. _release-3-19-1: ********************** v3.19.1 (2025-08-14) ********************** -- Increase test coverage :pr:`434`. +.. _release-3-19-1-added: -********************** - v3.19.0 (2025-08-13) -********************** +Added +===== + +- Add support for Python 3.14 :pr:`432` - by :user:`gaborbernat`. -- Add support for Python 3.14 :pr:`432`. +.. _release-3-18-0: ********************** v3.18.0 (2025-03-11) ********************** -- Support :mod:`fcntl` check on Emscripten :pr:`398`. -- Indicate that locks are exclusive/write locks :pr:`394`. +.. _release-3-18-0-added: + +Added +===== + +- Support :mod:`fcntl` check on Emscripten :pr:`398` - by :user:`juntyr`. + +.. _release-3-18-0-fixed: + +Fixed +===== + +- Indicate that locks are exclusive/write locks :pr:`394` - by :user:`bicarlsen`. + +.. _release-3-17-0: ********************** v3.17.0 (2025-01-21) ********************** +.. _release-3-17-0-added: + +Added +===== + - Drop Python 3.8 support :pr:`388`. +.. _release-3-16-1: + ********************** v3.16.1 (2024-09-17) ********************** -- CI improvements :pr:`362`. +.. _release-3-16-0: ********************** v3.16.0 (2024-09-07) ********************** -- Add Python 3.13 to CI :pr:`359`. +.. _release-3-16-0-added: + +Added +===== + +- Add Python 3.13 to CI :pr:`359` - by :user:`gaborbernat`. + +.. _release-3-15-4: ********************** v3.15.4 (2024-06-22) ********************** -- Pass ``file_lock`` as positional argument :pr:`347`. +.. _release-3-15-4-fixed: + +Fixed +===== + +- Pass ``file_lock`` as positional argument :pr:`347` - by :user:`kwist-sgr`. + +.. _release-3-15-3: ********************** v3.15.3 (2024-06-19) ********************** -- Fix ``TypeError: _CountedFileLock.__init__() got an unexpected keyword argument`` :pr:`345`. +.. _release-3-15-3-fixed: + +Fixed +===== + +- Fix ``TypeError: _CountedFileLock.__init__() got an unexpected keyword argument`` :pr:`345` - by :user:`kwist-sgr`. + +.. _release-3-15-2: ********************** v3.15.2 (2024-06-19) ********************** -- Use a metaclass to implement the singleton pattern :pr:`340`. +.. _release-3-15-2-changes: + +Changes +======= + +- Use a metaclass to implement the singleton pattern :pr:`340` - by :user:`kwist-sgr`. + +.. _release-3-15-1: ********************** v3.15.1 (2024-06-12) ********************** -- Restore ``__init__`` method; more robust initialization for singleton locks :pr:`338`. +.. _release-3-15-1-fixed: + +Fixed +===== + +- Restore ``__init__`` method; more robust initialization for singleton locks :pr:`338` - by :user:`ethanbb`. + +.. _release-3-15-0: ********************** v3.15.0 (2024-06-11) ********************** -- Add asyncio support :pr:`332`. -- Don't re-initialize :class:`~filelock.BaseFileLock` when returning existing singleton instance :pr:`334`. +.. _release-3-15-0-added: + +Added +===== + +- Add asyncio support :pr:`332` - by :user:`Ovizro`. + +.. _release-3-15-0-fixed: + +Fixed +===== + +- Don't re-initialize :class:`~filelock.BaseFileLock` when returning existing singleton instance :pr:`334` - by + :user:`ethanbb`. + +.. _release-3-14-0: ********************** v3.14.0 (2024-04-27) ********************** -- Add ``blocking`` parameter on lock constructor :pr:`325`. +.. _release-3-14-0-added: + +Added +===== + +- Add ``blocking`` parameter on lock constructor :pr:`325` - by :user:`iamkhav`. + +.. _release-3-13-4: ********************** v3.13.4 (2024-04-09) ********************** -- Raise error on incompatible singleton ``timeout`` and ``mode`` arguments :pr:`320`. +.. _release-3-13-4-fixed: + +Fixed +===== + +- Raise error on incompatible singleton ``timeout`` and ``mode`` arguments :pr:`320` - by :user:`nefrob`. + +.. _release-3-13-3: ********************** v3.13.3 (2024-03-25) ********************** -- Make singleton class instance dict unique per subclass :pr:`318`. +.. _release-3-13-3-changes: + +Changes +======= + +- Make singleton class instance dict unique per subclass :pr:`318` - by :user:`nefrob`. + +.. _release-3-13-2: ********************** v3.13.2 (2024-03-25) ********************** -- Fix permission denied error when lock file is placed in ``/tmp`` :pr:`317`. +.. _release-3-13-2-fixed: + +Fixed +===== + +- Fix permission denied error when lock file is placed in ``/tmp`` :pr:`317` - by :user:`kota-iizuka`. + +.. _release-3-13-1: ********************** v3.13.1 (2023-10-30) ********************** -- Allow users to subclass :class:`~filelock.FileLock` with custom keyword arguments :pr:`284`. +.. _release-3-13-1-added: + +Added +===== + +- Allow users to subclass :class:`~filelock.FileLock` with custom keyword arguments :pr:`284` - by :user:`hmaarrfk`. + +.. _release-3-13-0: ********************** v3.13.0 (2023-10-27) ********************** -- Support reentrant locking on lock file path via optional ``is_singleton`` instance :pr:`283`. +.. _release-3-13-0-added: + +Added +===== + +- Support reentrant locking on lock file path via optional ``is_singleton`` instance :pr:`283` - by :user:`nefrob`. + +.. _release-3-12-4: ********************** v3.12.4 (2023-09-13) ********************** -- Change ``typing-extensions`` to be installed only with the ``[typing]`` extra :pr:`276`. +.. _release-3-12-4-packaging: + +Packaging +========= + +- Change ``typing-extensions`` to be installed only with the ``[typing]`` extra :pr:`276` - by :user:`asottile`. + +.. _release-3-12-3: ********************** v3.12.3 (2023-08-28) ********************** -- Add ``tox.ini`` to sdist :pr:`265`. -- Create parent directories if necessary :pr:`254`. +.. _release-3-12-3-added: + +Added +===== + +- Create parent directories if necessary :pr:`254` - by :user:`gerlero`. + +.. _release-3-12-2: ********************** v3.12.2 (2023-06-12) ********************** -- Restore ``if TYPE_CHECKING`` syntax for :class:`~filelock.FileLock` definition :pr:`245`. +.. _release-3-12-2-fixed: + +Fixed +===== + +- Restore ``if TYPE_CHECKING`` syntax for :class:`~filelock.FileLock` definition :pr:`245` - by :user:`dlax`. + +.. _release-3-12-1: ********************** v3.12.1 (2023-06-10) ********************** -- Add Python 3.12 support :pr:`237`. -- Use ruff for linting :pr:`244`. +.. _release-3-12-1-added: + +Added +===== + +- Add Python 3.12 support :pr:`237` - by :user:`gaborbernat`. + +.. _release-3-12-0: ********************** v3.12.0 (2023-04-18) ********************** -- Make the thread local behavior something the caller can enable/disable via a flag during the lock creation, it's on by - default. -- Better error handling on Windows. +.. _release-3-12-0-changes: + +Changes +======= + +- Allow callers to disable thread-local lock state at construction; it remains enabled by default. :pr:`232` - by + :user:`csm10495`. + +.. _release-3-12-0-fixed: + +Fixed +===== + +- Fix a lock hang on Windows. :pr:`231` - by :user:`TheMatt2`. + +.. _release-3-11-0: ********************** v3.11.0 (2023-04-06) ********************** -- Make the lock thread local. +.. _release-3-11-0-changes: + +Changes +======= + +- Keep lock state local to each thread. :pr:`219` - by :user:`csm10495`. + +.. _release-3-10-7: ********************** v3.10.7 (2023-03-27) ********************** -- Use :func:`os.fchmod` instead of :func:`os.chmod` to work around bug in PyPy via Anaconda. +.. _release-3-10-7-fixed: + +Fixed +===== + +- Use :func:`os.fchmod` to avoid a :func:`os.chmod` failure on PyPy via Anaconda. :pr:`214` - by :user:`jfennick`. + +.. _release-3-10-6: ********************** v3.10.6 (2023-03-25) ********************** -- Enhance the robustness of the try/catch block in _soft.py. by :user:`jahrules`. +.. _release-3-10-6-fixed: + +Fixed +===== + +- Raise unexpected file-opening errors from :class:`~filelock.SoftFileLock` instead of repeatedly waiting for a lock + that cannot be acquired. :pr:`213` - by :user:`jahrules` and :user:`gaborbernat`. + +.. _release-3-10-5: ********************** v3.10.5 (2023-03-25) ********************** -- Add explicit error check as certain UNIX filesystems do not support flock. by :user:`jahrules`. +.. _release-3-10-5-added: + +Added +===== + +- Raise :class:`NotImplementedError` when a Unix filesystem does not support ``flock`` instead of waiting indefinitely. + :pr:`212` - by :user:`jahrules`. + +.. _release-3-10-4: ********************** v3.10.4 (2023-03-24) ********************** -- Update os.open to preserve mode= for certain edge cases. by :user:`jahrules`. +.. _release-3-10-4-fixed: + +Fixed +===== + +- Apply the requested file mode when creating a Unix lock file, including when the file cannot later be changed with + ``chmod``. :pr:`211` - by :user:`jahrules`. + +.. _release-3-10-3: ********************** v3.10.3 (2023-03-23) ********************** +.. _release-3-10-3-fixed: + +Fixed +===== + - Fix permission issue - by :user:`jahrules`. +.. _release-3-10-2: + ********************** v3.10.2 (2023-03-22) ********************** +.. _release-3-10-2-fixed: + +Fixed +===== + - Bug fix for using filelock with threaded programs causing undesired file permissions - by :user:`jahrules`. +.. _release-3-10-1: + ********************** v3.10.1 (2023-03-22) ********************** +.. _release-3-10-1-fixed: + +Fixed +===== + - Handle pickle for :class:`~filelock.Timeout` :pr:`203` - by :user:`TheMatt2`. +.. _release-3-10-0: + ********************** v3.10.0 (2023-03-15) ********************** +.. _release-3-10-0-added: + +Added +===== + - Add support for explicit file modes for lockfiles :pr:`192` - by :user:`jahrules`. +.. _release-3-9-1: + ********************* v3.9.1 (2023-03-14) ********************* -- Use :func:`time.perf_counter` instead of :func:`time.monotonic` for calculating timeouts. +.. _release-3-9-1-changes: + +Changes +======= + +- Use :func:`time.perf_counter` instead of :func:`time.monotonic` to calculate timeouts. :pr:`194` - by :user:`zpz`. + +.. _release-3-9-0: ********************* v3.9.0 (2022-12-28) ********************* -- Move build backend to ``hatchling`` :pr:`185` - by :user:`gaborbernat`. +.. _release-3-8-2: + +********************* + v3.8.2 (2022-12-05) +********************* + +.. _release-3-8-2-packaging: + +Packaging +========= + +- Update the published license classifier to Unlicense. :pr:`180` - by :user:`jond01` + +.. _release-3-8-1: ********************* v3.8.1 (2022-12-04) ********************* -- Fix mypy does not accept :class:`~filelock.FileLock` as a valid type +.. _release-3-8-1-fixed: + +Fixed +===== + +- Make :class:`~filelock.FileLock` valid in mypy type annotations. :pr:`177` - by :user:`gaborbernat`. + +.. _release-3-8-0: ********************* v3.8.0 (2022-12-04) ********************* -- Bump project dependencies -- Add timeout unit to docstrings -- Support 3.11 +.. _release-3-8-0-added: + +Added +===== + +- Add Python 3.11 support. :pr:`158` - by :user:`gaborbernat`. + +.. _release-3-8-0-documentation: + +Documentation +============= + +- State timeout units in the API docstrings. :pr:`148` - by :user:`jnordberg`. + +.. _release-3-7-1: ********************* v3.7.1 (2022-05-31) ********************* -- Make the readme documentation point to the index page +.. _release-3-7-1-documentation: + +Documentation +============= + +- Point README documentation links to the site index - by :user:`gaborbernat`. + +.. _release-3-7-0: ********************* v3.7.0 (2022-05-13) ********************* -- Add ability to return immediately when a lock cannot be obtained +.. _release-3-7-0-added: + +Added +===== + +- Allow a lock attempt to return immediately when the lock cannot be acquired. :pr:`142` - by :user:`myheroyuki`. + +.. _release-3-6-0: ********************* v3.6.0 (2022-02-17) ********************* -- Fix pylint warning "Abstract class :class:`~filelock.WindowsFileLock` with abstract methods - instantiated" :pr:`135` - by :user:`vonschultz` -- Fix pylint warning "Abstract class :class:`~filelock.UnixFileLock` with abstract methods instantiated" - :pr:`135` - by :user:`vonschultz` +.. _release-3-6-0-fixed: + +Fixed +===== + +- Fix pylint warning "Abstract class :class:`~filelock.WindowsFileLock` with abstract methods instantiated" :pr:`135` - + by :user:`vonschultz` +- Fix pylint warning "Abstract class :class:`~filelock.UnixFileLock` with abstract methods instantiated" :pr:`135` - by + :user:`vonschultz` + +.. _release-3-5-1: ********************* v3.5.1 (2022-02-16) ********************* -- Use :func:`time.monotonic` instead of :func:`time.time` for calculating timeouts. +.. _release-3-5-1-fixed: + +Fixed +===== + +- Use a monotonic clock for timeouts so wall-clock changes do not change their duration. :pr:`136` - by :user:`avian2`. + +.. _release-3-5-0: ********************* v3.5.0 (2022-02-15) ********************* -- Enable use as context decorator +.. _release-3-5-0-added: + +Added +===== + +- Allow a lock to be used as a context decorator. :pr:`128` - by Hirsch Alter. + +.. _release-3-4-2: ********************* v3.4.2 (2021-12-16) ********************* -- Drop support for python ``3.6`` +.. _release-3-4-2-added: + +Added +===== + +- Drop Python 3.6 support. :pr:`125` - by :user:`gaborbernat`. + +.. _release-3-4-1: ********************* v3.4.1 (2021-12-16) ********************* -- Add ``stacklevel`` to deprecation warnings for argument name change +.. _release-3-4-1-fixed: + +Fixed +===== + +- Point argument deprecation warnings at the caller by setting ``stacklevel``. :pr:`121` - by :user:`XuehaiPan`. + +.. _release-3-4-0: ********************* v3.4.0 (2021-11-16) ********************* -- Add correct spelling of poll interval parameter for :meth:`~filelock.BaseFileLock.acquire` method, raise - deprecation warning when using the misspelled form :pr:`119` - by :user:`XuehaiPan`. +.. _release-3-4-0-deprecated: + +Deprecated +========== + +- Add correct spelling of poll interval parameter for :meth:`~filelock.BaseFileLock.acquire` method, raise deprecation + warning when using the misspelled form :pr:`119` - by :user:`XuehaiPan`. + +.. _release-3-3-2: ********************* v3.3.2 (2021-10-29) ********************* -- Accept path types (like :class:`pathlib.Path` and :class:`pathlib.PurePath`) in the constructor for :class:`~filelock.FileLock` objects. +.. _release-3-3-2-added: + +Added +===== + +- Accept :class:`pathlib.Path` and :class:`pathlib.PurePath` in the :class:`~filelock.FileLock` constructor. :pr:`110` - + by :user:`f-koehler`. + +.. _release-3-3-1: ********************* v3.3.1 (2021-10-15) ********************* -- Add changelog to the documentation :pr:`108` - by :user:`gaborbernat` +.. _release-3-3-1-fixed: + +Fixed +===== + - Leave the log level of the ``filelock`` logger as not set (previously was set to warning) :pr:`108` - by :user:`gaborbernat` +.. _release-3-3-1-documentation: + +Documentation +============= + +- Add changelog to the documentation :pr:`108` - by :user:`gaborbernat` + +.. _release-3-3-0: + ********************* v3.3.0 (2021-10-03) ********************* -- Drop python 2.7 and 3.5 support, add type hints :pr:`100` - by :user:`gaborbernat` +.. _release-3-3-0-added: + +Added +===== + +- Drop Python 2.7 and 3.5 support. :pr:`100` - by :user:`gaborbernat`. +- Add type annotations. :pr:`100` - by :user:`gaborbernat`. + +.. _release-3-3-0-documentation: + +Documentation +============= + - Document asyncio support - by :user:`gaborbernat` -- fix typo :pr:`98` - by :user:`jugmac00` + +.. _release-3-2-1: ********************* v3.2.1 (2021-10-02) ********************* -- Improve documentation +.. _release-3-2-1-fixed: + +Fixed +===== + - Changed logger name from ``filelock._api`` to ``filelock`` :pr:`97` - by :user:`hkennyv` +.. _release-3-2-1-documentation: + +Documentation +============= + +- Add a tutorial covering lock acquisition, timeouts, and recursive use - by :user:`gaborbernat`. + +.. _release-3-2-0: + ********************* v3.2.0 (2021-09-30) ********************* -- Raise when trying to acquire in R/O or missing folder :pr:`96` - by :user:`gaborbernat` +.. _release-3-2-0-changes: + +Changes +======= + - Move lock acquire/release log from INFO to DEBUG :pr:`95` - by :user:`gaborbernat` -- Fix spelling and remove ignored flake8 checks - by :user:`gaborbernat` -- Split main module :pr:`94` - by :user:`gaborbernat` -- Move test suite to pytest :pr:`93` - by :user:`gaborbernat` + +.. _release-3-2-0-fixed: + +Fixed +===== + +- Raise when trying to acquire in R/O or missing folder :pr:`96` - by :user:`gaborbernat` + +.. _release-3-1-0: ********************* v3.1.0 (2021-09-27) ********************* -- Update links for new home at tox-dev :pr:`88` - by :user:`hugovk` -- Fixed link to LICENSE file :pr:`63` - by :user:`sharkwouter` -- Adopt tox-dev organization best practices :pr:`87` - by :user:`gaborbernat` -- Ownership moved from :user:`benediktschmitt` to the tox-dev organization (new primary maintainer :user:`gaborbernat`) +.. _release-3-1-0-documentation: + +Documentation +============= + +- Fix the LICENSE link. :pr:`63` - by :user:`sharkwouter`. +- Update links for the project's move to tox-dev. :pr:`88` - by :user:`hugovk`. + +.. _release-3-0-12: ********************** v3.0.12 (2019-05-18) ********************** -- *fixed* setuptools and twine/warehouse error by making the license only 1 line long -- *update* version for pypi upload -- *fixed* python2 setup error -- *added* test.py module to MANIFEST and made tests available in the setup commands :issue:`48` -- *fixed* documentation thanks to :user:`AnkurTank` :issue:`49` -- Update Trove classifiers for PyPI -- test: Skip test_del on PyPy since it hangs +.. _release-3-0-12-fixed: + +Fixed +===== + +- Fix package setup on Python 2 - by Benedikt Schmitt. + +.. _release-3-0-12-documentation: + +Documentation +============= + +- Correct the timeout documentation: zero means no wait, while a negative value waits indefinitely. :issue:`49` - by + Benedikt Schmitt; reported by :user:`AnkurTank`. + +.. _release-3-0-12-packaging: + +Packaging +========= + +- Fix package upload to PyPI by keeping the license summary on one line - by Benedikt Schmitt. +- Update the published PyPI classifiers - by :user:`hugovk`. + +.. _release-3-0-10: ********************** v3.0.10 (2018-11-01) ********************** -- Fix README rendering on PyPI +.. _release-3-0-10-documentation: + +Documentation +============= + +- Fix README rendering on PyPI - by :user:`adamchainz`. + +.. _release-3-0-9: ********************* v3.0.9 (2018-10-02) ********************* -- :pr:`38` from cottsay/shebang -- *updated* docs config for older sphinx compatibility -- *removed* misleading shebang from module +.. _release-3-0-8: ********************* v3.0.8 (2018-09-09) ********************* -- *updated* use setuptools - -********************* - v3.0.7 (2018-09-09) -********************* +.. _release-3-0-8-fixed: -- *fixed* garbage collection (:issue:`37`) -- *fix* travis ci badge (use rst not markdown) -- *changed* travis uri +Fixed +===== -********************* - v3.0.6 (2018-08-22) -********************* +- Fix a memory leak when acquiring locks on Python 2. :issue:`37` - by Benedikt Schmitt. -- *clean up* -- Fixed unit test for Python 2.7 -- Added Travis banner -- Added Travis CI support +.. _release-3-0-6: ********************* - v3.0.5 (2018-04-26) + v3.0.6 (2018-08-22) ********************* -- Corrected the prequel reference +.. _release-3-0-4: ********************* v3.0.4 (2018-02-01) ********************* -- *updated* README +.. _release-3-0-3: ********************* v3.0.3 (2018-01-30) ********************* -- *updated* readme +.. _release-3-0-3-documentation: + +Documentation +============= + +- Render the animated README example through a raw image URL on PyPI - by Benedikt Schmitt. + +.. _release-3-0-2: ********************* - v3.0.1 (2018-01-30) + v3.0.2 (2018-01-30) ********************* -- *updated* README (added navigation) -- *updated* documentation :issue:`22` -- *fix* the :class:`~filelock.SoftFileLock` test was influenced by the test for :class:`~filelock.FileLock` -- *undo* ``cb1d83d`` :issue:`31` +.. _release-3-0-2-fixed: + +Fixed +===== + +- Keep the Unix lock file after release to avoid a race between processes. :issue:`31` - by Benedikt Schmitt. + +.. _release-3-0-2-documentation: + +Documentation +============= + +- Add navigation to the README - by Benedikt Schmitt. +- Clarify when to use a separate lock file in the documentation. :issue:`22` - by Benedikt Schmitt. + +.. _release-3-0-0: ********************* v3.0.0 (2018-01-05) ********************* -- *updated* major version number due to :issue:`29` and :issue:`27` -- *fixed* use proper Python3 ``reraise`` method -- Attempting to clean up lock file on Unix after :meth:`~filelock.BaseFileLock.release` +.. _release-3-0-0-changes: + +Changes +======= + +- Remove the Unix lock file after :meth:`~filelock.BaseFileLock.release` - by :user:`spokorski`. + +.. _release-3-0-0-fixed: + +Fixed +===== + +- Reraise lock errors correctly on Python 3 - by Benedikt Schmitt. + +.. _release-2-0-13: ********************** v2.0.13 (2017-11-05) ********************** -- *changed* The logger is now acquired when first needed. :issue:`24` +.. _release-2-0-13-fixed: + +Fixed +===== + +- Acquire the logger only when first needed. :issue:`24` - by Benedikt Schmitt. + +.. _release-2-0-12: ********************** v2.0.12 (2017-09-02) ********************** -- correct spelling mistake +.. _release-2-0-11: ********************** v2.0.11 (2017-07-19) ********************** -- *added* official support for python 2 :issue:`20` +.. _release-2-0-11-added: + +Added +===== + +- Add official Python 2 support. :issue:`20` - by Benedikt Schmitt. + +.. _release-2-0-10: ********************** v2.0.10 (2017-06-07) ********************** -- *updated* readme +.. _release-2-0-9: ********************* v2.0.9 (2017-06-07) ********************* -- *updated* readme :issue:`19` -- *added* example :pr:`16` -- *updated* readthedocs url -- *updated* change order of the examples (:pr:`16`) +.. _release-2-0-9-fixed: + +Fixed +===== + +- Add example :pr:`16` - by :user:`pypingou`. +- Update change order of the examples (:pr:`16`) - by :user:`pypingou`. + +.. _release-2-0-9-documentation: + +Documentation +============= + +- Explain why the protected file needs a separate lock path. :issue:`19` - by Benedikt Schmitt. +- Correct the Read the Docs URL - by Benedikt Schmitt. + +.. _release-2-0-8: ********************* v2.0.8 (2017-01-24) ********************* -- Added logging -- Removed unused imports +.. _release-2-0-8-added: + +Added +===== + +- Add logging for lock operations - by :user:`expobrain`. + +.. _release-2-0-7: ********************* v2.0.7 (2016-11-05) ********************* -- *fixed* :issue:`14` (moved license and readme file to ``MANIFEST``) +.. _release-2-0-7-packaging: + +Packaging +========= + +- Keep the README and license out of the installed environment root while including them in the source archive. + :issue:`14` - by Benedikt Schmitt. + +.. _release-2-0-6: ********************* v2.0.6 (2016-05-01) ********************* -- *changed* unlocking sequence to fix transient test failures -- *changed* threads in tests so exceptions surface -- *added* test lock file cleanup +.. _release-2-0-6-fixed: + +Fixed +===== + +- Clear the lock descriptor before unlocking on Unix and Windows, avoiding a repeated release. - by :user:`cottsay`. + +.. _release-2-0-5: ********************* v2.0.5 (2015-11-11) ********************* -- Don't remove file after releasing lock -- *updated* docs +.. _release-2-0-5-fixed: -********************* - v2.0.4 (2015-07-29) -********************* +Fixed +===== -- *added* the new classes to ``__all__`` +- Keep the lock file after release - by :user:`kramarz`. -********************* - v2.0.3 (2015-07-29) -********************* +.. _release-2-0-5-added: + +Added +===== + +- Export :class:`~filelock.BaseFileLock` as a public class - by Benedikt Schmitt. -- *added* The :class:`~filelock.SoftFileLock` is now always tested +.. _release-2-0-5-documentation: + +Documentation +============= + +- Show the complete public API in the reference page - by Benedikt Schmitt. + +.. _release-2-0-4: ********************* - v2.0.2 (2015-07-29) + v2.0.4 (2015-07-29) ********************* -- The filelock classes are now always available and have been moved out of the ``if msvrct: ... elif fcntl ... else`` - clauses. +.. _release-2-0-4-added: + +Added +===== + +- Export the platform-specific lock classes consistently through ``__all__`` - by Benedikt Schmitt. + +.. _release-2-0-1: ********************* v2.0.1 (2015-06-13) ********************* -- fixed :issue:`5` -- *updated* test cases -- *updated* documentation -- *fixed* :issue:`2` which has been introduced with the lock counter +.. _release-2-0-1-fixed: + +Fixed +===== + +- Treat Windows lock contention as a timeout instead of raising ``IOError``. :issue:`5` - by Benedikt Schmitt. +- Avoid an extra acquisition for ``with lock.acquire(timeout=...)``, so one release frees the lock. :issue:`3` - by + Benedikt Schmitt. + +.. _release-2-0-1-documentation: + +Documentation +============= + +- Add a documentation site with recursive locking examples and an API reference - by Benedikt Schmitt. + +.. _release-2-0-0: ********************* v2.0.0 (2015-05-25) ********************* -- *added* default timeout (fixes :issue:`2`) +.. _release-2-0-0-added: + +Added +===== + +- Allow a default timeout at lock construction. :issue:`2` - by Benedikt Schmitt. + +.. _release-1-0-3: ********************* v1.0.3 (2015-04-22) ********************* -- *added* new test case, *fixed* unhandled exception +.. _release-1-0-3-fixed: + +Fixed +===== + +- Handle an exception while acquiring a lock - by Benedikt Schmitt. + +.. _release-1-0-2: ********************* v1.0.2 (2015-04-22) ********************* -- *fixed* a timeout could still be thrown if the lock is already acquired +.. _release-1-0-2-fixed: + +Fixed +===== + +- Do not time out when the same lock object already holds the lock - by Benedikt Schmitt. + +.. _release-1-0-1: ********************* v1.0.1 (2015-04-22) ********************* -- *fixed* :issue:`1` +.. _release-1-0-1-fixed: + +Fixed +===== + +- Return immediately after a successful first acquire instead of waiting one poll interval. :issue:`1` - by Benedikt + Schmitt. + +.. _release-1-0-0: ********************* v1.0.0 (2015-04-07) ********************* -- *added* lock counter, *added* unittest, *updated* to version 1 -- *changed* filenames -- *updated* version for pypi -- *updated* README, LICENSE (changed format from md to rst) -- *added* MANIFEST to gitignore -- *added* os independent file lock ; *changed* setup.py for pypi -- Update README.md -- initial version +.. _release-1-0-0-added: + +Added +===== + +- Add a lock counter so the same lock object can acquire recursively - by Benedikt Schmitt. + +.. _release-0-2-2: + +********************* + v0.2.2 (2015-03-25) +********************* + +.. _release-0-2-2-packaging: + +Packaging +========= + +- Include the renamed README and license files in the source distribution - by Benedikt Schmitt. + +.. _release-0-2-1: + +********************* + v0.2.1 (2015-03-25) +********************* + +.. _release-0-2-1-packaging: + +Packaging +========= + +- Publish the README and license as reStructuredText so package metadata can render them - by Benedikt Schmitt. + +.. _release-0-2-0: + +********************* + v0.2.0 (2014-07-06) +********************* + +.. _release-0-2-0-added: + +Added +===== + +- Add file locks for Unix and Windows - by Benedikt Schmitt. diff --git a/docs/changelog/template.jinja2 b/docs/changelog/template.jinja2 deleted file mode 100644 index c93bd221..00000000 --- a/docs/changelog/template.jinja2 +++ /dev/null @@ -1,13 +0,0 @@ -{% set header = versiondata.version + " (" + versiondata.date + ")" %} -{{ "*" * (header | length + 1) }} - {{ header }} -{{ "*" * (header | length + 1) }} - -{% for section, _ in sections.items() %} -{% for category, val in definitions.items() if category in sections[section] %} -{% for text, values in sections[section][category].items() %} -- {{ text }}{{ (" " + values | join(", ")) if values else "" }} -{% endfor %} -{% endfor %} -{% endfor %} -{{ "\n" }} diff --git a/docs/concepts.rst b/docs/concepts.rst index decf1c25..ce451824 100644 --- a/docs/concepts.rst +++ b/docs/concepts.rst @@ -534,6 +534,8 @@ of them deny direct access to the protected resource, and none of them defend ag cooperate. This section states, for each public lock, what it coordinates, what owns the claim, and what survives ``fork()``, descriptor duplication, task cancellation, and process exit. +.. _concepts:the same-uid boundary: + The same-UID boundary ===================== @@ -725,6 +727,8 @@ Why does SoftFileLock use a separate file instead of just checking a directory? A directory can't reliably be atomically created and deleted across platforms. A file can be created with ``O_EXCL`` (atomic, all-or-nothing) to detect conflicts. This is why SoftFileLock uses files. +.. _concepts:how does stale lock detection work across platforms?: + How does stale lock detection work across platforms? ==================================================== diff --git a/docs/conf.py b/docs/conf.py index 40ef7a39..200b1923 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -80,6 +80,7 @@ autodoc_mock_imports = ["_typeshed"] autodoc_default_options = {"member-order": "bysource", "undoc-members": True, "show-inheritance": True} autosectionlabel_prefix_document = True +autosectionlabel_maxdepth = 2 intersphinx_mapping = {"python": ("https://docs.python.org/3", None)} nitpicky = True diff --git a/pylock.toml b/pylock.toml new file mode 100644 index 00000000..6d1a0fae --- /dev/null +++ b/pylock.toml @@ -0,0 +1,3110 @@ +lock-version = "1.0" +environments = [ + "python_version == \"3.15\" and sys_platform == \"linux\" and platform_machine == \"x86_64\" and implementation_name == \"cpython\" and platform_python_implementation == \"CPython\" and platform_system == \"Linux\"", +] +requires-python = ">=3.11" +dependency-groups = [ + "build", + "coverage", + "dev", + "docs", + "fix", + "pkg-meta", + "project-runtime", + "release", + "release-news", + "release-notes", + "release-prepare", + "test", + "type", +] +default-groups = [ + "project-runtime", +] +created-by = "nab" + +[[packages]] +name = "accessible-pygments" +version = "0.0.5" +marker = "\"dev\" in dependency_groups or \"docs\" in dependency_groups or \"type\" in dependency_groups" +requires-python = ">=3.9" +dependencies = [ + { name = "pygments" }, +] +index = "https://pypi.org/simple/" + +[packages.sdist] +name = "accessible_pygments-0.0.5.tar.gz" +upload-time = 2024-05-10 11:23:10.216775+00:00 +url = "https://files.pythonhosted.org/packages/bc/c1/bbac6a50d02774f91572938964c582fff4270eee73ab822a4aeea4d8b11b/accessible_pygments-0.0.5.tar.gz" +size = 1377899 + +[packages.sdist.hashes] +sha256 = "40918d3e6a2b619ad424cb91e556bd3bd8865443d9f22f1dcdf79e33c8046872" + +[[packages.wheels]] +name = "accessible_pygments-0.0.5-py3-none-any.whl" +upload-time = 2024-05-10 11:23:08.421378+00:00 +url = "https://files.pythonhosted.org/packages/8d/3f/95338030883d8c8b91223b4e21744b04d11b161a3ef117295d8241f50ab4/accessible_pygments-0.0.5-py3-none-any.whl" +size = 1395903 + +[packages.wheels.hashes] +sha256 = "88ae3211e68a1d0b011504b2ffc1691feafce124b845bd072ab6f9f66f34d4b7" + +[[packages]] +name = "alabaster" +version = "1.0.0" +marker = "\"dev\" in dependency_groups or \"docs\" in dependency_groups or \"type\" in dependency_groups" +requires-python = ">=3.10" +index = "https://pypi.org/simple/" + +[packages.sdist] +name = "alabaster-1.0.0.tar.gz" +upload-time = 2024-07-26 18:15:03.762569+00:00 +url = "https://files.pythonhosted.org/packages/a6/f8/d9c74d0daf3f742840fd818d69cfae176fa332022fd44e3469487d5a9420/alabaster-1.0.0.tar.gz" +size = 24210 + +[packages.sdist.hashes] +sha256 = "c00dca57bca26fa62a6d7d0a9fcce65f3e026e9bfe33e9c538fd3fbb2144fd9e" + +[[packages.wheels]] +name = "alabaster-1.0.0-py3-none-any.whl" +upload-time = 2024-07-26 18:15:02.050654+00:00 +url = "https://files.pythonhosted.org/packages/7e/b3/6b4067be973ae96ba0d615946e314c5ae35f9f993eca561b356540bb0c2b/alabaster-1.0.0-py3-none-any.whl" +size = 13929 + +[packages.wheels.hashes] +sha256 = "fc6786402dc3fcb2de3cabd5fe455a2db534b371124f1f21de8731783dec828b" + +[[packages]] +name = "annotated-types" +version = "0.8.0" +marker = "\"dev\" in dependency_groups or \"pkg-meta\" in dependency_groups" +requires-python = ">=3.10" +index = "https://pypi.org/simple/" + +[packages.sdist] +name = "annotated_types-0.8.0.tar.gz" +upload-time = 2026-07-23 20:16:13.995706+00:00 +url = "https://files.pythonhosted.org/packages/5f/56/a8120250d128bed162cd73c76d45f6ef9991f3e068f62a8ee060afa3104a/annotated_types-0.8.0.tar.gz" +size = 15893 + +[packages.sdist.hashes] +sha256 = "13b2beaad985e05e2d6407ee4c4f35590b11f8d693a258a561055cac8f64cab7" + +[[packages.wheels]] +name = "annotated_types-0.8.0-py3-none-any.whl" +upload-time = 2026-07-23 20:16:12.938651+00:00 +url = "https://files.pythonhosted.org/packages/99/91/8acff4f5e50511b911bbccb72b8628a49c68ce14148cd9f6431094859a90/annotated_types-0.8.0-py3-none-any.whl" +size = 13427 + +[packages.wheels.hashes] +sha256 = "f072f4d804ea359e4eaf198b1af7a8b0943881a87f31bb764f8bf219bb9419e0" + +[[packages]] +name = "attrs" +version = "26.1.0" +marker = "\"dev\" in dependency_groups or \"pkg-meta\" in dependency_groups" +requires-python = ">=3.9" +index = "https://pypi.org/simple/" + +[packages.sdist] +name = "attrs-26.1.0.tar.gz" +upload-time = 2026-03-19 14:22:25.026315+00:00 +url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz" +size = 952055 + +[packages.sdist.hashes] +sha256 = "d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32" + +[[packages.wheels]] +name = "attrs-26.1.0-py3-none-any.whl" +upload-time = 2026-03-19 14:22:23.645947+00:00 +url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl" +size = 67548 + +[packages.wheels.hashes] +sha256 = "c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309" + +[[packages]] +name = "babel" +version = "2.18.0" +marker = "\"dev\" in dependency_groups or \"docs\" in dependency_groups or \"type\" in dependency_groups" +requires-python = ">=3.8" +index = "https://pypi.org/simple/" + +[packages.sdist] +name = "babel-2.18.0.tar.gz" +upload-time = 2026-02-01 12:30:56.078194+00:00 +url = "https://files.pythonhosted.org/packages/7d/b2/51899539b6ceeeb420d40ed3cd4b7a40519404f9baf3d4ac99dc413a834b/babel-2.18.0.tar.gz" +size = 9959554 + +[packages.sdist.hashes] +sha256 = "b80b99a14bd085fcacfa15c9165f651fbb3406e66cc603abf11c5750937c992d" + +[[packages.wheels]] +name = "babel-2.18.0-py3-none-any.whl" +upload-time = 2026-02-01 12:30:53.445172+00:00 +url = "https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl" +size = 10196845 + +[packages.wheels.hashes] +sha256 = "e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35" + +[[packages]] +name = "beautifulsoup4" +version = "4.15.0" +marker = "\"dev\" in dependency_groups or \"docs\" in dependency_groups or \"type\" in dependency_groups" +requires-python = ">=3.7.0" +dependencies = [ + { name = "soupsieve" }, + { name = "typing-extensions" }, +] +index = "https://pypi.org/simple/" + +[packages.sdist] +name = "beautifulsoup4-4.15.0.tar.gz" +upload-time = 2026-06-07 16:44:20.453113+00:00 +url = "https://files.pythonhosted.org/packages/43/65/318323f98dbee45d42dff61d8f047181bc6f2268a9068cfad035a46be5af/beautifulsoup4-4.15.0.tar.gz" +size = 632571 + +[packages.sdist.hashes] +sha256 = "288e3ca7d54b06f2ac191970bc275c1939cb46d450b255bf6718b04aa37ab4f7" + +[[packages.wheels]] +name = "beautifulsoup4-4.15.0-py3-none-any.whl" +upload-time = 2026-06-07 16:44:21.566528+00:00 +url = "https://files.pythonhosted.org/packages/88/c6/92fcd42f1ba33e1184263f25bfabf3d27c383410470f169e4b8163bf9c17/beautifulsoup4-4.15.0-py3-none-any.whl" +size = 109924 + +[packages.wheels.hashes] +sha256 = "d6f88de62e1d4e38ecb1077eb9724cd0eff29d2a08ca16a401e9b9e93f117cf9" + +[[packages]] +name = "certifi" +version = "2026.10.18" +marker = "\"dev\" in dependency_groups or \"docs\" in dependency_groups or \"pkg-meta\" in dependency_groups or \"type\" in dependency_groups" +requires-python = ">=3.7" +index = "https://pypi.org/simple/" + +[packages.sdist] +name = "certifi-2026.10.18.tar.gz" +upload-time = 2026-10-18 16:21:07.655710+00:00 +url = "https://files.pythonhosted.org/packages/7e/eb/a2aa1ac3ea30dd3f30ca57f40232138b6b0040719316ecb85daf61167771/certifi-2026.10.18.tar.gz" +size = 135227 + +[packages.sdist.hashes] +sha256 = "b9995d97b64d0431a5af837d8ca8cd6238104d1f319f04bd93ea43124d6c861e" + +[[packages.wheels]] +name = "certifi-2026.10.18-py3-none-any.whl" +upload-time = 2026-10-18 16:21:06.276884+00:00 +url = "https://files.pythonhosted.org/packages/08/ba/95dd616042484f7c83a06974388b37b837732740ee70bc5dc715b5fcd55a/certifi-2026.10.18-py3-none-any.whl" +size = 134093 + +[packages.wheels.hashes] +sha256 = "73191c47aaa79da394e8fee2dbc2ec827432cab56311122c9e47abf79e390f62" + +[[packages]] +name = "cffi" +version = "2.1.1" +marker = "\"dev\" in dependency_groups or \"pkg-meta\" in dependency_groups" +requires-python = ">=3.10" +dependencies = [ + { name = "pycparser" }, +] +index = "https://pypi.org/simple/" + +[packages.sdist] +name = "cffi-2.1.1.tar.gz" +upload-time = 2026-08-03 21:21:18.939988+00:00 +url = "https://files.pythonhosted.org/packages/9e/ef/008a1939e372c06329a3fce4279c02f328488f3526744906eeec3da7ad5f/cffi-2.1.1.tar.gz" +size = 530807 + +[packages.sdist.hashes] +sha256 = "dd31f52ea1086513bb9df30f8fcee9b8918323ae067a3d5b78bc826a000712be" + +[[packages.wheels]] +name = "cffi-2.1.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl" +upload-time = 2026-08-03 21:20:57.336082+00:00 +url = "https://files.pythonhosted.org/packages/44/16/29e6d01b388bef055ecd6ca8244b3f4d336bd09e92d5d892187b9601084e/cffi-2.1.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl" +size = 221630 + +[packages.wheels.hashes] +sha256 = "aeae0e330c9f6acd681f647d46cefd30c29f93e3392882e792e82080c9691399" + +[[packages]] +name = "cfgv" +version = "3.5.0" +marker = "\"dev\" in dependency_groups or \"fix\" in dependency_groups" +requires-python = ">=3.10" +index = "https://pypi.org/simple/" + +[packages.sdist] +name = "cfgv-3.5.0.tar.gz" +upload-time = 2025-11-19 20:55:51.612035+00:00 +url = "https://files.pythonhosted.org/packages/4e/b5/721b8799b04bf9afe054a3899c6cf4e880fcf8563cc71c15610242490a0c/cfgv-3.5.0.tar.gz" +size = 7334 + +[packages.sdist.hashes] +sha256 = "d5b1034354820651caa73ede66a6294d6e95c1b00acc5e9b098e917404669132" + +[[packages.wheels]] +name = "cfgv-3.5.0-py2.py3-none-any.whl" +upload-time = 2025-11-19 20:55:50.744331+00:00 +url = "https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl" +size = 7445 + +[packages.wheels.hashes] +sha256 = "a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0" + +[[packages]] +name = "chardet" +version = "7.7.0" +marker = "\"coverage\" in dependency_groups or \"dev\" in dependency_groups or \"test\" in dependency_groups or \"type\" in dependency_groups" +requires-python = ">=3.10" +index = "https://pypi.org/simple/" + +[packages.sdist] +name = "chardet-7.7.0.tar.gz" +upload-time = 2026-10-18 00:06:25.352198+00:00 +url = "https://files.pythonhosted.org/packages/47/92/3d893ac5d338b9a9af34c5b517d77492dcea8a10254254eead4215a96f3f/chardet-7.7.0.tar.gz" +size = 1008480 + +[packages.sdist.hashes] +sha256 = "0ba38a05201b0abf7570c12d8af45c30b2af835bd5cc52bfc7105082eda82c1e" + +[[packages.wheels]] +name = "chardet-7.7.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl" +upload-time = 2026-10-18 00:06:13.977728+00:00 +url = "https://files.pythonhosted.org/packages/f9/12/2b3a006ae31a7ab4ccac11a00abb27af23b4a5d57ddb00e937375e747a2a/chardet-7.7.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl" +size = 1660352 + +[packages.wheels.hashes] +sha256 = "42c4d8a2dc8abcc067417adc13c2f64f48482e48ce48e3af769b5a44214e5a5c" + +[[packages.wheels]] +name = "chardet-7.7.0-py3-none-any.whl" +upload-time = 2026-10-18 00:06:24.018863+00:00 +url = "https://files.pythonhosted.org/packages/cc/75/88a3eb36264375ff951bd2f9397c409727ab6aa6a18e1bd933851571d421/chardet-7.7.0-py3-none-any.whl" +size = 706680 + +[packages.wheels.hashes] +sha256 = "11cf9b719ea3675a2a7d8a3071bbe4d7ea24b26bfe39a51588d515ddd05cacad" + +[[packages]] +name = "charset-normalizer" +version = "3.5.2" +marker = "\"dev\" in dependency_groups or \"docs\" in dependency_groups or \"pkg-meta\" in dependency_groups or \"type\" in dependency_groups" +requires-python = ">=3.7" +index = "https://pypi.org/simple/" + +[packages.sdist] +name = "charset_normalizer-3.5.2.tar.gz" +upload-time = 2026-09-30 04:39:23.398219+00:00 +url = "https://files.pythonhosted.org/packages/33/1c/f41d4e74c28ab327ff3acd36053f7ea506c55872d7a90b0fa71aa3ab0c89/charset_normalizer-3.5.2.tar.gz" +size = 172659 + +[packages.sdist.hashes] +sha256 = "39de2a259fc954455c57274dc94c79d5842774e1247a016aff30bc0efed0f4ef" + +[[packages.wheels]] +name = "charset_normalizer-3.5.2-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl" +upload-time = 2026-09-30 04:37:37.397915+00:00 +url = "https://files.pythonhosted.org/packages/a0/a1/4fbf5d0f0f1b2a080474c1cf9a2f12c4c6531bb0e8ba591055e846d2b4e9/charset_normalizer-3.5.2-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl" +size = 257336 + +[packages.wheels.hashes] +sha256 = "3df041de8887954562c9b261cba85ca0e9ded74048daf125f45edcfaa4832229" + +[[packages.wheels]] +name = "charset_normalizer-3.5.2-cp37-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl" +upload-time = 2026-09-30 04:38:28.032254+00:00 +url = "https://files.pythonhosted.org/packages/7e/84/371eac6b30bdbcbf2d632a1a01809103459216fcaae61b8b8d922c1bfb8a/charset_normalizer-3.5.2-cp37-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl" +size = 253276 + +[packages.wheels.hashes] +sha256 = "1c50fe28bbc2ced33386f298650d91218076c05420e6cbd790b913adc41659e7" + +[[packages.wheels]] +name = "charset_normalizer-3.5.2-py3-none-any.whl" +upload-time = 2026-09-30 04:39:21.828049+00:00 +url = "https://files.pythonhosted.org/packages/fc/ad/d07d7862a62ffa6d79d68074d14823243dd235a77c45262acbf6adeb28bf/charset_normalizer-3.5.2-py3-none-any.whl" +size = 68872 + +[packages.wheels.hashes] +sha256 = "b6b751274acb69d77b3323d6b7dbaa3c7fdfc1eb829b7eb61d262f32e1af9685" + +[[packages]] +name = "check-wheel-contents" +version = "0.6.3" +marker = "\"dev\" in dependency_groups or \"pkg-meta\" in dependency_groups" +requires-python = ">=3.10" +dependencies = [ + { name = "attrs" }, + { name = "click" }, + { name = "packaging" }, + { name = "pydantic" }, + { name = "wheel-filename" }, +] +index = "https://pypi.org/simple/" + +[packages.sdist] +name = "check_wheel_contents-0.6.3.tar.gz" +upload-time = 2025-08-02 14:01:45.546952+00:00 +url = "https://files.pythonhosted.org/packages/95/37/2b9e0d38c2f668791ffe8b97711185c364d91c027e47f189c371c2348d18/check_wheel_contents-0.6.3.tar.gz" +size = 586023 + +[packages.sdist.hashes] +sha256 = "10e6939e2fe4e6ce1edf2ff6ec6157808677e80782e78021ae139dd88473a442" + +[[packages.wheels]] +name = "check_wheel_contents-0.6.3-py3-none-any.whl" +upload-time = 2025-08-02 14:01:43.968164+00:00 +url = "https://files.pythonhosted.org/packages/be/05/f39fde9f31ef80b285ef5822fad4ddabf73fec62a1f02c5beb4b2f328972/check_wheel_contents-0.6.3-py3-none-any.whl" +size = 27541 + +[packages.wheels.hashes] +sha256 = "5ae39c8c434b972f0740d04610759168590713175aab584b012b1b84f6771874" + +[[packages]] +name = "click" +version = "8.5.0" +marker = "\"dev\" in dependency_groups or \"docs\" in dependency_groups or \"pkg-meta\" in dependency_groups or \"release\" in dependency_groups or \"release-news\" in dependency_groups or \"release-notes\" in dependency_groups or \"release-prepare\" in dependency_groups or \"type\" in dependency_groups" +requires-python = ">=3.10" +index = "https://pypi.org/simple/" + +[packages.sdist] +name = "click-8.5.0.tar.gz" +upload-time = 2026-08-26 13:33:14.560531+00:00 +url = "https://files.pythonhosted.org/packages/c7/0e/7fa0ef50764b67090eca4114772a2abf8b6148198475e54c660b97caeee6/click-8.5.0.tar.gz" +size = 382235 + +[packages.sdist.hashes] +sha256 = "ba0d2089de75ea0310e2dde03160e6ca10009947fb95a182f9b54021bb272e34" + +[[packages.wheels]] +name = "click-8.5.0-py3-none-any.whl" +upload-time = 2026-08-26 13:33:12.928043+00:00 +url = "https://files.pythonhosted.org/packages/58/50/6c0d534c5f134586a8e1ba4e330569e32f057e33372ae556463212fb4cd3/click-8.5.0-py3-none-any.whl" +size = 125251 + +[packages.wheels.hashes] +sha256 = "255bc9599cf7748b4b1a446ccc735421bd08a2ae529a8b88597d3de5664ee360" + +[[packages]] +name = "covdefaults" +version = "2.3.0" +marker = "\"coverage\" in dependency_groups or \"dev\" in dependency_groups or \"test\" in dependency_groups or \"type\" in dependency_groups" +requires-python = ">=3.7" +dependencies = [ + { name = "coverage" }, +] +index = "https://pypi.org/simple/" + +[packages.sdist] +name = "covdefaults-2.3.0.tar.gz" +upload-time = 2023-03-05 16:43:34.779146+00:00 +url = "https://files.pythonhosted.org/packages/44/ee/9a6f2611f72e4c5657ae5542a510cf4164d2c673687c0ea73bb1cbd85b4d/covdefaults-2.3.0.tar.gz" +size = 4835 + +[packages.sdist.hashes] +sha256 = "4e99f679f12d792bc62e5510fa3eb59546ed47bd569e36e4fddc4081c9c3ebf7" + +[[packages.wheels]] +name = "covdefaults-2.3.0-py2.py3-none-any.whl" +upload-time = 2023-03-05 16:43:33.245581+00:00 +url = "https://files.pythonhosted.org/packages/76/4c/823bc951445aa97e5a1b7e337690db3abf85212c8d138e170922e7916ac8/covdefaults-2.3.0-py2.py3-none-any.whl" +size = 5144 + +[packages.wheels.hashes] +sha256 = "2832961f6ffcfe4b57c338bc3418a3526f495c26fb9c54565409c5532f7c41be" + +[[packages]] +name = "coverage" +version = "7.16.2" +marker = "\"coverage\" in dependency_groups or \"dev\" in dependency_groups or \"test\" in dependency_groups or \"type\" in dependency_groups" +requires-python = ">=3.10" +index = "https://pypi.org/simple/" + +[packages.sdist] +name = "coverage-7.16.2.tar.gz" +upload-time = 2026-09-27 12:29:01.118768+00:00 +url = "https://files.pythonhosted.org/packages/2f/55/d1eaf3e73781174340a00dc1ba2aee8a65f82fadb18e2797b192b6b3925b/coverage-7.16.2.tar.gz" +size = 971999 + +[packages.sdist.hashes] +sha256 = "ca64d9f1f384f151b9511bec01126072acd2f313439f8ed015a22d8790aab6fa" + +[[packages.wheels]] +name = "coverage-7.16.2-cp315-cp315-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl" +upload-time = 2026-09-27 12:28:05.150722+00:00 +url = "https://files.pythonhosted.org/packages/de/29/48fca82a7ebf7ff7b2e35019cc9537e7f65e4d2aa1215cc5a8792c989251/coverage-7.16.2-cp315-cp315-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl" +size = 259106 + +[packages.wheels.hashes] +sha256 = "8fc15cc8d0d06e873c00ef18e1372d605f9aaf3de27d8c24e50782e75bc8b843" + +[[packages.wheels]] +name = "coverage-7.16.2-py3-none-any.whl" +upload-time = 2026-09-27 12:28:59.075376+00:00 +url = "https://files.pythonhosted.org/packages/3f/0c/7a64e1ac90541a8edf50daef0914848011fb057a5bf55284a4811e21939a/coverage-7.16.2-py3-none-any.whl" +size = 215754 + +[packages.wheels.hashes] +sha256 = "11d28e9123a9156cb405d8d27b44256c9a58fb5decc2073a8f17862057e3aa0f" + +[[packages]] +name = "cryptography" +version = "50.0.2" +marker = "\"dev\" in dependency_groups or \"pkg-meta\" in dependency_groups" +requires-python = "!=3.9.0,!=3.9.1,>=3.9" +dependencies = [ + { name = "cffi" }, +] +index = "https://pypi.org/simple/" + +[packages.sdist] +name = "cryptography-50.0.2.tar.gz" +upload-time = 2026-09-30 15:30:04.884601+00:00 +url = "https://files.pythonhosted.org/packages/9d/af/182eb91b0df3fe75c4d9f26fe70684569566745f6ba7e5c9c73a862c5252/cryptography-50.0.2.tar.gz" +size = 880623 + +[packages.sdist.hashes] +sha256 = "7b46165bb56eb4704e2eaaf86f3c940d19154535d9b0ca7d6d590b04060e00d5" + +[[packages.wheels]] +name = "cryptography-50.0.2-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl" +upload-time = 2026-09-30 14:43:49.010540+00:00 +url = "https://files.pythonhosted.org/packages/21/69/64cef1f702bf6657e0cc186ed1a2891d50d29fb41586b254e1c07adea261/cryptography-50.0.2-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl" +size = 4719841 + +[packages.wheels.hashes] +sha256 = "630ebfea3bf689d075f82316324ff7433dc447fe6bc1bfc76524b74b4a9567d2" + +[[packages.wheels]] +name = "cryptography-50.0.2-cp311-abi3-manylinux_2_28_x86_64.whl" +upload-time = 2026-09-30 14:43:55.272150+00:00 +url = "https://files.pythonhosted.org/packages/1a/f1/b474e930c4d910328780e3940da76f5aa5cbc48ce1fc14e44d239d9ea9db/cryptography-50.0.2-cp311-abi3-manylinux_2_28_x86_64.whl" +size = 4753050 + +[packages.wheels.hashes] +sha256 = "4061c0079120205fb760c58acab6443e217307dcf05e3702cf970e0689972856" + +[[packages.wheels]] +name = "cryptography-50.0.2-cp311-abi3-manylinux_2_34_x86_64.whl" +upload-time = 2026-09-30 14:44:04.545137+00:00 +url = "https://files.pythonhosted.org/packages/d3/69/2c833a049475e0a3444e94c7d0aca0aa51d166374a449b09e92ac98138de/cryptography-50.0.2-cp311-abi3-manylinux_2_34_x86_64.whl" +size = 4752576 + +[packages.wheels.hashes] +sha256 = "9dab55f57c74c3cad24c323bacbbd04be4705ba6eb0d92e920b1fc4837ed5079" + +[[packages.wheels]] +name = "cryptography-50.0.2-cp315-abi3.abi3t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl" +upload-time = 2026-09-30 14:44:45.769143+00:00 +url = "https://files.pythonhosted.org/packages/aa/a4/2c8d734e43d97f0842ee9f1b7b4bfb3d0cf5e19edebf43c2afe6675c2320/cryptography-50.0.2-cp315-abi3.abi3t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl" +size = 4966465 + +[packages.wheels.hashes] +sha256 = "f2f9bd7f90c64fe89253f0a2c05e3c4856072660429ce8831b4235bf29403a67" + +[[packages.wheels]] +name = "cryptography-50.0.2-cp315-abi3.abi3t-manylinux_2_28_x86_64.whl" +upload-time = 2026-09-30 14:44:53.379605+00:00 +url = "https://files.pythonhosted.org/packages/02/a8/8df951850d6b31d2a00218f19e2b3f999523437ed7a819df7fa427942fca/cryptography-50.0.2-cp315-abi3.abi3t-manylinux_2_28_x86_64.whl" +size = 5001199 + +[packages.wheels.hashes] +sha256 = "58a0c478eeca76fe5e07993c5a0703def34a6dc6a0cda4f5564639b33112ffe7" + +[[packages.wheels]] +name = "cryptography-50.0.2-cp315-abi3.abi3t-manylinux_2_34_x86_64.whl" +upload-time = 2026-09-30 14:45:05.009031+00:00 +url = "https://files.pythonhosted.org/packages/24/95/83378121ef3eaaaf71d4b781577ff794acb39b9e1b87a3f156898c8497ed/cryptography-50.0.2-cp315-abi3.abi3t-manylinux_2_34_x86_64.whl" +size = 5000859 + +[packages.wheels.hashes] +sha256 = "e105ab60406787da31fccc883fc0f733af1efd78f0136a4599692c4083a73d0c" + +[[packages.wheels]] +name = "cryptography-50.0.2-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl" +upload-time = 2026-09-30 15:29:26.807001+00:00 +url = "https://files.pythonhosted.org/packages/bf/f1/1c1f6874e8550cfddd4b688ceb38cefb6ed15ceed224d56f133f3d88c214/cryptography-50.0.2-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl" +size = 4749145 + +[packages.wheels.hashes] +sha256 = "ee247f5c245c9a2fe7c8e2214e295918838e44e00a45a6718451e4004219e767" + +[[packages.wheels]] +name = "cryptography-50.0.2-cp39-abi3-manylinux_2_28_x86_64.whl" +upload-time = 2026-09-30 15:29:32.605292+00:00 +url = "https://files.pythonhosted.org/packages/4f/87/ef344a9e616871f2519c22d6afcda79ddd5d35e9592d95eb6e677608d055/cryptography-50.0.2-cp39-abi3-manylinux_2_28_x86_64.whl" +size = 4781387 + +[packages.wheels.hashes] +sha256 = "f21e8a22c8605750c7af886bab299a363721264061b4ac0a30efb73cfd58efc5" + +[[packages.wheels]] +name = "cryptography-50.0.2-cp39-abi3-manylinux_2_34_x86_64.whl" +upload-time = 2026-09-30 15:29:41.251208+00:00 +url = "https://files.pythonhosted.org/packages/40/cf/6b5c8e2fd9202d98988ab7cb5cc5c991704c4ad55f492ff408e4969f83f1/cryptography-50.0.2-cp39-abi3-manylinux_2_34_x86_64.whl" +size = 4780973 + +[packages.wheels.hashes] +sha256 = "1981f1db4630889b9ef7803fadef12b056f428cb6b85c27ba57b774793b6093c" + +[[packages]] +name = "cython" +version = "3.3.0" +marker = "\"build\" in dependency_groups" +requires-python = ">=3.9" +index = "https://pypi.org/simple/" + +[packages.sdist] +name = "cython-3.3.0.tar.gz" +upload-time = 2026-08-22 05:16:39.493040+00:00 +url = "https://files.pythonhosted.org/packages/a9/d8/4981ef716ad0e3ff0d3ef383aefc6b03c4a88dee33b272bf8e0d833001ca/cython-3.3.0.tar.gz" +size = 3727515 + +[packages.sdist.hashes] +sha256 = "eed0d93fbca7087f143b42c34b05a825849bdf17f101572c2105acfa49aa88b8" + +[[packages.wheels]] +name = "cython-3.3.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl" +upload-time = 2026-08-22 05:17:36.538454+00:00 +url = "https://files.pythonhosted.org/packages/c9/aa/4c0b6773ecf6bc1ec6cda7db8312a566611b330fb6dae87d740e44a47822/cython-3.3.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl" +size = 3454285 + +[packages.wheels.hashes] +sha256 = "b447f6906e0555f05dc4742ef1f99091b1e5d9aa9f16616e772fbf9ff6271616" + +[[packages.wheels]] +name = "cython-3.3.0-py3-none-any.whl" +upload-time = 2026-08-22 05:16:36.741632+00:00 +url = "https://files.pythonhosted.org/packages/bf/77/67b0b24e45073a699610e50f00c18474ff9b09ea29ecc95083bdf5e60acd/cython-3.3.0-py3-none-any.whl" +size = 1349151 + +[packages.wheels.hashes] +sha256 = "9b24b5c8cd536946b62086fcafee6d5509d3f549f72d553d2336af87ffbe0da1" + +[[packages]] +name = "diff-cover" +version = "10.6.0" +marker = "\"coverage\" in dependency_groups or \"dev\" in dependency_groups or \"test\" in dependency_groups or \"type\" in dependency_groups" +requires-python = ">=3.10" +dependencies = [ + { name = "chardet" }, + { name = "jinja2" }, + { name = "pluggy" }, + { name = "pygments" }, +] +index = "https://pypi.org/simple/" + +[packages.sdist] +name = "diff_cover-10.6.0.tar.gz" +upload-time = 2026-09-22 00:43:00.885282+00:00 +url = "https://files.pythonhosted.org/packages/63/ef/479d5b18c5cd61c4fbb769c60d4829ea9d70aef1267e302d6de60deed7f3/diff_cover-10.6.0.tar.gz" +size = 107697 + +[packages.sdist.hashes] +sha256 = "8d1194dfb537f70480ac97c9c45dd40721cf4c326c9903fbe997db67a4db6195" + +[[packages.wheels]] +name = "diff_cover-10.6.0-py3-none-any.whl" +upload-time = 2026-09-22 00:42:59.593738+00:00 +url = "https://files.pythonhosted.org/packages/47/04/7130707bb385835cca7f168c80d812c9b42af9ae8230a26b3e21c9437c98/diff_cover-10.6.0-py3-none-any.whl" +size = 63351 + +[packages.wheels.hashes] +sha256 = "a701553060f72d59b81f9cb1645b75ef66204427eb9dd99873c91092220e52ce" + +[[packages]] +name = "distlib" +version = "0.4.4" +marker = "\"dev\" in dependency_groups or \"fix\" in dependency_groups or \"test\" in dependency_groups or \"type\" in dependency_groups" +index = "https://pypi.org/simple/" + +[packages.sdist] +name = "distlib-0.4.4.tar.gz" +upload-time = 2026-10-16 13:56:34.203008+00:00 +url = "https://files.pythonhosted.org/packages/68/bc/d36966de7304047a1001a7559fbf266c464bb5d8cc85464536ef8c9bcc32/distlib-0.4.4.tar.gz" +size = 621304 + +[packages.sdist.hashes] +sha256 = "ca4175b4ac7f71223cc30ed7248c45d2b38c2439d8ee6e498ab67ed63cf3508d" + +[[packages.wheels]] +name = "distlib-0.4.4-py2.py3-none-any.whl" +upload-time = 2026-10-16 13:56:32.053304+00:00 +url = "https://files.pythonhosted.org/packages/c1/2b/238f02a4927fa2e27aa995371d240a468b52e42eae44a678434aff736b38/distlib-0.4.4-py2.py3-none-any.whl" +size = 472099 + +[packages.wheels.hashes] +sha256 = "cf09d55b1e85a89576b036aeb71550bfd68320f3f21761ca329eadadad918638" + +[[packages]] +name = "docutils" +version = "0.22.4" +marker = "\"dev\" in dependency_groups or \"docs\" in dependency_groups or \"pkg-meta\" in dependency_groups or \"release\" in dependency_groups or \"release-news\" in dependency_groups or \"release-notes\" in dependency_groups or \"release-prepare\" in dependency_groups or \"type\" in dependency_groups" +requires-python = ">=3.9" +index = "https://pypi.org/simple/" + +[packages.sdist] +name = "docutils-0.22.4.tar.gz" +upload-time = 2025-12-18 19:00:26.443107+00:00 +url = "https://files.pythonhosted.org/packages/ae/b6/03bb70946330e88ffec97aefd3ea75ba575cb2e762061e0e62a213befee8/docutils-0.22.4.tar.gz" +size = 2291750 + +[packages.sdist.hashes] +sha256 = "4db53b1fde9abecbb74d91230d32ab626d94f6badfc575d6db9194a49df29968" + +[[packages.wheels]] +name = "docutils-0.22.4-py3-none-any.whl" +upload-time = 2025-12-18 19:00:18.077803+00:00 +url = "https://files.pythonhosted.org/packages/02/10/5da547df7a391dcde17f59520a231527b8571e6f46fc8efb02ccb370ab12/docutils-0.22.4-py3-none-any.whl" +size = 633196 + +[packages.wheels.hashes] +sha256 = "d0013f540772d1420576855455d050a2180186c91c15779301ac2ccb3eeb68de" + +[[packages]] +name = "filelock" +version = "4.2.4" +marker = "\"dev\" in dependency_groups or \"fix\" in dependency_groups or \"test\" in dependency_groups or \"type\" in dependency_groups" +requires-python = ">=3.11" +index = "https://pypi.org/simple/" + +[packages.sdist] +name = "filelock-4.2.4.tar.gz" +upload-time = 2026-10-19 01:57:51.001795+00:00 +url = "https://files.pythonhosted.org/packages/d8/28/3893324bcadf8f89b1616637468d766af3f625008c0735bf704fdfa86617/filelock-4.2.4.tar.gz" +size = 652444 + +[packages.sdist.hashes] +sha256 = "c6d8663535e1650ee430507b4ffbac7b464dd64b7647ea0112a9f406499d163d" + +[[packages.wheels]] +name = "filelock-4.2.4-py3-none-any.whl" +upload-time = 2026-10-19 01:57:49.419460+00:00 +url = "https://files.pythonhosted.org/packages/e0/1b/5e74289a1878e666bb4b65fe5805a04cafcac2aa5f519ef1ead67ef91c56/filelock-4.2.4-py3-none-any.whl" +size = 162475 + +[packages.wheels.hashes] +sha256 = "301241438c57936ef93f4243a94f2559022268bfcf9b375a9a72eb0f8db8358e" + +[[packages]] +name = "furo" +version = "2025.12.19" +marker = "\"dev\" in dependency_groups or \"docs\" in dependency_groups or \"type\" in dependency_groups" +requires-python = ">=3.8" +dependencies = [ + { name = "accessible-pygments" }, + { name = "beautifulsoup4" }, + { name = "pygments" }, + { name = "sphinx" }, + { name = "sphinx-basic-ng" }, +] +index = "https://pypi.org/simple/" + +[packages.sdist] +name = "furo-2025.12.19.tar.gz" +upload-time = 2025-12-19 17:34:40.889109+00:00 +url = "https://files.pythonhosted.org/packages/ec/20/5f5ad4da6a5a27c80f2ed2ee9aee3f9e36c66e56e21c00fde467b2f8f88f/furo-2025.12.19.tar.gz" +size = 1661473 + +[packages.sdist.hashes] +sha256 = "188d1f942037d8b37cd3985b955839fea62baa1730087dc29d157677c857e2a7" + +[[packages.wheels]] +name = "furo-2025.12.19-py3-none-any.whl" +upload-time = 2025-12-19 17:34:38.905692+00:00 +url = "https://files.pythonhosted.org/packages/f4/b2/50e9b292b5cac13e9e81272c7171301abc753a60460d21505b606e15cf21/furo-2025.12.19-py3-none-any.whl" +size = 339262 + +[packages.wheels.hashes] +sha256 = "bb0ead5309f9500130665a26bee87693c41ce4dbdff864dbfb6b0dae4673d24f" + +[[packages]] +name = "hatch-fancy-pypi-readme" +version = "25.1.0" +marker = "\"build\" in dependency_groups" +requires-python = ">=3.8" +dependencies = [ + { name = "hatchling" }, +] +index = "https://pypi.org/simple/" + +[packages.sdist] +name = "hatch_fancy_pypi_readme-25.1.0.tar.gz" +upload-time = 2025-05-01 10:34:42.989334+00:00 +url = "https://files.pythonhosted.org/packages/f3/0f/aed57c301f339936eb91cb4d8c1e5088a101081854bd3ec18a889df32365/hatch_fancy_pypi_readme-25.1.0.tar.gz" +size = 29701 + +[packages.sdist.hashes] +sha256 = "9c58ed3dff90d51f43414ce37009ad1d5b0f08ffc9fc216998a06380f01c0045" + +[[packages.wheels]] +name = "hatch_fancy_pypi_readme-25.1.0-py3-none-any.whl" +upload-time = 2025-05-01 10:34:41.204793+00:00 +url = "https://files.pythonhosted.org/packages/58/b4/ad71e051b34f1935783de37beffee16e343ff2514b204dc515f9e939d4af/hatch_fancy_pypi_readme-25.1.0-py3-none-any.whl" +size = 10046 + +[packages.wheels.hashes] +sha256 = "ce0134c40d63d874ac48f48ccc678b8f3b62b8e50e9318520d2bffc752eedaf3" + +[[packages]] +name = "hatch-vcs" +version = "0.5.0" +marker = "\"build\" in dependency_groups" +requires-python = ">=3.9" +dependencies = [ + { name = "hatchling" }, + { name = "setuptools-scm" }, +] +index = "https://pypi.org/simple/" + +[packages.sdist] +name = "hatch_vcs-0.5.0.tar.gz" +upload-time = 2025-05-27 05:16:04.490926+00:00 +url = "https://files.pythonhosted.org/packages/6b/b0/4cc743d38adbee9d57d786fa496ed1daadb17e48589b6da8fa55717a0746/hatch_vcs-0.5.0.tar.gz" +size = 11424 + +[packages.sdist.hashes] +sha256 = "0395fa126940340215090c344a2bf4e2a77bcbe7daab16f41b37b98c95809ff9" + +[[packages.wheels]] +name = "hatch_vcs-0.5.0-py3-none-any.whl" +upload-time = 2025-05-27 05:16:03.184909+00:00 +url = "https://files.pythonhosted.org/packages/5f/48/1f85cee4b7b4f40b9b814b1febbc661bda6ced9649e410a0b74f6e415dd0/hatch_vcs-0.5.0-py3-none-any.whl" +size = 8507 + +[packages.wheels.hashes] +sha256 = "b49677dbdc597460cc22d01b27ab3696f5e16a21ecf2700fb01bc28e1f2a04a7" + +[[packages]] +name = "hatchling" +version = "1.32.4" +marker = "\"build\" in dependency_groups" +requires-python = ">=3.10" +dependencies = [ + { name = "packaging" }, + { name = "pathspec" }, + { name = "pluggy" }, + { name = "tomlkit" }, + { name = "trove-classifiers" }, +] +index = "https://pypi.org/simple/" + +[packages.sdist] +name = "hatchling-1.32.4.tar.gz" +upload-time = 2026-09-20 22:48:45.398505+00:00 +url = "https://files.pythonhosted.org/packages/f6/97/b5312f01a8c6daf729a9d272dd442e0c546dbcc630495788786c4b567ed0/hatchling-1.32.4.tar.gz" +size = 58065 + +[packages.sdist.hashes] +sha256 = "c4468f73144c054d2aab4ef0f0378c43b9878bf07f8ffd6b79690e970d375f07" + +[[packages.wheels]] +name = "hatchling-1.32.4-py3-none-any.whl" +upload-time = 2026-09-20 22:48:44.190681+00:00 +url = "https://files.pythonhosted.org/packages/5f/80/91f51f439c05d4ec4623c22928ce16a938d6d793bf709477830823497859/hatchling-1.32.4-py3-none-any.whl" +size = 78748 + +[packages.wheels.hashes] +sha256 = "08ecf7548fb48205e7f213d70c71e67b8271b7242093dc3f1da578b42c734a2c" + +[[packages]] +name = "id" +version = "1.6.1" +marker = "\"dev\" in dependency_groups or \"pkg-meta\" in dependency_groups" +requires-python = ">=3.9" +dependencies = [ + { name = "urllib3" }, +] +index = "https://pypi.org/simple/" + +[packages.sdist] +name = "id-1.6.1.tar.gz" +upload-time = 2026-02-04 16:19:41.260133+00:00 +url = "https://files.pythonhosted.org/packages/6d/04/c2156091427636080787aac190019dc64096e56a23b7364d3c1764ee3a06/id-1.6.1.tar.gz" +size = 18088 + +[packages.sdist.hashes] +sha256 = "d0732d624fb46fd4e7bc4e5152f00214450953b9e772c182c1c22964def1a069" + +[[packages.wheels]] +name = "id-1.6.1-py3-none-any.whl" +upload-time = 2026-02-04 16:19:40.051745+00:00 +url = "https://files.pythonhosted.org/packages/42/77/de194443bf38daed9452139e960c632b0ef9f9a5dd9ce605fdf18ca9f1b1/id-1.6.1-py3-none-any.whl" +size = 14689 + +[packages.wheels.hashes] +sha256 = "f5ec41ed2629a508f5d0988eda142e190c9c6da971100612c4de9ad9f9b237ca" + +[[packages]] +name = "identify" +version = "2.6.20" +marker = "\"dev\" in dependency_groups or \"fix\" in dependency_groups" +requires-python = ">=3.10" +index = "https://pypi.org/simple/" + +[packages.sdist] +name = "identify-2.6.20.tar.gz" +upload-time = 2026-09-26 20:29:07.187608+00:00 +url = "https://files.pythonhosted.org/packages/53/35/d70c0006c7cee65999ea94a6273e60b2094f600a3d8b71b04318253fc643/identify-2.6.20.tar.gz" +size = 99681 + +[packages.sdist.hashes] +sha256 = "ad729860a923858d26917c2f4fb0a1d83d27a75b1e090c06440c573f048f3285" + +[[packages.wheels]] +name = "identify-2.6.20-py2.py3-none-any.whl" +upload-time = 2026-09-26 20:29:06.054092+00:00 +url = "https://files.pythonhosted.org/packages/fa/70/fffc9613501877c0a10a1ef73a165e6ded3e53f6e7502227fef56d973933/identify-2.6.20-py2.py3-none-any.whl" +size = 99506 + +[packages.wheels.hashes] +sha256 = "6a16b69b93187244e0548cbfd25b3e4a6f9a7a2ad784625c3bec2b8d27b81aaa" + +[[packages]] +name = "idna" +version = "3.20" +marker = "\"dev\" in dependency_groups or \"docs\" in dependency_groups or \"pkg-meta\" in dependency_groups or \"type\" in dependency_groups" +requires-python = ">=3.9" +index = "https://pypi.org/simple/" + +[packages.sdist] +name = "idna-3.20.tar.gz" +upload-time = 2026-09-17 14:11:04.752193+00:00 +url = "https://files.pythonhosted.org/packages/f5/08/8eea9d4b8302028f3abb2c0813953f7aec26d33b7a8960ed760e65ff29fa/idna-3.20.tar.gz" +size = 216463 + +[packages.sdist.hashes] +sha256 = "a7db850025b95ded1eae8a46181a1a6c56c92c96f0e2b005d9ff8dc0210cab44" + +[[packages.wheels]] +name = "idna-3.20-py3-none-any.whl" +upload-time = 2026-09-17 14:11:03.168153+00:00 +url = "https://files.pythonhosted.org/packages/58/a2/bb081bab032533a855d44de1d56f8e8426114ff1ba5d1f07a438a0a654f8/idna-3.20-py3-none-any.whl" +size = 69583 + +[packages.wheels.hashes] +sha256 = "ab7ae7122974553370f0bdb919e1a960b2cd1bc1ef0276416d896db81c14582c" + +[[packages]] +name = "imagesize" +version = "2.0.1" +marker = "\"dev\" in dependency_groups or \"docs\" in dependency_groups or \"type\" in dependency_groups" +requires-python = ">=3.10" +index = "https://pypi.org/simple/" + +[packages.sdist] +name = "imagesize-2.0.1.tar.gz" +upload-time = 2026-08-24 12:35:19.130667+00:00 +url = "https://files.pythonhosted.org/packages/fb/5e/513ff06670c84e7b9887c1fdf61b2d42b4f574a831f2f1d2222023049d8a/imagesize-2.0.1.tar.gz" +size = 1883774 + +[packages.sdist.hashes] +sha256 = "b2ba6a4dea487a7ebcd53248d3476aca449d30db12a2dde5e0c5ca9624fd77e5" + +[[packages.wheels]] +name = "imagesize-2.0.1-py3-none-any.whl" +upload-time = 2026-08-24 12:35:12.548956+00:00 +url = "https://files.pythonhosted.org/packages/01/f9/575c8d760eae1fc99651b7cc5efd96ad5379ca4d6b53750b0fb4fe983f34/imagesize-2.0.1-py3-none-any.whl" +size = 14794 + +[packages.wheels.hashes] +sha256 = "ea0c9a0384df69ed86a943a15cde37d0360b82491b3910dc2215e202e62b5b02" + +[[packages]] +name = "iniconfig" +version = "2.3.1" +marker = "\"dev\" in dependency_groups or \"test\" in dependency_groups or \"type\" in dependency_groups" +requires-python = ">=3.10" +index = "https://pypi.org/simple/" + +[packages.sdist] +name = "iniconfig-2.3.1.tar.gz" +upload-time = 2026-10-06 22:48:38.076390+00:00 +url = "https://files.pythonhosted.org/packages/01/e1/2069291243c926a2ff1cd706c7f3eeb9b62144bf60f77c9fb9ff2fb26bd3/iniconfig-2.3.1.tar.gz" +size = 21209 + +[packages.sdist.hashes] +sha256 = "67f4b9c50da0dedf52af349e7749a80a9057a5031199791b906c3bb3ae878960" + +[[packages.wheels]] +name = "iniconfig-2.3.1-py3-none-any.whl" +upload-time = 2026-10-06 22:48:36.959685+00:00 +url = "https://files.pythonhosted.org/packages/56/43/4ca9e49d27a1fcf6bece6f6aec0ea46bb9112489b93d4b688fb415457bdb/iniconfig-2.3.1-py3-none-any.whl" +size = 7552 + +[packages.wheels.hashes] +sha256 = "9121e2c1fdb355232495be3194c8dfe87ccc2d5dee45947b78e68f499790d7a7" + +[[packages]] +name = "jaraco-classes" +version = "3.4.0" +marker = "\"dev\" in dependency_groups or \"pkg-meta\" in dependency_groups" +requires-python = ">=3.8" +dependencies = [ + { name = "more-itertools" }, +] +index = "https://pypi.org/simple/" + +[packages.sdist] +name = "jaraco.classes-3.4.0.tar.gz" +upload-time = 2024-03-31 07:27:36.643708+00:00 +url = "https://files.pythonhosted.org/packages/06/c0/ed4a27bc5571b99e3cff68f8a9fa5b56ff7df1c2251cc715a652ddd26402/jaraco.classes-3.4.0.tar.gz" +size = 11780 + +[packages.sdist.hashes] +sha256 = "47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd" + +[[packages.wheels]] +name = "jaraco.classes-3.4.0-py3-none-any.whl" +upload-time = 2024-03-31 07:27:34.792832+00:00 +url = "https://files.pythonhosted.org/packages/7f/66/b15ce62552d84bbfcec9a4873ab79d993a1dd4edb922cbfccae192bd5b5f/jaraco.classes-3.4.0-py3-none-any.whl" +size = 6777 + +[packages.wheels.hashes] +sha256 = "f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790" + +[[packages]] +name = "jaraco-context" +version = "6.1.2" +marker = "\"dev\" in dependency_groups or \"pkg-meta\" in dependency_groups" +requires-python = ">=3.10" +index = "https://pypi.org/simple/" + +[packages.sdist] +name = "jaraco_context-6.1.2.tar.gz" +upload-time = 2026-03-20 22:13:33.922545+00:00 +url = "https://files.pythonhosted.org/packages/af/50/4763cd07e722bb6285316d390a164bc7e479db9d90daa769f22578f698b4/jaraco_context-6.1.2.tar.gz" +size = 16801 + +[packages.sdist.hashes] +sha256 = "f1a6c9d391e661cc5b8d39861ff077a7dc24dc23833ccee564b234b81c82dfe3" + +[[packages.wheels]] +name = "jaraco_context-6.1.2-py3-none-any.whl" +upload-time = 2026-03-20 22:13:32.808832+00:00 +url = "https://files.pythonhosted.org/packages/f2/58/bc8954bda5fcda97bd7c19be11b85f91973d67a706ed4a3aec33e7de22db/jaraco_context-6.1.2-py3-none-any.whl" +size = 7871 + +[packages.wheels.hashes] +sha256 = "bf8150b79a2d5d91ae48629d8b427a8f7ba0e1097dd6202a9059f29a36379535" + +[[packages]] +name = "jaraco-functools" +version = "4.6.0" +marker = "\"dev\" in dependency_groups or \"pkg-meta\" in dependency_groups" +requires-python = ">=3.10" +dependencies = [ + { name = "more-itertools" }, +] +index = "https://pypi.org/simple/" + +[packages.sdist] +name = "jaraco_functools-4.6.0.tar.gz" +upload-time = 2026-07-14 01:28:02.544761+00:00 +url = "https://files.pythonhosted.org/packages/6c/1f/c23395957d41ccf27c4e535c3d334c4051e5395b3752057ba4cbaec35c56/jaraco_functools-4.6.0.tar.gz" +size = 20837 + +[packages.sdist.hashes] +sha256 = "880c577ec9720b3a052d5bc611fb9f2269b3d87902ef42440df443b88e443280" + +[[packages.wheels]] +name = "jaraco_functools-4.6.0-py3-none-any.whl" +upload-time = 2026-07-14 01:28:01.590470+00:00 +url = "https://files.pythonhosted.org/packages/02/36/ecc85bc96c273dc8a11273ed4782272975e6338d4a3e9228621175edf0e3/jaraco_functools-4.6.0-py3-none-any.whl" +size = 11677 + +[packages.wheels.hashes] +sha256 = "99e3dc0060c5cbe8fcd1cdb36258e2a65ca40f1566b2033b12abb1bb44dd3c30" + +[[packages]] +name = "jeepney" +version = "0.9.0" +marker = "\"dev\" in dependency_groups or \"pkg-meta\" in dependency_groups" +requires-python = ">=3.7" +index = "https://pypi.org/simple/" + +[packages.sdist] +name = "jeepney-0.9.0.tar.gz" +upload-time = 2025-02-27 18:51:01.684959+00:00 +url = "https://files.pythonhosted.org/packages/7b/6f/357efd7602486741aa73ffc0617fb310a29b588ed0fd69c2399acbb85b0c/jeepney-0.9.0.tar.gz" +size = 106758 + +[packages.sdist.hashes] +sha256 = "cf0e9e845622b81e4a28df94c40345400256ec608d0e55bb8a3feaa9163f5732" + +[[packages.wheels]] +name = "jeepney-0.9.0-py3-none-any.whl" +upload-time = 2025-02-27 18:51:00.104279+00:00 +url = "https://files.pythonhosted.org/packages/b2/a3/e137168c9c44d18eff0376253da9f1e9234d0239e0ee230d2fee6cea8e55/jeepney-0.9.0-py3-none-any.whl" +size = 49010 + +[packages.wheels.hashes] +sha256 = "97e5714520c16fc0a45695e5365a2e11b81ea79bba796e26f9f1d178cb182683" + +[[packages]] +name = "jinja2" +version = "3.1.6" +marker = "\"coverage\" in dependency_groups or \"dev\" in dependency_groups or \"docs\" in dependency_groups or \"release\" in dependency_groups or \"release-news\" in dependency_groups or \"release-notes\" in dependency_groups or \"release-prepare\" in dependency_groups or \"test\" in dependency_groups or \"type\" in dependency_groups" +requires-python = ">=3.7" +dependencies = [ + { name = "markupsafe" }, +] +index = "https://pypi.org/simple/" + +[packages.sdist] +name = "jinja2-3.1.6.tar.gz" +upload-time = 2025-03-05 20:05:02.478302+00:00 +url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz" +size = 245115 + +[packages.sdist.hashes] +sha256 = "0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d" + +[[packages.wheels]] +name = "jinja2-3.1.6-py3-none-any.whl" +upload-time = 2025-03-05 20:05:00.369721+00:00 +url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl" +size = 134899 + +[packages.wheels.hashes] +sha256 = "85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67" + +[[packages]] +name = "keyring" +version = "25.7.0" +marker = "\"dev\" in dependency_groups or \"pkg-meta\" in dependency_groups" +requires-python = ">=3.9" +dependencies = [ + { name = "jaraco-classes" }, + { name = "jaraco-context" }, + { name = "jaraco-functools" }, + { name = "jeepney" }, + { name = "secretstorage" }, +] +index = "https://pypi.org/simple/" + +[packages.sdist] +name = "keyring-25.7.0.tar.gz" +upload-time = 2025-11-16 16:26:09.482176+00:00 +url = "https://files.pythonhosted.org/packages/43/4b/674af6ef2f97d56f0ab5153bf0bfa28ccb6c3ed4d1babf4305449668807b/keyring-25.7.0.tar.gz" +size = 63516 + +[packages.sdist.hashes] +sha256 = "fe01bd85eb3f8fb3dd0405defdeac9a5b4f6f0439edbb3149577f244a2e8245b" + +[[packages.wheels]] +name = "keyring-25.7.0-py3-none-any.whl" +upload-time = 2025-11-16 16:26:08.402146+00:00 +url = "https://files.pythonhosted.org/packages/81/db/e655086b7f3a705df045bf0933bdd9c2f79bb3c97bfef1384598bb79a217/keyring-25.7.0-py3-none-any.whl" +size = 39160 + +[packages.wheels.hashes] +sha256 = "be4a0b195f149690c166e850609a477c532ddbfbaed96a404d4e43f8d5e2689f" + +[[packages]] +name = "markdown-it-py" +version = "4.2.0" +marker = "\"dev\" in dependency_groups or \"pkg-meta\" in dependency_groups or \"release\" in dependency_groups or \"release-news\" in dependency_groups or \"release-notes\" in dependency_groups or \"release-prepare\" in dependency_groups" +requires-python = ">=3.10" +dependencies = [ + { name = "mdurl" }, +] +index = "https://pypi.org/simple/" + +[packages.sdist] +name = "markdown_it_py-4.2.0.tar.gz" +upload-time = 2026-05-07 12:08:28.360612+00:00 +url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz" +size = 82454 + +[packages.sdist.hashes] +sha256 = "04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49" + +[[packages.wheels]] +name = "markdown_it_py-4.2.0-py3-none-any.whl" +upload-time = 2026-05-07 12:08:27.182789+00:00 +url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl" +size = 91687 + +[packages.wheels.hashes] +sha256 = "9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a" + +[[packages]] +name = "markupsafe" +version = "3.0.4" +marker = "\"coverage\" in dependency_groups or \"dev\" in dependency_groups or \"docs\" in dependency_groups or \"release\" in dependency_groups or \"release-news\" in dependency_groups or \"release-notes\" in dependency_groups or \"release-prepare\" in dependency_groups or \"test\" in dependency_groups or \"type\" in dependency_groups" +requires-python = ">=3.9" +index = "https://pypi.org/simple/" + +[packages.sdist] +name = "markupsafe-3.0.4.tar.gz" +upload-time = 2026-10-02 23:07:22.290270+00:00 +url = "https://files.pythonhosted.org/packages/38/9b/e422a865e1d5d57d0e509b4e0bf1c1a70a7f6382c29a5aa428df994c8bc8/markupsafe-3.0.4.tar.gz" +size = 153777 + +[packages.sdist.hashes] +sha256 = "2e9ad7dd851bf45fab9f75cbff4cb493fee9979e8d8c7c9c3ee119022518edd6" + +[[packages.wheels]] +name = "markupsafe-3.0.4-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl" +upload-time = 2026-10-02 23:06:22.524416+00:00 +url = "https://files.pythonhosted.org/packages/82/96/ef49135ce260db4ca4a12b119ed468449cd248db6b1468e2112b546d7a2e/markupsafe-3.0.4-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl" +size = 23111 + +[packages.wheels.hashes] +sha256 = "925f929d6b59a8b3f8b8c6ac363cd0af7eecc81efb3071770b3c6717c450a369" + +[[packages]] +name = "mdformat" +version = "1.0.0" +marker = "\"release\" in dependency_groups or \"release-news\" in dependency_groups or \"release-notes\" in dependency_groups or \"release-prepare\" in dependency_groups" +requires-python = ">=3.10" +dependencies = [ + { name = "markdown-it-py" }, +] +index = "https://pypi.org/simple/" + +[packages.sdist] +name = "mdformat-1.0.0.tar.gz" +upload-time = 2025-10-16 12:05:03.695033+00:00 +url = "https://files.pythonhosted.org/packages/3f/05/32b5e14b192b0a8a309f32232c580aefedd9d06017cb8fe8fce34bec654c/mdformat-1.0.0.tar.gz" +size = 56953 + +[packages.sdist.hashes] +sha256 = "4954045fcae797c29f86d4ad879e43bb151fa55dbaf74ac6eaeacf1d45bb3928" + +[[packages.wheels]] +name = "mdformat-1.0.0-py3-none-any.whl" +upload-time = 2025-10-16 12:05:02.607432+00:00 +url = "https://files.pythonhosted.org/packages/54/9a/8fe71b95985ca7a4001effbcc58e5a07a1f2a2884203f74dcf48a3b08315/mdformat-1.0.0-py3-none-any.whl" +size = 53288 + +[packages.wheels.hashes] +sha256 = "bca015d65a1d063a02e885a91daee303057bc7829c2cd37b2075a50dbb65944b" + +[[packages]] +name = "mdformat-gfm" +version = "1.0.0" +marker = "\"release\" in dependency_groups or \"release-news\" in dependency_groups or \"release-notes\" in dependency_groups or \"release-prepare\" in dependency_groups" +requires-python = ">=3.10" +dependencies = [ + { name = "markdown-it-py" }, + { name = "mdformat" }, + { name = "mdit-py-plugins" }, + { name = "wcwidth" }, +] +index = "https://pypi.org/simple/" + +[packages.sdist] +name = "mdformat_gfm-1.0.0.tar.gz" +upload-time = 2025-10-16 09:12:22.402429+00:00 +url = "https://files.pythonhosted.org/packages/56/6f/a626ebb142a290474401b67e2d61e73ce096bf7798ee22dfe6270f924b3f/mdformat_gfm-1.0.0.tar.gz" +size = 10112 + +[packages.sdist.hashes] +sha256 = "d1d49a409a6acb774ce7635c72d69178df7dce1dc8cdd10e19f78e8e57b72623" + +[[packages.wheels]] +name = "mdformat_gfm-1.0.0-py3-none-any.whl" +upload-time = 2025-10-16 09:12:21.276157+00:00 +url = "https://files.pythonhosted.org/packages/e6/18/6bc2189b744dd383cad03764f41f30352b1278d2205096f77a29c0b327ad/mdformat_gfm-1.0.0-py3-none-any.whl" +size = 10970 + +[packages.wheels.hashes] +sha256 = "7305a50efd2a140d7c83505b58e3ac5df2b09e293f9bbe72f6c7bee8c678b005" + +[[packages]] +name = "mdit-py-plugins" +version = "0.6.1" +marker = "\"release\" in dependency_groups or \"release-news\" in dependency_groups or \"release-notes\" in dependency_groups or \"release-prepare\" in dependency_groups" +requires-python = ">=3.10" +dependencies = [ + { name = "markdown-it-py" }, +] +index = "https://pypi.org/simple/" + +[packages.sdist] +name = "mdit_py_plugins-0.6.1.tar.gz" +upload-time = 2026-05-13 09:03:38.910230+00:00 +url = "https://files.pythonhosted.org/packages/59/fc/f8d0863f8862f25602c0404d75568e89fb6b4109804645e5cdfb1be5cf56/mdit_py_plugins-0.6.1.tar.gz" +size = 56114 + +[packages.sdist.hashes] +sha256 = "a2bca0f039f39dbd35fb74ae1b5f998608c437463371f0ff7f49a19a17a114d0" + +[[packages.wheels]] +name = "mdit_py_plugins-0.6.1-py3-none-any.whl" +upload-time = 2026-05-13 09:03:37.760000+00:00 +url = "https://files.pythonhosted.org/packages/a5/69/6da5581c6a7fede7dc261bf4e67d6adca4196f176b43288b55b3db395b6e/mdit_py_plugins-0.6.1-py3-none-any.whl" +size = 66663 + +[packages.wheels.hashes] +sha256 = "214c82fb2ac524472ab6a5bcab1de80f73b50443e187f401bfd77efbc7c6481d" + +[[packages]] +name = "mdurl" +version = "0.1.2" +marker = "\"dev\" in dependency_groups or \"pkg-meta\" in dependency_groups or \"release\" in dependency_groups or \"release-news\" in dependency_groups or \"release-notes\" in dependency_groups or \"release-prepare\" in dependency_groups" +requires-python = ">=3.7" +index = "https://pypi.org/simple/" + +[packages.sdist] +name = "mdurl-0.1.2.tar.gz" +upload-time = 2022-08-14 12:40:10.846281+00:00 +url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz" +size = 8729 + +[packages.sdist.hashes] +sha256 = "bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba" + +[[packages.wheels]] +name = "mdurl-0.1.2-py3-none-any.whl" +upload-time = 2022-08-14 12:40:09.779676+00:00 +url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl" +size = 9979 + +[packages.wheels.hashes] +sha256 = "84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8" + +[[packages]] +name = "more-itertools" +version = "11.2.1" +marker = "\"dev\" in dependency_groups or \"pkg-meta\" in dependency_groups" +requires-python = ">=3.11" +index = "https://pypi.org/simple/" + +[packages.sdist] +name = "more_itertools-11.2.1.tar.gz" +upload-time = 2026-10-14 18:50:30.407709+00:00 +url = "https://files.pythonhosted.org/packages/52/b8/c63995bebbb9ac9c059350e0fb8c82c2420c85aeca7d34e40aecf3df9281/more_itertools-11.2.1.tar.gz" +size = 154133 + +[packages.sdist.hashes] +sha256 = "cbf08fd0af284dc69718b9b76a8e6203df624d70209ea511e4219d350c856f63" + +[[packages.wheels]] +name = "more_itertools-11.2.1-py3-none-any.whl" +upload-time = 2026-10-14 18:50:29.116732+00:00 +url = "https://files.pythonhosted.org/packages/5d/57/3c4b1c6f2e0cc29ba023802458ea3d2a996f87085559e4a5f2192d549532/more_itertools-11.2.1-py3-none-any.whl" +size = 74405 + +[packages.wheels.hashes] +sha256 = "35a7377edd1dd6608dcb2cdf534ded55ea32d49448875ad042cd3f879fb1ded0" + +[[packages]] +name = "nh3" +version = "0.3.7" +marker = "\"dev\" in dependency_groups or \"pkg-meta\" in dependency_groups" +requires-python = ">=3.8" +index = "https://pypi.org/simple/" + +[packages.sdist] +name = "nh3-0.3.7.tar.gz" +upload-time = 2026-08-23 14:26:30.728660+00:00 +url = "https://files.pythonhosted.org/packages/18/2f/022b27146d52d24b1b353b003359134788ecbcd6fcdf6283adbd57c0fbc8/nh3-0.3.7.tar.gz" +size = 25662 + +[packages.sdist.hashes] +sha256 = "71860d01c16f4d8c72e334e0674beb2b0899dbd0bf760de18932ef4390303848" + +[[packages.wheels]] +name = "nh3-0.3.7-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" +upload-time = 2026-08-23 14:26:17.272038+00:00 +url = "https://files.pythonhosted.org/packages/a6/ed/c5510c615dce55b6fcc364aa1838142f938beed64f5e4927490dfcaf4405/nh3-0.3.7-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" +size = 832161 + +[packages.wheels.hashes] +sha256 = "70f5ac8626e899a4bab0ef74ca2f5bd602f49c7b739e6e5026b4afc6d63dac42" + +[[packages]] +name = "nodeenv" +version = "1.11.0" +marker = "\"dev\" in dependency_groups or \"fix\" in dependency_groups" +requires-python = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +index = "https://pypi.org/simple/" + +[packages.sdist] +name = "nodeenv-1.11.0.tar.gz" +upload-time = 2026-09-26 11:29:21.367929+00:00 +url = "https://files.pythonhosted.org/packages/9a/8e/105de02c1322cfada6d9710d9146ef8026419d433c9d08359a2d35805811/nodeenv-1.11.0.tar.gz" +size = 99301 + +[packages.sdist.hashes] +sha256 = "3ce8fe5b71d16e8af7039ca65257354100bc772965d6bc549070649e53b1b146" + +[[packages.wheels]] +name = "nodeenv-1.11.0-py2.py3-none-any.whl" +upload-time = 2026-09-26 11:29:19.933601+00:00 +url = "https://files.pythonhosted.org/packages/54/c8/12811c9b48fde162bb72b6f2e78fada9a247a09d7bb5be2050a5d099c77b/nodeenv-1.11.0-py2.py3-none-any.whl" +size = 34277 + +[packages.wheels.hashes] +sha256 = "edaa16e6c14d7cf395d75d4bbd5a26390f4dc06501a33b4e76282b02cc688a25" + +[[packages]] +name = "packaging" +version = "26.3" +marker = "\"build\" in dependency_groups or \"dev\" in dependency_groups or \"docs\" in dependency_groups or \"fix\" in dependency_groups or \"pkg-meta\" in dependency_groups or \"release\" in dependency_groups or \"release-news\" in dependency_groups or \"release-notes\" in dependency_groups or \"release-prepare\" in dependency_groups or \"test\" in dependency_groups or \"type\" in dependency_groups" +requires-python = ">=3.9" +index = "https://pypi.org/simple/" + +[packages.sdist] +name = "packaging-26.3.tar.gz" +upload-time = 2026-08-04 18:15:28.737071+00:00 +url = "https://files.pythonhosted.org/packages/7d/fa/3944b40b07da9ce895c0e6303a5ab7d53da063554f534556b134a54d6093/packaging-26.3.tar.gz" +size = 313412 + +[packages.sdist.hashes] +sha256 = "94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79" + +[[packages.wheels]] +name = "packaging-26.3-py3-none-any.whl" +upload-time = 2026-08-04 18:15:27.159957+00:00 +url = "https://files.pythonhosted.org/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl" +size = 129956 + +[packages.wheels.hashes] +sha256 = "d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c" + +[[packages]] +name = "pathspec" +version = "1.1.1" +marker = "\"build\" in dependency_groups" +requires-python = ">=3.9" +index = "https://pypi.org/simple/" + +[packages.sdist] +name = "pathspec-1.1.1.tar.gz" +upload-time = 2026-04-27 01:46:08.907631+00:00 +url = "https://files.pythonhosted.org/packages/5a/82/42f767fc1c1143d6fd36efb827202a2d997a375e160a71eb2888a925aac1/pathspec-1.1.1.tar.gz" +size = 135180 + +[packages.sdist.hashes] +sha256 = "17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a" + +[[packages.wheels]] +name = "pathspec-1.1.1-py3-none-any.whl" +upload-time = 2026-04-27 01:46:07.060850+00:00 +url = "https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl" +size = 57328 + +[packages.wheels.hashes] +sha256 = "a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189" + +[[packages]] +name = "platformdirs" +version = "4.13.5" +marker = "\"dev\" in dependency_groups or \"fix\" in dependency_groups or \"test\" in dependency_groups or \"type\" in dependency_groups" +requires-python = ">=3.11" +index = "https://pypi.org/simple/" + +[packages.sdist] +name = "platformdirs-4.13.5.tar.gz" +upload-time = 2026-10-19 01:02:11.778459+00:00 +url = "https://files.pythonhosted.org/packages/58/e6/d0abbb256b44e6a05748c6942511ff67db8dbb068de0c35b24bf1b395b09/platformdirs-4.13.5.tar.gz" +size = 62362 + +[packages.sdist.hashes] +sha256 = "05831adf87d392760f4b5c3a0a95579ab5a10b18b9b9bd43680dcb47544788a0" + +[[packages.wheels]] +name = "platformdirs-4.13.5-py3-none-any.whl" +upload-time = 2026-10-19 01:02:10.170818+00:00 +url = "https://files.pythonhosted.org/packages/6c/8a/1d8cb3842909dc193a73835adedf412e32e18f8ffd59f43983949622fe6f/platformdirs-4.13.5-py3-none-any.whl" +size = 32779 + +[packages.wheels.hashes] +sha256 = "53d3ec4280b815c1e0b02edad7f9ee8fda1ba9c0865dd9c2e6e63849ce82fc1e" + +[[packages]] +name = "pluggy" +version = "1.7.0" +marker = "\"build\" in dependency_groups or \"coverage\" in dependency_groups or \"dev\" in dependency_groups or \"test\" in dependency_groups or \"type\" in dependency_groups" +requires-python = ">=3.10" +index = "https://pypi.org/simple/" + +[packages.sdist] +name = "pluggy-1.7.0.tar.gz" +upload-time = 2026-10-15 09:50:58.343484+00:00 +url = "https://files.pythonhosted.org/packages/bf/db/7fc19e6f2dc92a966727031389fc2e08b558f0f25eb7403c1119ad4713cd/pluggy-1.7.0.tar.gz" +size = 123304 + +[packages.sdist.hashes] +sha256 = "d1eaa46ebb595891b860ab086b4d09c8588af65ebd4361b8e8f4bb8920b90ba8" + +[[packages.wheels]] +name = "pluggy-1.7.0-py3-none-any.whl" +upload-time = 2026-10-15 09:50:56.808462+00:00 +url = "https://files.pythonhosted.org/packages/40/9e/2b38731e0fc536806f16490e1a12d7f0dc2a1235aa8cc07bcc75416a7daa/pluggy-1.7.0-py3-none-any.whl" +size = 27082 + +[packages.wheels.hashes] +sha256 = "7dd7b0d8832ba3cb632c306926ded123429211b83641b35dc5c41ad2d34f9bec" + +[[packages]] +name = "pre-commit" +version = "4.7.0" +marker = "\"dev\" in dependency_groups or \"fix\" in dependency_groups" +requires-python = ">=3.11" +dependencies = [ + { name = "cfgv" }, + { name = "identify" }, + { name = "nodeenv" }, + { name = "pyyaml" }, + { name = "virtualenv" }, +] +index = "https://pypi.org/simple/" + +[packages.sdist] +name = "pre_commit-4.7.0.tar.gz" +upload-time = 2026-10-12 20:45:40.320969+00:00 +url = "https://files.pythonhosted.org/packages/3a/c3/6965c2690ddce885ec75121572d065a5b94c5a5d8d3e657927ecda9fd715/pre_commit-4.7.0.tar.gz" +size = 198120 + +[packages.sdist.hashes] +sha256 = "1f3deee12b914c8fa184bd2b5953391c2e4f36763e403d4a23dc9983fa7c6e86" + +[[packages.wheels]] +name = "pre_commit-4.7.0-py2.py3-none-any.whl" +upload-time = 2026-10-12 20:45:38.858255+00:00 +url = "https://files.pythonhosted.org/packages/eb/83/ce0a196e4f7b20b11ce3aa1fc72fb973f64b7628d51c1b8aebad37fb7f81/pre_commit-4.7.0-py2.py3-none-any.whl" +size = 226261 + +[packages.wheels.hashes] +sha256 = "2e229038ad3656081b70c27913157e4b636ef306f643f024a8a8e3f0f7b2f877" + +[[packages]] +name = "pre-commit-uv" +version = "4.4.0" +marker = "\"dev\" in dependency_groups or \"fix\" in dependency_groups" +requires-python = ">=3.11" +dependencies = [ + { name = "pre-commit" }, + { name = "uv" }, +] +index = "https://pypi.org/simple/" + +[packages.sdist] +name = "pre_commit_uv-4.4.0.tar.gz" +upload-time = 2026-10-11 00:27:45.724147+00:00 +url = "https://files.pythonhosted.org/packages/95/3c/65ca92a8343efcf16107824d01c4664bb9a2b2e8b7fb4efc954375e56c26/pre_commit_uv-4.4.0.tar.gz" +size = 7591 + +[packages.sdist.hashes] +sha256 = "8e1b91485fdaba0673fc0f81229cab364c00fb6193b5fe0bcee2862b1a4ce1d4" + +[[packages.wheels]] +name = "pre_commit_uv-4.4.0-py3-none-any.whl" +upload-time = 2026-10-11 00:27:44.627800+00:00 +url = "https://files.pythonhosted.org/packages/29/fa/d5fe0626ce7a70510e6fbf2c6a9f6da4f1cbc42b86542ada1eae0eea6b55/pre_commit_uv-4.4.0-py3-none-any.whl" +size = 6209 + +[packages.wheels.hashes] +sha256 = "9012b09950a50efdcfb03b95355e1f65c14f9a5cb425d12c6bf2c6d6807617d7" + +[[packages]] +name = "prek" +version = "0.5.6" +marker = "\"release\" in dependency_groups or \"release-prepare\" in dependency_groups" +requires-python = ">=3.8" +index = "https://pypi.org/simple/" + +[packages.sdist] +name = "prek-0.5.6.tar.gz" +upload-time = 2026-10-19 05:46:52.314250+00:00 +url = "https://files.pythonhosted.org/packages/4f/96/c4062bcaebdabfa945d07c424fb8c05d53a72d4f3fdcf255b1188abbb150/prek-0.5.6.tar.gz" +size = 579962 + +[packages.sdist.hashes] +sha256 = "87a8c19f873b96be809b4368959ed908cfa06255dba15082d0fb91173e9293bc" + +[[packages.wheels]] +name = "prek-0.5.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" +upload-time = 2026-10-19 05:46:42.326947+00:00 +url = "https://files.pythonhosted.org/packages/e1/f7/04e08d2343ce1d54c5da879e2dfea7df20f3140b1b2023a937fac87fa545/prek-0.5.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" +size = 6165929 + +[packages.wheels.hashes] +sha256 = "aabec01beeb372bcdc48f6b339021bc33c07edf477b337fc92dc5d3b01f99150" + +[[packages]] +name = "pycparser" +version = "3.11" +marker = "\"dev\" in dependency_groups or \"pkg-meta\" in dependency_groups" +requires-python = ">=3.10" +index = "https://pypi.org/simple/" + +[packages.sdist] +name = "pycparser-3.11.tar.gz" +upload-time = 2026-10-09 12:56:59.539345+00:00 +url = "https://files.pythonhosted.org/packages/da/a8/c5fdbeee588bb8ada9458774f43adf1bdd30bd59157055142183e769a024/pycparser-3.11.tar.gz" +size = 113796 + +[packages.sdist.hashes] +sha256 = "d875f09c3507d00e1aba0eecc6dcadc1352f30fff09dc6bff2f1c2935e97c2bc" + +[[packages.wheels]] +name = "pycparser-3.11-py3-none-any.whl" +upload-time = 2026-10-09 12:56:58.131173+00:00 +url = "https://files.pythonhosted.org/packages/90/11/0e6f11117525ff0eec40ebac3d313376f102df93ca44ad9e893ee85e4f89/pycparser-3.11-py3-none-any.whl" +size = 51178 + +[packages.wheels.hashes] +sha256 = "51d5a8ba2be0bbe440b99d2112604c95bbbc3c2748a64260186c541e1729cd80" + +[[packages]] +name = "pydantic" +version = "2.14.1" +marker = "\"dev\" in dependency_groups or \"pkg-meta\" in dependency_groups" +requires-python = ">=3.10" +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +index = "https://pypi.org/simple/" + +[packages.sdist] +name = "pydantic-2.14.1.tar.gz" +upload-time = 2026-10-11 18:37:55.396662+00:00 +url = "https://files.pythonhosted.org/packages/7c/0b/8e10b2e693af8ec54346a14caf36221334775975a747c9ceaa3f8371d96d/pydantic-2.14.1.tar.gz" +size = 900362 + +[packages.sdist.hashes] +sha256 = "94f478203dd03404682a1ada216965651dd74b1d2d5ffd62e00e0837caab5c26" + +[[packages.wheels]] +name = "pydantic-2.14.1-py3-none-any.whl" +upload-time = 2026-10-11 18:37:53.437059+00:00 +url = "https://files.pythonhosted.org/packages/35/ea/a56b9fe5066f3537b7882f77e9c5dfb26d8c2eefdaed9b5fc73d57b4dc22/pydantic-2.14.1-py3-none-any.whl" +size = 488106 + +[packages.wheels.hashes] +sha256 = "9195d967ec791692a04438115466764fb8b9a27b31f14a760437694f40d6b454" + +[[packages]] +name = "pydantic-core" +version = "2.50.1" +marker = "\"dev\" in dependency_groups or \"pkg-meta\" in dependency_groups" +requires-python = ">=3.10" +dependencies = [ + { name = "typing-extensions" }, +] +index = "https://pypi.org/simple/" + +[packages.sdist] +name = "pydantic_core-2.50.1.tar.gz" +upload-time = 2026-10-11 18:35:44.820993+00:00 +url = "https://files.pythonhosted.org/packages/a6/24/af4ec4be49fbc810f35b0bdcacc3d433b56bbfa469fd3bfd4ae116cd9bf1/pydantic_core-2.50.1.tar.gz" +size = 513515 + +[packages.sdist.hashes] +sha256 = "e50d7b94baac6c7d09927fa5ca5800a0c7ee5015c7fcff65beb3a1931b5a6e09" + +[[packages.wheels]] +name = "pydantic_core-2.50.1-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" +upload-time = 2026-10-11 18:34:15.560294+00:00 +url = "https://files.pythonhosted.org/packages/b8/0d/1b1149f60a00ea21ba5f70e28acbd40feb4598af414f80f53c921fac07c9/pydantic_core-2.50.1-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" +size = 2159961 + +[packages.wheels.hashes] +sha256 = "7689580e72a642ab5ec64d5f55b2e33636fa43b4ebe63c0c2c965ef307c7d1aa" + +[[packages]] +name = "pygments" +version = "2.21.0" +marker = "\"coverage\" in dependency_groups or \"dev\" in dependency_groups or \"docs\" in dependency_groups or \"pkg-meta\" in dependency_groups or \"test\" in dependency_groups or \"type\" in dependency_groups" +requires-python = ">=3.9" +index = "https://pypi.org/simple/" + +[packages.sdist] +name = "pygments-2.21.0.tar.gz" +upload-time = 2026-08-17 08:02:48.824391+00:00 +url = "https://files.pythonhosted.org/packages/49/2e/ced460408999b33da6b31b0021b0f37d329e202d4169aeb164493778f25b/pygments-2.21.0.tar.gz" +size = 5005329 + +[packages.sdist.hashes] +sha256 = "610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c" + +[[packages.wheels]] +name = "pygments-2.21.0-py3-none-any.whl" +upload-time = 2026-08-17 08:02:44.912148+00:00 +url = "https://files.pythonhosted.org/packages/71/46/17f022dd3e953bf20a04a028a21ec746d942f8d2af30fa0f124fa0e6a684/pygments-2.21.0-py3-none-any.whl" +size = 1250147 + +[packages.wheels.hashes] +sha256 = "2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9" + +[[packages]] +name = "pytest" +version = "9.1.1" +marker = "\"dev\" in dependency_groups or \"test\" in dependency_groups or \"type\" in dependency_groups" +requires-python = ">=3.10" +dependencies = [ + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +index = "https://pypi.org/simple/" + +[packages.sdist] +name = "pytest-9.1.1.tar.gz" +upload-time = 2026-06-19 10:58:32.857420+00:00 +url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz" +size = 1636369 + +[packages.sdist.hashes] +sha256 = "1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313" + +[[packages.wheels]] +name = "pytest-9.1.1-py3-none-any.whl" +upload-time = 2026-06-19 10:58:31.347074+00:00 +url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl" +size = 386536 + +[packages.wheels.hashes] +sha256 = "37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c" + +[[packages]] +name = "pytest-asyncio" +version = "1.4.0" +marker = "\"dev\" in dependency_groups or \"test\" in dependency_groups or \"type\" in dependency_groups" +requires-python = ">=3.10" +dependencies = [ + { name = "pytest" }, +] +index = "https://pypi.org/simple/" + +[packages.sdist] +name = "pytest_asyncio-1.4.0.tar.gz" +upload-time = 2026-05-26 09:56:04.083461+00:00 +url = "https://files.pythonhosted.org/packages/43/7c/d36d04db312ecf4298932ef77e6e4a9e8ad017906e24e34f0b0c361a2473/pytest_asyncio-1.4.0.tar.gz" +size = 58514 + +[packages.sdist.hashes] +sha256 = "c6c0d2259945122819f171a32ecea2c349ead889ee28176caaf492143424be42" + +[[packages.wheels]] +name = "pytest_asyncio-1.4.0-py3-none-any.whl" +upload-time = 2026-05-26 09:56:02.576082+00:00 +url = "https://files.pythonhosted.org/packages/03/e2/08a497ef684b88559c9cc5f4ad53a37e7b99e727094a86d6ea32536d5d3c/pytest_asyncio-1.4.0-py3-none-any.whl" +size = 16930 + +[packages.wheels.hashes] +sha256 = "933ca923a23075a87fb7070c0ec272a6848489824d887c85c812670932835aa1" + +[[packages]] +name = "pytest-cov" +version = "7.1.0" +marker = "\"dev\" in dependency_groups or \"test\" in dependency_groups or \"type\" in dependency_groups" +requires-python = ">=3.9" +dependencies = [ + { name = "coverage" }, + { name = "pluggy" }, + { name = "pytest" }, +] +index = "https://pypi.org/simple/" + +[packages.sdist] +name = "pytest_cov-7.1.0.tar.gz" +upload-time = 2026-03-21 20:11:16.284275+00:00 +url = "https://files.pythonhosted.org/packages/b1/51/a849f96e117386044471c8ec2bd6cfebacda285da9525c9106aeb28da671/pytest_cov-7.1.0.tar.gz" +size = 55592 + +[packages.sdist.hashes] +sha256 = "30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2" + +[[packages.wheels]] +name = "pytest_cov-7.1.0-py3-none-any.whl" +upload-time = 2026-03-21 20:11:14.438324+00:00 +url = "https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl" +size = 22876 + +[packages.wheels.hashes] +sha256 = "a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678" + +[[packages]] +name = "pytest-mock" +version = "3.17.0" +marker = "\"dev\" in dependency_groups or \"test\" in dependency_groups or \"type\" in dependency_groups" +requires-python = ">=3.10" +dependencies = [ + { name = "pytest" }, +] +index = "https://pypi.org/simple/" + +[packages.sdist] +name = "pytest_mock-3.17.0.tar.gz" +upload-time = 2026-10-17 16:21:42.435701+00:00 +url = "https://files.pythonhosted.org/packages/97/c2/28b9f5b955ff2bcd4727a6f768e2ca9b586cd7c37b95ea5f7b9b873ac7b3/pytest_mock-3.17.0.tar.gz" +size = 43064 + +[packages.sdist.hashes] +sha256 = "e36576e0985e01027e999891e7a966c67822d40c89ef1b4c8b6e22737e3590b4" + +[[packages.wheels]] +name = "pytest_mock-3.17.0-py3-none-any.whl" +upload-time = 2026-10-17 16:21:41.265482+00:00 +url = "https://files.pythonhosted.org/packages/e8/ea/47fbdc8f37410956fd445720e927589b1e2a47772c5f28109f0ce2fa26d9/pytest_mock-3.17.0-py3-none-any.whl" +size = 11248 + +[packages.wheels.hashes] +sha256 = "1b82aa4b37137248bd687055e109723debeb07b489de1565297dc9589c5f2d6b" + +[[packages]] +name = "pytest-randomly" +version = "5.0.0" +marker = "\"dev\" in dependency_groups or \"test\" in dependency_groups or \"type\" in dependency_groups" +requires-python = ">=3.10" +dependencies = [ + { name = "pytest" }, +] +index = "https://pypi.org/simple/" + +[packages.sdist] +name = "pytest_randomly-5.0.0.tar.gz" +upload-time = 2026-09-01 22:34:20.441039+00:00 +url = "https://files.pythonhosted.org/packages/01/3b/6a40e1b9d925651e601e056a97f60d8a1daeddeac03d5609be60cb4362ce/pytest_randomly-5.0.0.tar.gz" +size = 8542 + +[packages.sdist.hashes] +sha256 = "e9c575a5873ef168ddbe340ed9e97ce9edb4492ccc821e4b2ac6bb1f0ed515d2" + +[[packages.wheels]] +name = "pytest_randomly-5.0.0-py3-none-any.whl" +upload-time = 2026-09-01 22:34:19.227009+00:00 +url = "https://files.pythonhosted.org/packages/10/b4/47e939285caad9a623d021512912ac08dc92a467ad075d179f43729d2934/pytest_randomly-5.0.0-py3-none-any.whl" +size = 8920 + +[packages.wheels.hashes] +sha256 = "8a0d4703115c0c25b38b6e129fc16b1947b9643ff26a41bc1d185d7e5a7689c1" + +[[packages]] +name = "pytest-timeout" +version = "2.4.0" +marker = "\"dev\" in dependency_groups or \"test\" in dependency_groups or \"type\" in dependency_groups" +requires-python = ">=3.7" +dependencies = [ + { name = "pytest" }, +] +index = "https://pypi.org/simple/" + +[packages.sdist] +name = "pytest_timeout-2.4.0.tar.gz" +upload-time = 2025-05-05 19:44:34.990551+00:00 +url = "https://files.pythonhosted.org/packages/ac/82/4c9ecabab13363e72d880f2fb504c5f750433b2b6f16e99f4ec21ada284c/pytest_timeout-2.4.0.tar.gz" +size = 17973 + +[packages.sdist.hashes] +sha256 = "7e68e90b01f9eff71332b25001f85c75495fc4e3a836701876183c4bcfd0540a" + +[[packages.wheels]] +name = "pytest_timeout-2.4.0-py3-none-any.whl" +upload-time = 2025-05-05 19:44:33.502941+00:00 +url = "https://files.pythonhosted.org/packages/fa/b6/3127540ecdf1464a00e5a01ee60a1b09175f6913f0644ac748494d9c4b21/pytest_timeout-2.4.0-py3-none-any.whl" +size = 14382 + +[packages.wheels.hashes] +sha256 = "c42667e5cdadb151aeb5b26d114aff6bdf5a907f176a007a30b940d3d865b5c2" + +[[packages]] +name = "python-discovery" +version = "1.6.4" +marker = "\"dev\" in dependency_groups or \"fix\" in dependency_groups or \"test\" in dependency_groups or \"type\" in dependency_groups" +requires-python = ">=3.9" +dependencies = [ + { name = "filelock" }, +] +index = "https://pypi.org/simple/" + +[packages.sdist] +name = "python_discovery-1.6.4.tar.gz" +upload-time = 2026-10-19 01:45:14.855085+00:00 +url = "https://files.pythonhosted.org/packages/f3/ee/cf608372c88ce2c136ee13c69ab6f667dbc0d0c1176af188b4ddc8853e66/python_discovery-1.6.4.tar.gz" +size = 89883 + +[packages.sdist.hashes] +sha256 = "bc70d31f0ec5de11eda13d4d64c16e24b3506a65984684f2b6c98cb83b2ea9df" + +[[packages.wheels]] +name = "python_discovery-1.6.4-py3-none-any.whl" +upload-time = 2026-10-19 01:45:13.668192+00:00 +url = "https://files.pythonhosted.org/packages/60/91/1aa70d3dfcab0dbcf88a2cbc3bce7f7c69562287a90216e617eaa4937e25/python_discovery-1.6.4-py3-none-any.whl" +size = 39385 + +[packages.wheels.hashes] +sha256 = "530179cc51a1816d512b15635b3dc931295ae12efb183783ad31190a5d425d2b" + +[[packages]] +name = "pyyaml" +version = "6.0.3" +marker = "\"dev\" in dependency_groups or \"docs\" in dependency_groups or \"fix\" in dependency_groups or \"release\" in dependency_groups or \"release-news\" in dependency_groups or \"release-notes\" in dependency_groups or \"release-prepare\" in dependency_groups or \"type\" in dependency_groups" +requires-python = ">=3.8" +index = "https://pypi.org/simple/" + +[packages.sdist] +name = "pyyaml-6.0.3.tar.gz" +upload-time = 2025-09-25 21:33:16.546886+00:00 +url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz" +size = 130960 + +[packages.sdist.hashes] +sha256 = "d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f" + +[[packages]] +name = "readme-renderer" +version = "46.0" +marker = "\"dev\" in dependency_groups or \"pkg-meta\" in dependency_groups" +requires-python = ">=3.10" +dependencies = [ + { name = "docutils" }, + { name = "nh3" }, + { name = "pygments" }, +] +index = "https://pypi.org/simple/" + +[packages.sdist] +name = "readme_renderer-46.0.tar.gz" +upload-time = 2026-08-28 15:18:32.490778+00:00 +url = "https://files.pythonhosted.org/packages/25/d7/9309494fad74ee831d4546f69325b5519f37c6dfb2d9ba495db8c6d4f4ca/readme_renderer-46.0.tar.gz" +size = 38382 + +[packages.sdist.hashes] +sha256 = "af3e964914f6310a33ff67b72a4bdd940bed8d7c3bdecd2d14f40edf284bfe90" + +[[packages.wheels]] +name = "readme_renderer-46.0-py3-none-any.whl" +upload-time = 2026-08-28 15:18:31.132970+00:00 +url = "https://files.pythonhosted.org/packages/4b/72/ac5ca81fe9121fcaa9d828d21017cba00a16a98e4ea5fb60c878f93dda4f/readme_renderer-46.0-py3-none-any.whl" +size = 14239 + +[packages.wheels.hashes] +sha256 = "d0dae1f74bb273b534770cb4cccb6bb78735540afdb03c2146f4e19dcd412560" + +[[packages]] +name = "release-creator" +version = "4.1.0" +marker = "\"release\" in dependency_groups or \"release-news\" in dependency_groups or \"release-notes\" in dependency_groups or \"release-prepare\" in dependency_groups" +requires-python = ">=3.15" +dependencies = [ + { name = "docutils" }, + { name = "markdown-it-py" }, + { name = "mdformat" }, + { name = "mdformat-gfm" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "towncrier" }, +] +index = "https://pypi.org/simple/" + +[packages.sdist] +name = "release_creator-4.1.0.tar.gz" +upload-time = 2026-10-26 18:24:45.805437+00:00 +url = "https://files.pythonhosted.org/packages/7f/fd/05d7f3c604476a238d67d5aaa2a4085312e7078c193dbd7ad99ad48d560d/release_creator-4.1.0.tar.gz" +size = 186016 + +[packages.sdist.hashes] +sha256 = "c783037f5ca2efc16fa7cccfb1c2abf0a442f116a63ca82bd7bfb7e2a8343a15" + +[[packages.wheels]] +name = "release_creator-4.1.0-py3-none-any.whl" +upload-time = 2026-10-26 18:24:44.353933+00:00 +url = "https://files.pythonhosted.org/packages/30/d0/38480e99025c021d07e0710af02547562dfadbd798bef26dc01869a3f81e/release_creator-4.1.0-py3-none-any.whl" +size = 52922 + +[packages.wheels.hashes] +sha256 = "67deae16d11ac261a1512f18a22c0cf13418f43afc3e56cb74e58ad4422062e3" + +[[packages]] +name = "requests" +version = "2.34.2" +marker = "\"dev\" in dependency_groups or \"docs\" in dependency_groups or \"pkg-meta\" in dependency_groups or \"type\" in dependency_groups" +requires-python = ">=3.10" +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +index = "https://pypi.org/simple/" + +[packages.sdist] +name = "requests-2.34.2.tar.gz" +upload-time = 2026-05-14 19:25:27.735762+00:00 +url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz" +size = 142856 + +[packages.sdist.hashes] +sha256 = "f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed" + +[[packages.wheels]] +name = "requests-2.34.2-py3-none-any.whl" +upload-time = 2026-05-14 19:25:26.443000+00:00 +url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl" +size = 73075 + +[packages.wheels.hashes] +sha256 = "2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0" + +[[packages]] +name = "requests-toolbelt" +version = "1.0.0" +marker = "\"dev\" in dependency_groups or \"pkg-meta\" in dependency_groups" +requires-python = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" +dependencies = [ + { name = "requests" }, +] +index = "https://pypi.org/simple/" + +[packages.sdist] +name = "requests-toolbelt-1.0.0.tar.gz" +upload-time = 2023-05-01 04:11:33.229998+00:00 +url = "https://files.pythonhosted.org/packages/f3/61/d7545dafb7ac2230c70d38d31cbfe4cc64f7144dc41f6e4e4b78ecd9f5bb/requests-toolbelt-1.0.0.tar.gz" +size = 206888 + +[packages.sdist.hashes] +sha256 = "7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6" + +[[packages.wheels]] +name = "requests_toolbelt-1.0.0-py2.py3-none-any.whl" +upload-time = 2023-05-01 04:11:28.427086+00:00 +url = "https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl" +size = 54481 + +[packages.wheels.hashes] +sha256 = "cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06" + +[[packages]] +name = "rfc3986" +version = "2.0.0" +marker = "\"dev\" in dependency_groups or \"pkg-meta\" in dependency_groups" +requires-python = ">=3.7" +index = "https://pypi.org/simple/" + +[packages.sdist] +name = "rfc3986-2.0.0.tar.gz" +upload-time = 2022-01-10 00:52:30.832978+00:00 +url = "https://files.pythonhosted.org/packages/85/40/1520d68bfa07ab5a6f065a186815fb6610c86fe957bc065754e47f7b0840/rfc3986-2.0.0.tar.gz" +size = 49026 + +[packages.sdist.hashes] +sha256 = "97aacf9dbd4bfd829baad6e6309fa6573aaf1be3f6fa735c8ab05e46cecb261c" + +[[packages.wheels]] +name = "rfc3986-2.0.0-py2.py3-none-any.whl" +upload-time = 2022-01-10 00:52:29.594625+00:00 +url = "https://files.pythonhosted.org/packages/ff/9a/9afaade874b2fa6c752c36f1548f718b5b83af81ed9b76628329dab81c1b/rfc3986-2.0.0-py2.py3-none-any.whl" +size = 31326 + +[packages.wheels.hashes] +sha256 = "50b1502b60e289cb37883f3dfd34532b8873c7de9f49bb546641ce9cbd256ebd" + +[[packages]] +name = "rich" +version = "15.0.0" +marker = "\"dev\" in dependency_groups or \"pkg-meta\" in dependency_groups" +requires-python = ">=3.9.0" +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +index = "https://pypi.org/simple/" + +[packages.sdist] +name = "rich-15.0.0.tar.gz" +upload-time = 2026-04-12 08:24:00.750018+00:00 +url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz" +size = 230680 + +[packages.sdist.hashes] +sha256 = "edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36" + +[[packages.wheels]] +name = "rich-15.0.0-py3-none-any.whl" +upload-time = 2026-04-12 08:24:02.830783+00:00 +url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl" +size = 310654 + +[packages.wheels.hashes] +sha256 = "33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb" + +[[packages]] +name = "roman-numerals" +version = "4.1.0" +marker = "\"dev\" in dependency_groups or \"docs\" in dependency_groups or \"type\" in dependency_groups" +requires-python = ">=3.10" +index = "https://pypi.org/simple/" + +[packages.sdist] +name = "roman_numerals-4.1.0.tar.gz" +upload-time = 2025-12-17 18:25:34.381818+00:00 +url = "https://files.pythonhosted.org/packages/ae/f9/41dc953bbeb056c17d5f7a519f50fdf010bd0553be2d630bc69d1e022703/roman_numerals-4.1.0.tar.gz" +size = 9077 + +[packages.sdist.hashes] +sha256 = "1af8b147eb1405d5839e78aeb93131690495fe9da5c91856cb33ad55a7f1e5b2" + +[[packages.wheels]] +name = "roman_numerals-4.1.0-py3-none-any.whl" +upload-time = 2025-12-17 18:25:33.098993+00:00 +url = "https://files.pythonhosted.org/packages/04/54/6f679c435d28e0a568d8e8a7c0a93a09010818634c3c3907fc98d8983770/roman_numerals-4.1.0-py3-none-any.whl" +size = 7676 + +[packages.wheels.hashes] +sha256 = "647ba99caddc2cc1e55a51e4360689115551bf4476d90e8162cf8c345fe233c7" + +[[packages]] +name = "secretstorage" +version = "3.5.0" +marker = "\"dev\" in dependency_groups or \"pkg-meta\" in dependency_groups" +requires-python = ">=3.10" +dependencies = [ + { name = "cryptography" }, + { name = "jeepney" }, +] +index = "https://pypi.org/simple/" + +[packages.sdist] +name = "secretstorage-3.5.0.tar.gz" +upload-time = 2025-11-23 19:02:53.191898+00:00 +url = "https://files.pythonhosted.org/packages/1c/03/e834bcd866f2f8a49a85eaff47340affa3bfa391ee9912a952a1faa68c7b/secretstorage-3.5.0.tar.gz" +size = 19884 + +[packages.sdist.hashes] +sha256 = "f04b8e4689cbce351744d5537bf6b1329c6fc68f91fa666f60a380edddcd11be" + +[[packages.wheels]] +name = "secretstorage-3.5.0-py3-none-any.whl" +upload-time = 2025-11-23 19:02:51.545472+00:00 +url = "https://files.pythonhosted.org/packages/b7/46/f5af3402b579fd5e11573ce652019a67074317e18c1935cc0b4ba9b35552/secretstorage-3.5.0-py3-none-any.whl" +size = 15554 + +[packages.wheels.hashes] +sha256 = "0ce65888c0725fcb2c5bc0fdb8e5438eece02c523557ea40ce0703c266248137" + +[[packages]] +name = "setuptools" +version = "84.0.0" +marker = "\"build\" in dependency_groups" +requires-python = ">=3.10" +index = "https://pypi.org/simple/" + +[packages.sdist] +name = "setuptools-84.0.0.tar.gz" +upload-time = 2026-08-08 18:27:58.365696+00:00 +url = "https://files.pythonhosted.org/packages/6d/44/f5da03a8ef95d369145c5bb53050e7877c9f3d312e128605fd9504829143/setuptools-84.0.0.tar.gz" +size = 1168449 + +[packages.sdist.hashes] +sha256 = "f4695c21257f0d9b537ec2692c941d02ee143b7cc1276941349a546573b2ef73" + +[[packages.wheels]] +name = "setuptools-84.0.0-py3-none-any.whl" +upload-time = 2026-08-08 18:27:56.719708+00:00 +url = "https://files.pythonhosted.org/packages/95/9c/c510029fc6ef33a6275cd2c5d3cecd6613dfd6aa401d57c54f1c18852ccf/setuptools-84.0.0-py3-none-any.whl" +size = 818216 + +[packages.wheels.hashes] +sha256 = "51a52592b3b99e102b609654876bd65f19f999935166d1352678931132b0c670" + +[[packages]] +name = "setuptools-scm" +version = "10.3.4" +marker = "\"build\" in dependency_groups" +requires-python = ">=3.8" +dependencies = [ + { name = "packaging" }, + { name = "setuptools" }, + { name = "vcs-versioning" }, +] +index = "https://pypi.org/simple/" + +[packages.sdist] +name = "setuptools_scm-10.3.4.tar.gz" +upload-time = 2026-09-23 19:21:42.950372+00:00 +url = "https://files.pythonhosted.org/packages/85/d8/fc143f88819ccf10ba2388ba86732ee2de193e578234e25a783f6cc14bf7/setuptools_scm-10.3.4.tar.gz" +size = 78410 + +[packages.sdist.hashes] +sha256 = "a69f28bfc245608781205e912faae437c2b2165773afa4e7b979d77447a69dd2" + +[[packages.wheels]] +name = "setuptools_scm-10.3.4-py3-none-any.whl" +upload-time = 2026-09-23 19:21:41.607429+00:00 +url = "https://files.pythonhosted.org/packages/da/f5/54538a1f17ea753c42b0928dc2e62986d463f8c393340b9a1e602f2d4dd9/setuptools_scm-10.3.4-py3-none-any.whl" +size = 33366 + +[packages.wheels.hashes] +sha256 = "82f34c3e3084fc2b57d397200637cc13f2338004052d2fee47baa5f0e902464e" + +[[packages]] +name = "snowballstemmer" +version = "3.1.1" +marker = "\"dev\" in dependency_groups or \"docs\" in dependency_groups or \"type\" in dependency_groups" +requires-python = ">=3.3" +index = "https://pypi.org/simple/" + +[packages.sdist] +name = "snowballstemmer-3.1.1.tar.gz" +upload-time = 2026-06-03 00:56:40.194915+00:00 +url = "https://files.pythonhosted.org/packages/43/f8/0a71edf031f03c40db17503cb8ca78a69a171254e568e7db241b0ab57ea1/snowballstemmer-3.1.1.tar.gz" +size = 123314 + +[packages.sdist.hashes] +sha256 = "e07bbc54a0d798fe6010a12398422e62a8bfbba95c394fd0956ef58cb4d3e260" + +[[packages.wheels]] +name = "snowballstemmer-3.1.1-py3-none-any.whl" +upload-time = 2026-06-03 00:56:38.614300+00:00 +url = "https://files.pythonhosted.org/packages/4c/07/2ebca9b11fb9be7340a818d8d6f63feaebb146be2c4afbd6061701d6df6e/snowballstemmer-3.1.1-py3-none-any.whl" +size = 104164 + +[packages.wheels.hashes] +sha256 = "7e207fa178741da09cdee59d3ecec3827ad5f92b1fc5c9ff3755b639f71f5752" + +[[packages]] +name = "soupsieve" +version = "3.0.3" +marker = "\"dev\" in dependency_groups or \"docs\" in dependency_groups or \"type\" in dependency_groups" +requires-python = ">=3.11.5" +index = "https://pypi.org/simple/" + +[packages.sdist] +name = "soupsieve-3.0.3.tar.gz" +upload-time = 2026-10-12 13:21:17.696815+00:00 +url = "https://files.pythonhosted.org/packages/5e/77/2dcfa996b01702ab8fd0763d84098f6a640d6162a328f1c04c2697579a1a/soupsieve-3.0.3.tar.gz" +size = 115923 + +[packages.sdist.hashes] +sha256 = "7dcf6022eed0399eb9934a75e020148f7a2024c37b7dfcd3cf2c5505d69c364e" + +[[packages.wheels]] +name = "soupsieve-3.0.3-py3-none-any.whl" +upload-time = 2026-10-12 13:21:16.474827+00:00 +url = "https://files.pythonhosted.org/packages/49/ca/f639c80449997b88aba7bc9705d25dd76cc0844f45f187862fd8f8bb18fa/soupsieve-3.0.3-py3-none-any.whl" +size = 41705 + +[packages.wheels.hashes] +sha256 = "fa30e3ba4809cb81ce1f3209f2fbe3e779fc445f0439bc147a0d7c4601743f21" + +[[packages]] +name = "sphinx" +version = "9.1.0" +marker = "\"dev\" in dependency_groups or \"docs\" in dependency_groups or \"type\" in dependency_groups" +requires-python = ">=3.12" +dependencies = [ + { name = "alabaster" }, + { name = "babel" }, + { name = "docutils" }, + { name = "imagesize" }, + { name = "jinja2" }, + { name = "packaging" }, + { name = "pygments" }, + { name = "requests" }, + { name = "roman-numerals" }, + { name = "snowballstemmer" }, + { name = "sphinxcontrib-applehelp" }, + { name = "sphinxcontrib-devhelp" }, + { name = "sphinxcontrib-htmlhelp" }, + { name = "sphinxcontrib-jsmath" }, + { name = "sphinxcontrib-qthelp" }, + { name = "sphinxcontrib-serializinghtml" }, +] +index = "https://pypi.org/simple/" + +[packages.sdist] +name = "sphinx-9.1.0.tar.gz" +upload-time = 2025-12-31 15:09:27.646696+00:00 +url = "https://files.pythonhosted.org/packages/cd/bd/f08eb0f4eed5c83f1ba2a3bd18f7745a2b1525fad70660a1c00224ec468a/sphinx-9.1.0.tar.gz" +size = 8718324 + +[packages.sdist.hashes] +sha256 = "7741722357dd75f8190766926071fed3bdc211c74dd2d7d4df5404da95930ddb" + +[[packages.wheels]] +name = "sphinx-9.1.0-py3-none-any.whl" +upload-time = 2025-12-31 15:09:25.561651+00:00 +url = "https://files.pythonhosted.org/packages/73/f7/b1884cb3188ab181fc81fa00c266699dab600f927a964df02ec3d5d1916a/sphinx-9.1.0-py3-none-any.whl" +size = 3921742 + +[packages.wheels.hashes] +sha256 = "c84fdd4e782504495fe4f2c0b3413d6c2bf388589bb352d439b2a3bb99991978" + +[[packages]] +name = "sphinx-autodoc-typehints" +version = "3.17.2" +marker = "\"dev\" in dependency_groups or \"docs\" in dependency_groups or \"type\" in dependency_groups" +requires-python = ">=3.12" +dependencies = [ + { name = "sphinx" }, +] +index = "https://pypi.org/simple/" + +[packages.sdist] +name = "sphinx_autodoc_typehints-3.17.2.tar.gz" +upload-time = 2026-10-17 18:30:30.923136+00:00 +url = "https://files.pythonhosted.org/packages/9c/fa/c967fd665cc102c90338a90292aba336f852ee6f9b3477ffe4da565edb11/sphinx_autodoc_typehints-3.17.2.tar.gz" +size = 570767 + +[packages.sdist.hashes] +sha256 = "de41ed4001f4818bb6d37c22fd10ee44a93e6c8e945c6c7e3984e28a9df87d1c" + +[[packages.wheels]] +name = "sphinx_autodoc_typehints-3.17.2-py3-none-any.whl" +upload-time = 2026-10-17 18:30:28.728186+00:00 +url = "https://files.pythonhosted.org/packages/78/d4/8fc500ff2fafe8a6e1847abd8b1072494dc1a5f136f8e890e973db7de4eb/sphinx_autodoc_typehints-3.17.2-py3-none-any.whl" +size = 140285 + +[packages.wheels.hashes] +sha256 = "b0ad1f96503d69b63c2a1c043f12e2a727c9549e6503d4adbf2125dbf5ff50ed" + +[[packages]] +name = "sphinx-basic-ng" +version = "1.0.0b2" +marker = "\"dev\" in dependency_groups or \"docs\" in dependency_groups or \"type\" in dependency_groups" +requires-python = ">=3.7" +dependencies = [ + { name = "sphinx" }, +] +index = "https://pypi.org/simple/" + +[packages.sdist] +name = "sphinx_basic_ng-1.0.0b2.tar.gz" +upload-time = 2023-07-08 18:40:54.166146+00:00 +url = "https://files.pythonhosted.org/packages/98/0b/a866924ded68efec7a1759587a4e478aec7559d8165fac8b2ad1c0e774d6/sphinx_basic_ng-1.0.0b2.tar.gz" +size = 20736 + +[packages.sdist.hashes] +sha256 = "9ec55a47c90c8c002b5960c57492ec3021f5193cb26cebc2dc4ea226848651c9" + +[[packages.wheels]] +name = "sphinx_basic_ng-1.0.0b2-py3-none-any.whl" +upload-time = 2023-07-08 18:40:52.659879+00:00 +url = "https://files.pythonhosted.org/packages/3c/dd/018ce05c532a22007ac58d4f45232514cd9d6dd0ee1dc374e309db830983/sphinx_basic_ng-1.0.0b2-py3-none-any.whl" +size = 22496 + +[packages.wheels.hashes] +sha256 = "eb09aedbabfb650607e9b4b68c9d240b90b1e1be221d6ad71d61c52e29f7932b" + +[[packages]] +name = "sphinx-copybutton" +version = "0.5.2" +marker = "\"dev\" in dependency_groups or \"docs\" in dependency_groups or \"type\" in dependency_groups" +requires-python = ">=3.7" +dependencies = [ + { name = "sphinx" }, +] +index = "https://pypi.org/simple/" + +[packages.sdist] +name = "sphinx-copybutton-0.5.2.tar.gz" +upload-time = 2023-04-14 08:10:22.998763+00:00 +url = "https://files.pythonhosted.org/packages/fc/2b/a964715e7f5295f77509e59309959f4125122d648f86b4fe7d70ca1d882c/sphinx-copybutton-0.5.2.tar.gz" +size = 23039 + +[packages.sdist.hashes] +sha256 = "4cf17c82fb9646d1bc9ca92ac280813a3b605d8c421225fd9913154103ee1fbd" + +[[packages.wheels]] +name = "sphinx_copybutton-0.5.2-py3-none-any.whl" +upload-time = 2023-04-14 08:10:20.844249+00:00 +url = "https://files.pythonhosted.org/packages/9e/48/1ea60e74949eecb12cdd6ac43987f9fd331156388dcc2319b45e2ebb81bf/sphinx_copybutton-0.5.2-py3-none-any.whl" +size = 13343 + +[packages.wheels.hashes] +sha256 = "fb543fd386d917746c9a2c50360c7905b605726b9355cd26e9974857afeae06e" + +[[packages]] +name = "sphinx-design" +version = "0.7.0" +marker = "\"dev\" in dependency_groups or \"docs\" in dependency_groups or \"type\" in dependency_groups" +requires-python = ">=3.11" +dependencies = [ + { name = "sphinx" }, +] +index = "https://pypi.org/simple/" + +[packages.sdist] +name = "sphinx_design-0.7.0.tar.gz" +upload-time = 2026-01-19 13:12:53.297780+00:00 +url = "https://files.pythonhosted.org/packages/13/7b/804f311da4663a4aecc6cf7abd83443f3d4ded970826d0c958edc77d4527/sphinx_design-0.7.0.tar.gz" +size = 2203582 + +[packages.sdist.hashes] +sha256 = "d2a3f5b19c24b916adb52f97c5f00efab4009ca337812001109084a740ec9b7a" + +[[packages.wheels]] +name = "sphinx_design-0.7.0-py3-none-any.whl" +upload-time = 2026-01-19 13:12:51.077508+00:00 +url = "https://files.pythonhosted.org/packages/30/cf/45dd359f6ca0c3762ce0490f681da242f0530c49c81050c035c016bfdd3a/sphinx_design-0.7.0-py3-none-any.whl" +size = 2220350 + +[packages.wheels.hashes] +sha256 = "f82bf179951d58f55dca78ab3706aeafa496b741a91b1911d371441127d64282" + +[[packages]] +name = "sphinx-last-updated-by-git" +version = "0.3.8" +marker = "\"dev\" in dependency_groups or \"docs\" in dependency_groups or \"type\" in dependency_groups" +requires-python = ">=3.7" +dependencies = [ + { name = "sphinx" }, +] +index = "https://pypi.org/simple/" + +[packages.sdist] +name = "sphinx_last_updated_by_git-0.3.8.tar.gz" +upload-time = 2024-08-11 07:15:54.601652+00:00 +url = "https://files.pythonhosted.org/packages/03/fd/de1685b6dab173dff31da24e0d3b29f02873fc24a1cdbb7678721ddc8581/sphinx_last_updated_by_git-0.3.8.tar.gz" +size = 10785 + +[packages.sdist.hashes] +sha256 = "c145011f4609d841805b69a9300099fc02fed8f5bb9e5bcef77d97aea97b7761" + +[[packages.wheels]] +name = "sphinx_last_updated_by_git-0.3.8-py3-none-any.whl" +upload-time = 2024-08-11 07:15:53.244461+00:00 +url = "https://files.pythonhosted.org/packages/e1/fb/e496f16fa11fbe2dbdd0b5e306ede153dfed050aae4766fc89d500720dc7/sphinx_last_updated_by_git-0.3.8-py3-none-any.whl" +size = 8580 + +[packages.wheels.hashes] +sha256 = "6382c8285ac1f222483a58569b78c0371af5e55f7fbf9c01e5e8a72d6fdfa499" + +[[packages]] +name = "sphinx-llm" +version = "1.0.0" +marker = "\"dev\" in dependency_groups or \"docs\" in dependency_groups or \"type\" in dependency_groups" +requires-python = ">=3.9" +dependencies = [ + { name = "sphinx" }, + { name = "sphinx-markdown-builder" }, +] +index = "https://pypi.org/simple/" + +[packages.sdist] +name = "sphinx_llm-1.0.0.tar.gz" +upload-time = 2026-08-14 07:48:29.578550+00:00 +url = "https://files.pythonhosted.org/packages/33/58/6b1802820a17e7a128b45046d735f934af00f4290704b647b126213bdeb4/sphinx_llm-1.0.0.tar.gz" +size = 381976 + +[packages.sdist.hashes] +sha256 = "1cc8284343a0100caa3110010da5933cdad8da37d2b9dda0e2a7d503b8da6b1c" + +[[packages.wheels]] +name = "sphinx_llm-1.0.0-py3-none-any.whl" +upload-time = 2026-08-14 07:48:28.161181+00:00 +url = "https://files.pythonhosted.org/packages/af/5b/2a338974998bc0fe563635625abdb16a000ee06a63101b361c834cd56ccb/sphinx_llm-1.0.0-py3-none-any.whl" +size = 60282 + +[packages.wheels.hashes] +sha256 = "8d45dbe5f9caffb4d78589c35d15fc8399dd41fc5b445ee4292ff83969754e75" + +[[packages]] +name = "sphinx-markdown-builder" +version = "0.6.11" +marker = "\"dev\" in dependency_groups or \"docs\" in dependency_groups or \"type\" in dependency_groups" +requires-python = ">=3.7" +dependencies = [ + { name = "docutils" }, + { name = "sphinx" }, + { name = "tabulate" }, +] +index = "https://pypi.org/simple/" + +[packages.sdist] +name = "sphinx_markdown_builder-0.6.11.tar.gz" +upload-time = 2026-08-16 06:51:38.158799+00:00 +url = "https://files.pythonhosted.org/packages/ad/13/102d7134743120db25e636ffb57a30818e95a416583dca83aadac9c5dd87/sphinx_markdown_builder-0.6.11.tar.gz" +size = 24020 + +[packages.sdist.hashes] +sha256 = "ba3bf33e0e42ff30ae686ac73957a95e6d045790dcfbaca012dd8ae01794e577" + +[[packages.wheels]] +name = "sphinx_markdown_builder-0.6.11-py3-none-any.whl" +upload-time = 2026-08-16 06:51:36.810420+00:00 +url = "https://files.pythonhosted.org/packages/c9/71/06b415631c2e494c80546073b46013d1715dceb3abe30bd63b34b4e7d9ca/sphinx_markdown_builder-0.6.11-py3-none-any.whl" +size = 18088 + +[packages.wheels.hashes] +sha256 = "acfb94d6ee66c7a47da0ca0420578fa16f115812af25b8fd21755d5909aacf51" + +[[packages]] +name = "sphinx-sitemap" +version = "2.9.0" +marker = "\"dev\" in dependency_groups or \"docs\" in dependency_groups or \"type\" in dependency_groups" +dependencies = [ + { name = "sphinx-last-updated-by-git" }, +] +index = "https://pypi.org/simple/" + +[packages.sdist] +name = "sphinx_sitemap-2.9.0.tar.gz" +upload-time = 2025-10-06 00:24:00.036537+00:00 +url = "https://files.pythonhosted.org/packages/61/17/56fe0f65e3567f829b2b4153a622be1d4b222b781e0d90d7db5a7738f30f/sphinx_sitemap-2.9.0.tar.gz" +size = 6978 + +[packages.sdist.hashes] +sha256 = "70f97bcdf444e3d68e118355cf82a1f54c4d3c03d651cd17fe87398b26e25e21" + +[[packages.wheels]] +name = "sphinx_sitemap-2.9.0-py3-none-any.whl" +upload-time = 2025-10-06 00:23:58.778831+00:00 +url = "https://files.pythonhosted.org/packages/e4/94/3c57e8b1985e755c48972e2ecd59526d4bf0b52a1fe805bc52a8e98cb92d/sphinx_sitemap-2.9.0-py3-none-any.whl" +size = 6218 + +[packages.wheels.hashes] +sha256 = "f1f1d3a9ad012ba17a7ef0b560d303bff2d0db26647567d6e810bcc754466664" + +[[packages]] +name = "sphinxcontrib-applehelp" +version = "2.0.0" +marker = "\"dev\" in dependency_groups or \"docs\" in dependency_groups or \"type\" in dependency_groups" +requires-python = ">=3.9" +index = "https://pypi.org/simple/" + +[packages.sdist] +name = "sphinxcontrib_applehelp-2.0.0.tar.gz" +upload-time = 2024-07-29 01:09:00.465812+00:00 +url = "https://files.pythonhosted.org/packages/ba/6e/b837e84a1a704953c62ef8776d45c3e8d759876b4a84fe14eba2859106fe/sphinxcontrib_applehelp-2.0.0.tar.gz" +size = 20053 + +[packages.sdist.hashes] +sha256 = "2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1" + +[[packages.wheels]] +name = "sphinxcontrib_applehelp-2.0.0-py3-none-any.whl" +upload-time = 2024-07-29 01:08:58.990722+00:00 +url = "https://files.pythonhosted.org/packages/5d/85/9ebeae2f76e9e77b952f4b274c27238156eae7979c5421fba91a28f4970d/sphinxcontrib_applehelp-2.0.0-py3-none-any.whl" +size = 119300 + +[packages.wheels.hashes] +sha256 = "4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5" + +[[packages]] +name = "sphinxcontrib-devhelp" +version = "2.0.0" +marker = "\"dev\" in dependency_groups or \"docs\" in dependency_groups or \"type\" in dependency_groups" +requires-python = ">=3.9" +index = "https://pypi.org/simple/" + +[packages.sdist] +name = "sphinxcontrib_devhelp-2.0.0.tar.gz" +upload-time = 2024-07-29 01:09:23.417467+00:00 +url = "https://files.pythonhosted.org/packages/f6/d2/5beee64d3e4e747f316bae86b55943f51e82bb86ecd325883ef65741e7da/sphinxcontrib_devhelp-2.0.0.tar.gz" +size = 12967 + +[packages.sdist.hashes] +sha256 = "411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad" + +[[packages.wheels]] +name = "sphinxcontrib_devhelp-2.0.0-py3-none-any.whl" +upload-time = 2024-07-29 01:09:21.945229+00:00 +url = "https://files.pythonhosted.org/packages/35/7a/987e583882f985fe4d7323774889ec58049171828b58c2217e7f79cdf44e/sphinxcontrib_devhelp-2.0.0-py3-none-any.whl" +size = 82530 + +[packages.wheels.hashes] +sha256 = "aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2" + +[[packages]] +name = "sphinxcontrib-htmlhelp" +version = "2.1.0" +marker = "\"dev\" in dependency_groups or \"docs\" in dependency_groups or \"type\" in dependency_groups" +requires-python = ">=3.9" +index = "https://pypi.org/simple/" + +[packages.sdist] +name = "sphinxcontrib_htmlhelp-2.1.0.tar.gz" +upload-time = 2024-07-29 01:09:37.889318+00:00 +url = "https://files.pythonhosted.org/packages/43/93/983afd9aa001e5201eab16b5a444ed5b9b0a7a010541e0ddfbbfd0b2470c/sphinxcontrib_htmlhelp-2.1.0.tar.gz" +size = 22617 + +[packages.sdist.hashes] +sha256 = "c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9" + +[[packages.wheels]] +name = "sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl" +upload-time = 2024-07-29 01:09:36.407072+00:00 +url = "https://files.pythonhosted.org/packages/0a/7b/18a8c0bcec9182c05a0b3ec2a776bba4ead82750a55ff798e8d406dae604/sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl" +size = 98705 + +[packages.wheels.hashes] +sha256 = "166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8" + +[[packages]] +name = "sphinxcontrib-jsmath" +version = "1.0.1" +marker = "\"dev\" in dependency_groups or \"docs\" in dependency_groups or \"type\" in dependency_groups" +requires-python = ">=3.5" +index = "https://pypi.org/simple/" + +[packages.sdist] +name = "sphinxcontrib-jsmath-1.0.1.tar.gz" +upload-time = 2019-01-21 16:10:16.347697+00:00 +url = "https://files.pythonhosted.org/packages/b2/e8/9ed3830aeed71f17c026a07a5097edcf44b692850ef215b161b8ad875729/sphinxcontrib-jsmath-1.0.1.tar.gz" +size = 5787 + +[packages.sdist.hashes] +sha256 = "a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8" + +[[packages.wheels]] +name = "sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl" +upload-time = 2019-01-21 16:10:14.333769+00:00 +url = "https://files.pythonhosted.org/packages/c2/42/4c8646762ee83602e3fb3fbe774c2fac12f317deb0b5dbeeedd2d3ba4b77/sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl" +size = 5071 + +[packages.wheels.hashes] +sha256 = "2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178" + +[[packages]] +name = "sphinxcontrib-mermaid" +version = "2.1.1" +marker = "\"dev\" in dependency_groups or \"docs\" in dependency_groups or \"type\" in dependency_groups" +requires-python = ">=3.10" +dependencies = [ + { name = "jinja2" }, + { name = "pyyaml" }, + { name = "sphinx" }, +] +index = "https://pypi.org/simple/" + +[packages.sdist] +name = "sphinxcontrib_mermaid-2.1.1.tar.gz" +upload-time = 2026-09-01 16:10:23.671939+00:00 +url = "https://files.pythonhosted.org/packages/8c/87/06537dff20aa57df2d0f2afa87edff4afa85dec6f57f03f7acc24f83ff88/sphinxcontrib_mermaid-2.1.1.tar.gz" +size = 22353 + +[packages.sdist.hashes] +sha256 = "f8d7f1b2f0ad69b7ebdd5cbf532fb4ee107893b5373bc2d0e9634089c0b2cfee" + +[[packages.wheels]] +name = "sphinxcontrib_mermaid-2.1.1-py3-none-any.whl" +upload-time = 2026-09-01 16:10:22.377054+00:00 +url = "https://files.pythonhosted.org/packages/50/6a/5bf2222b8084c7a7bc3ec23d470c91e3e03e82e4ce0bbacaa61f456ce7c1/sphinxcontrib_mermaid-2.1.1-py3-none-any.whl" +size = 16368 + +[packages.wheels.hashes] +sha256 = "3b9ac1a8e0b5154dec3c0acf9b5c54dd9ba58540e416f20858c300814c25648f" + +[[packages]] +name = "sphinxcontrib-qthelp" +version = "2.0.0" +marker = "\"dev\" in dependency_groups or \"docs\" in dependency_groups or \"type\" in dependency_groups" +requires-python = ">=3.9" +index = "https://pypi.org/simple/" + +[packages.sdist] +name = "sphinxcontrib_qthelp-2.0.0.tar.gz" +upload-time = 2024-07-29 01:09:56.435212+00:00 +url = "https://files.pythonhosted.org/packages/68/bc/9104308fc285eb3e0b31b67688235db556cd5b0ef31d96f30e45f2e51cae/sphinxcontrib_qthelp-2.0.0.tar.gz" +size = 17165 + +[packages.sdist.hashes] +sha256 = "4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab" + +[[packages.wheels]] +name = "sphinxcontrib_qthelp-2.0.0-py3-none-any.whl" +upload-time = 2024-07-29 01:09:54.885300+00:00 +url = "https://files.pythonhosted.org/packages/27/83/859ecdd180cacc13b1f7e857abf8582a64552ea7a061057a6c716e790fce/sphinxcontrib_qthelp-2.0.0-py3-none-any.whl" +size = 88743 + +[packages.wheels.hashes] +sha256 = "b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb" + +[[packages]] +name = "sphinxcontrib-serializinghtml" +version = "2.0.0" +marker = "\"dev\" in dependency_groups or \"docs\" in dependency_groups or \"type\" in dependency_groups" +requires-python = ">=3.9" +index = "https://pypi.org/simple/" + +[packages.sdist] +name = "sphinxcontrib_serializinghtml-2.0.0.tar.gz" +upload-time = 2024-07-29 01:10:09.332393+00:00 +url = "https://files.pythonhosted.org/packages/3b/44/6716b257b0aa6bfd51a1b31665d1c205fb12cb5ad56de752dfa15657de2f/sphinxcontrib_serializinghtml-2.0.0.tar.gz" +size = 16080 + +[packages.sdist.hashes] +sha256 = "e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d" + +[[packages.wheels]] +name = "sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl" +upload-time = 2024-07-29 01:10:08.203690+00:00 +url = "https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl" +size = 92072 + +[packages.wheels.hashes] +sha256 = "6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331" + +[[packages]] +name = "sphinxcontrib-towncrier" +version = "0.5.0a0" +marker = "\"dev\" in dependency_groups or \"docs\" in dependency_groups or \"type\" in dependency_groups" +requires-python = ">=3.9" +dependencies = [ + { name = "sphinx" }, + { name = "towncrier" }, +] +index = "https://pypi.org/simple/" + +[packages.sdist] +name = "sphinxcontrib_towncrier-0.5.0a0.tar.gz" +upload-time = 2025-02-28 01:59:16.894775+00:00 +url = "https://files.pythonhosted.org/packages/21/fe/72ed57093e28af10595c50839b183c5fdf0952482e9ef0ca6eb90eb85c5d/sphinxcontrib_towncrier-0.5.0a0.tar.gz" +size = 62453 + +[packages.sdist.hashes] +sha256 = "294e69df6e275e7a86df7ea6a927cc7c28c2c370a884cd5c45de6ec989858f27" + +[[packages.wheels]] +name = "sphinxcontrib_towncrier-0.5.0a0-py3-none-any.whl" +upload-time = 2025-02-28 01:59:15.178594+00:00 +url = "https://files.pythonhosted.org/packages/ac/5c/f7e39f243636a5e1894f2f5a72579977bf3968922afdb75175ee45062066/sphinxcontrib_towncrier-0.5.0a0-py3-none-any.whl" +size = 12609 + +[packages.wheels.hashes] +sha256 = "11d130c3ad5e4649821d543c4ea7ab64bbe78df4d859ef94f4298e7845dc0f59" + +[[packages]] +name = "sphinxext-opengraph" +version = "0.13.0" +marker = "\"dev\" in dependency_groups or \"docs\" in dependency_groups or \"type\" in dependency_groups" +requires-python = ">=3.9" +dependencies = [ + { name = "sphinx" }, +] +index = "https://pypi.org/simple/" + +[packages.sdist] +name = "sphinxext_opengraph-0.13.0.tar.gz" +upload-time = 2025-08-29 12:20:31.066405+00:00 +url = "https://files.pythonhosted.org/packages/f6/c0/eb6838e3bae624ce6c8b90b245d17e84252863150e95efdb88f92c8aa3fb/sphinxext_opengraph-0.13.0.tar.gz" +size = 1026875 + +[packages.sdist.hashes] +sha256 = "103335d08567ad8468faf1425f575e3b698e9621f9323949a6c8b96d9793e80b" + +[[packages.wheels]] +name = "sphinxext_opengraph-0.13.0-py3-none-any.whl" +upload-time = 2025-08-29 12:20:29.072834+00:00 +url = "https://files.pythonhosted.org/packages/bf/a4/66c1fd4f8fab88faf71cee04a945f9806ba0fef753f2cfc8be6353f64508/sphinxext_opengraph-0.13.0-py3-none-any.whl" +size = 1004152 + +[packages.wheels.hashes] +sha256 = "936c07828edc9ad9a7b07908b29596dc84ed0b3ceaa77acdf51282d232d4d80e" + +[[packages]] +name = "tabulate" +version = "0.10.0" +marker = "\"dev\" in dependency_groups or \"docs\" in dependency_groups or \"type\" in dependency_groups" +requires-python = ">=3.10" +index = "https://pypi.org/simple/" + +[packages.sdist] +name = "tabulate-0.10.0.tar.gz" +upload-time = 2026-03-04 18:55:34.402969+00:00 +url = "https://files.pythonhosted.org/packages/46/58/8c37dea7bbf769b20d58e7ace7e5edfe65b849442b00ffcdd56be88697c6/tabulate-0.10.0.tar.gz" +size = 91754 + +[packages.sdist.hashes] +sha256 = "e2cfde8f79420f6deeffdeda9aaec3b6bc5abce947655d17ac662b126e48a60d" + +[[packages.wheels]] +name = "tabulate-0.10.0-py3-none-any.whl" +upload-time = 2026-03-04 18:55:31.284751+00:00 +url = "https://files.pythonhosted.org/packages/99/55/db07de81b5c630da5cbf5c7df646580ca26dfaefa593667fc6f2fe016d2e/tabulate-0.10.0-py3-none-any.whl" +size = 39814 + +[packages.wheels.hashes] +sha256 = "f0b0622e567335c8fabaaa659f1b33bcb6ddfe2e496071b743aa113f8774f2d3" + +[[packages]] +name = "tomlkit" +version = "0.15.2" +marker = "\"build\" in dependency_groups" +requires-python = ">=3.9" +index = "https://pypi.org/simple/" + +[packages.sdist] +name = "tomlkit-0.15.2.tar.gz" +upload-time = 2026-10-16 04:07:08.166827+00:00 +url = "https://files.pythonhosted.org/packages/08/72/fc01563df95c189a9f65850096bdc205bc2fe37da6c740cd64afaf18d07e/tomlkit-0.15.2.tar.gz" +size = 190522 + +[packages.sdist.hashes] +sha256 = "83e32755ba1d20d9340bd069ee97e9f6232ba2346b63ffcba425de01f7e327a6" + +[[packages.wheels]] +name = "tomlkit-0.15.2-py3-none-any.whl" +upload-time = 2026-10-16 04:07:09.216014+00:00 +url = "https://files.pythonhosted.org/packages/da/46/7d84138a60a8bd796825b2d38ebe92d0d16501ca57d501b7c5d677401bbc/tomlkit-0.15.2-py3-none-any.whl" +size = 52225 + +[packages.wheels.hashes] +sha256 = "05562412163a3037b94497385fb7f9204c637bc605af7a48199e41cf2272f437" + +[[packages]] +name = "towncrier" +version = "26.9.0" +marker = "\"dev\" in dependency_groups or \"docs\" in dependency_groups or \"release\" in dependency_groups or \"release-news\" in dependency_groups or \"release-notes\" in dependency_groups or \"release-prepare\" in dependency_groups or \"type\" in dependency_groups" +requires-python = ">=3.9" +dependencies = [ + { name = "click" }, + { name = "jinja2" }, +] +index = "https://pypi.org/simple/" + +[packages.sdist] +name = "towncrier-26.9.0.tar.gz" +upload-time = 2026-09-04 12:57:25.341701+00:00 +url = "https://files.pythonhosted.org/packages/92/d4/e85ce614bf45b1a7fe13b82cc687819c2eaac5a1dd94b84834322095fd98/towncrier-26.9.0.tar.gz" +size = 79338 + +[packages.sdist.hashes] +sha256 = "ace9031631c718cc0709107b8755e6e31350eb8acdc877af43df3177addb9a55" + +[[packages.wheels]] +name = "towncrier-26.9.0-py3-none-any.whl" +upload-time = 2026-09-04 12:57:23.986160+00:00 +url = "https://files.pythonhosted.org/packages/82/88/f9340b545dafa64de053798e760515a2736fbb60092cf9aa6b146ccf3ede/towncrier-26.9.0-py3-none-any.whl" +size = 67030 + +[packages.wheels.hashes] +sha256 = "ae4d223e6aadaff98d29b7f09e58d9c9ccf4cd3b455a2d2e0486d432cd8bf660" + +[[packages]] +name = "trove-classifiers" +version = "2026.9.21.13" +marker = "\"build\" in dependency_groups" +index = "https://pypi.org/simple/" + +[packages.sdist] +name = "trove_classifiers-2026.9.21.13.tar.gz" +upload-time = 2026-09-21 13:29:07.301512+00:00 +url = "https://files.pythonhosted.org/packages/bf/93/af436dfaa845cab5d96f0adbc1e4f3730532d37fa249e4eb796fb1d7fc82/trove_classifiers-2026.9.21.13.tar.gz" +size = 17085 + +[packages.sdist.hashes] +sha256 = "0a9ebc8d4e2f3e8a22848c5258033035bec17a3012ac3fea16dbaa764489eb71" + +[[packages.wheels]] +name = "trove_classifiers-2026.9.21.13-py3-none-any.whl" +upload-time = 2026-09-21 13:29:06.123578+00:00 +url = "https://files.pythonhosted.org/packages/30/81/0da8afb52a71d0a4f2bd3152357b1a441e393b286374802b9d3addab4ab5/trove_classifiers-2026.9.21.13-py3-none-any.whl" +size = 14232 + +[packages.wheels.hashes] +sha256 = "8b1ff4f9c191b1040b71c37f1e445ab99732911e3cd91de52838453a854d7a17" + +[[packages]] +name = "twine" +version = "7.0.0" +marker = "\"dev\" in dependency_groups or \"pkg-meta\" in dependency_groups" +requires-python = ">=3.10" +dependencies = [ + { name = "id" }, + { name = "keyring" }, + { name = "packaging" }, + { name = "readme-renderer" }, + { name = "requests" }, + { name = "requests-toolbelt" }, + { name = "rfc3986" }, + { name = "rich" }, + { name = "urllib3" }, +] +index = "https://pypi.org/simple/" + +[packages.sdist] +name = "twine-7.0.0.tar.gz" +upload-time = 2026-07-27 15:59:00.825945+00:00 +url = "https://files.pythonhosted.org/packages/92/3c/58f808a359700f39a967dffede33efeac809262c03303fa3eec6afff8f49/twine-7.0.0.tar.gz" +size = 215032 + +[packages.sdist.hashes] +sha256 = "85cdb29c518efef867360ae4acd4b0dfd61c8654a22fca08e6f8539f05022177" + +[[packages.wheels]] +name = "twine-7.0.0-py3-none-any.whl" +upload-time = 2026-07-27 15:58:59.260418+00:00 +url = "https://files.pythonhosted.org/packages/96/08/ddcdc06225eaad6de0e48e1002b06d919dbde20582d0662c7af51308e5d6/twine-7.0.0-py3-none-any.whl" +size = 43204 + +[packages.wheels.hashes] +sha256 = "b854164df26db268af05f49aa5c0344b10e27a494343ff05b1e0bad3b135f5a7" + +[[packages]] +name = "ty" +version = "0.0.89" +marker = "\"dev\" in dependency_groups or \"type\" in dependency_groups" +requires-python = ">=3.8" +index = "https://pypi.org/simple/" + +[packages.sdist] +name = "ty-0.0.89.tar.gz" +upload-time = 2026-10-16 10:35:05.249809+00:00 +url = "https://files.pythonhosted.org/packages/67/ea/dfb9bdbc168c50a568809466596751afa0c7da60da03b8c12906d1decde4/ty-0.0.89.tar.gz" +size = 7978241 + +[packages.sdist.hashes] +sha256 = "bc1027b1ea95cca25c2a3a0b41433c809cf4b86a384d2b2434eba4bcd34548d4" + +[[packages.wheels]] +name = "ty-0.0.89-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" +upload-time = 2026-10-16 10:34:44.063367+00:00 +url = "https://files.pythonhosted.org/packages/3e/61/da22c714fd4f4dff642d60fb0d8bf8da1985d126f21cc89237f8bf707c92/ty-0.0.89-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" +size = 14510395 + +[packages.wheels.hashes] +sha256 = "95a410f10b26cc3861757b626727db9d979deba7d2a3ab59f964fac8f0514fdc" + +[[packages]] +name = "typing-extensions" +version = "4.16.0" +marker = "\"dev\" in dependency_groups or \"docs\" in dependency_groups or \"pkg-meta\" in dependency_groups or \"type\" in dependency_groups" +requires-python = ">=3.9" +index = "https://pypi.org/simple/" + +[packages.sdist] +name = "typing_extensions-4.16.0.tar.gz" +upload-time = 2026-07-02 08:40:05.920538+00:00 +url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz" +size = 113555 + +[packages.sdist.hashes] +sha256 = "dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5" + +[[packages.wheels]] +name = "typing_extensions-4.16.0-py3-none-any.whl" +upload-time = 2026-07-02 08:40:04.659120+00:00 +url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl" +size = 45571 + +[packages.wheels.hashes] +sha256 = "481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8" + +[[packages]] +name = "typing-inspection" +version = "0.4.4" +marker = "\"dev\" in dependency_groups or \"pkg-meta\" in dependency_groups" +requires-python = ">=3.10" +dependencies = [ + { name = "typing-extensions" }, +] +index = "https://pypi.org/simple/" + +[packages.sdist] +name = "typing_inspection-0.4.4.tar.gz" +upload-time = 2026-08-12 12:37:25.997329+00:00 +url = "https://files.pythonhosted.org/packages/a3/26/b09b8010994eccc3c09092e6b34058f36a460eea2d4c3e8b910c695975a0/typing_inspection-0.4.4.tar.gz" +size = 76928 + +[packages.sdist.hashes] +sha256 = "547274fa6b0a561ccf549cc9524b999a578e737d015d8709d021f9d0d13bea47" + +[[packages.wheels]] +name = "typing_inspection-0.4.4-py3-none-any.whl" +upload-time = 2026-08-12 12:37:24.648475+00:00 +url = "https://files.pythonhosted.org/packages/67/81/4add07e5172b7ac40d8ed5ff580409a7801a4fe26d529bdd915401dabfbe/typing_inspection-0.4.4-py3-none-any.whl" +size = 14750 + +[packages.wheels.hashes] +sha256 = "65b8397ba37ccbce054456aaccddfc91e6e3083c92824df348d96ca832f3f147" + +[[packages]] +name = "urllib3" +version = "2.8.0" +marker = "\"dev\" in dependency_groups or \"docs\" in dependency_groups or \"pkg-meta\" in dependency_groups or \"type\" in dependency_groups" +requires-python = ">=3.10" +index = "https://pypi.org/simple/" + +[packages.sdist] +name = "urllib3-2.8.0.tar.gz" +upload-time = 2026-09-15 19:29:36.253420+00:00 +url = "https://files.pythonhosted.org/packages/e3/05/b17359e1cefb4f909b5e40b1b90a496d987258916dbbf88e842c729f510e/urllib3-2.8.0.tar.gz" +size = 458972 + +[packages.sdist.hashes] +sha256 = "63bf2ead4c879426ebf22ef2a781eeb4aa3b4ae798a0435506f8687fd5bb9b63" + +[[packages.wheels]] +name = "urllib3-2.8.0-py3-none-any.whl" +upload-time = 2026-09-15 19:29:34.577402+00:00 +url = "https://files.pythonhosted.org/packages/92/9d/c4e665119135114480843e7ab388fa94d8480650450e6f8e26b70d323a4c/urllib3-2.8.0-py3-none-any.whl" +size = 135717 + +[packages.wheels.hashes] +sha256 = "0cf3cae568d36aa9576b28dfb35f11328f1cb974ca7647d9475ebb86c75ac6e3" + +[[packages]] +name = "uv" +version = "0.13.1" +marker = "\"dev\" in dependency_groups or \"fix\" in dependency_groups or \"pkg-meta\" in dependency_groups" +requires-python = ">=3.8" +index = "https://pypi.org/simple/" + +[packages.sdist] +name = "uv-0.13.1.tar.gz" +upload-time = 2026-10-14 05:13:35.353145+00:00 +url = "https://files.pythonhosted.org/packages/ef/10/81f499de381ac81e09961e1c19255c5cf49563ac418464def4190b6c7400/uv-0.13.1.tar.gz" +size = 7434239 + +[packages.sdist.hashes] +sha256 = "6df50a86b09be66a413f8f1b16258d585d4dd1cbff591077ae1e6f65c26de2eb" + +[[packages.wheels]] +name = "uv-0.13.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" +upload-time = 2026-10-14 05:13:13.069021+00:00 +url = "https://files.pythonhosted.org/packages/14/0b/6602a613533602e7a083e908c4375f9b0e78a94ba2e21c29b21c41a0a611/uv-0.13.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" +size = 17279385 + +[packages.wheels.hashes] +sha256 = "696dcf6d807d85da978f4db8472da81dccc1254b18b52cbd1367c4d923c421e0" + +[[packages]] +name = "vcs-versioning" +version = "2.6.0" +marker = "\"build\" in dependency_groups" +requires-python = ">=3.8" +dependencies = [ + { name = "packaging" }, +] +index = "https://pypi.org/simple/" + +[packages.sdist] +name = "vcs_versioning-2.6.0.tar.gz" +upload-time = 2026-10-06 22:21:55.024903+00:00 +url = "https://files.pythonhosted.org/packages/ad/e7/2a691db7d076b75d099fdba1a14cd966931f2aeb591e77f6d22f15097326/vcs_versioning-2.6.0.tar.gz" +size = 166800 + +[packages.sdist.hashes] +sha256 = "22e9159288e2d8bca2fa6f6c31c34c9c66aec157f60e9e526f5d160d2516d60f" + +[[packages.wheels]] +name = "vcs_versioning-2.6.0-py3-none-any.whl" +upload-time = 2026-10-06 22:21:53.267565+00:00 +url = "https://files.pythonhosted.org/packages/73/1d/ad4064bf12f4503bf61e65d74c93c523672776d25ee0a06aaf42ae09ef3f/vcs_versioning-2.6.0-py3-none-any.whl" +size = 123154 + +[packages.wheels.hashes] +sha256 = "f8bebd1302ebc12e67ceb30fc7dd698ec969414b6fa43b30c3f14ac9b1e72c41" + +[[packages]] +name = "virtualenv" +version = "21.14.11" +marker = "\"dev\" in dependency_groups or \"fix\" in dependency_groups or \"test\" in dependency_groups or \"type\" in dependency_groups" +requires-python = ">=3.9" +dependencies = [ + { name = "distlib" }, + { name = "filelock" }, + { name = "packaging" }, + { name = "platformdirs" }, + { name = "python-discovery" }, +] +index = "https://pypi.org/simple/" + +[packages.sdist] +name = "virtualenv-21.14.11.tar.gz" +upload-time = 2026-10-18 07:06:34.716606+00:00 +url = "https://files.pythonhosted.org/packages/7f/be/4d1d59d309db76b5808fcfd78bd5996f4e7241366691c38fc331a3136398/virtualenv-21.14.11.tar.gz" +size = 5503091 + +[packages.sdist.hashes] +sha256 = "593052d8477ea8c55a842387ce111bd08c72618f0c1133c302cafe335e570a57" + +[[packages.wheels]] +name = "virtualenv-21.14.11-py3-none-any.whl" +upload-time = 2026-10-18 07:06:32.719110+00:00 +url = "https://files.pythonhosted.org/packages/ce/6b/d85da8982b41190eafcb9df91dff9d83fc6989e98af4b5be4c8f85510c95/virtualenv-21.14.11-py3-none-any.whl" +size = 5495268 + +[packages.wheels.hashes] +sha256 = "b319f8590a84e5de5f8f2c803ecd2237e9bc0ffa7c1e875e3169c107cecb7454" + +[[packages]] +name = "wcwidth" +version = "0.9.2" +marker = "\"release\" in dependency_groups or \"release-news\" in dependency_groups or \"release-notes\" in dependency_groups or \"release-prepare\" in dependency_groups" +requires-python = ">=3.9" +index = "https://pypi.org/simple/" + +[packages.sdist] +name = "wcwidth-0.9.2.tar.gz" +upload-time = 2026-10-05 00:24:05.521972+00:00 +url = "https://files.pythonhosted.org/packages/f0/b4/7830542634bb2d3e62aa3b586a72d5b3b6c91c3168929e7000ef3fed041d/wcwidth-0.9.2.tar.gz" +size = 955039 + +[packages.sdist.hashes] +sha256 = "ae0ef90b90f6af38b54f1fe6d58662ec33b3cb4b8391958a62416d654231727b" + +[[packages.wheels]] +name = "wcwidth-0.9.2-cp310-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl" +upload-time = 2026-10-05 00:23:40.175985+00:00 +url = "https://files.pythonhosted.org/packages/db/6c/f940133c71427c208575910e981942bd78c98b1f7cd0d1425ca4b7457c04/wcwidth-0.9.2-cp310-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl" +size = 769763 + +[packages.wheels.hashes] +sha256 = "751bef0ab404b6a1dc028b56b4b85d46486be1c55833f80da533e42dc691f389" + +[[packages.wheels]] +name = "wcwidth-0.9.2-py3-none-any.whl" +upload-time = 2026-10-05 00:23:21.097514+00:00 +url = "https://files.pythonhosted.org/packages/9c/b4/0bfa065af506540d9d558e3e5548cff00bc1f9b24e6e2a8512498e8628de/wcwidth-0.9.2-py3-none-any.whl" +size = 301667 + +[packages.wheels.hashes] +sha256 = "89ca642c5bf0101157a09366be69fad0379db1f700ae39a920e103234573670e" + +[[packages]] +name = "wheel-filename" +version = "1.4.2" +marker = "\"dev\" in dependency_groups or \"pkg-meta\" in dependency_groups" +requires-python = ">=3.8" +index = "https://pypi.org/simple/" + +[packages.sdist] +name = "wheel_filename-1.4.2.tar.gz" +upload-time = 2024-12-01 13:03:16.012739+00:00 +url = "https://files.pythonhosted.org/packages/38/be/726dab762b770d0417e505c58e26d661aac1ec0c831e483cda4817ca2417/wheel_filename-1.4.2.tar.gz" +size = 7911 + +[packages.sdist.hashes] +sha256 = "87891c465dcbb40b40394a906f01a93214bdd51aa5d25e3a9a59cae62bc298fd" + +[[packages.wheels]] +name = "wheel_filename-1.4.2-py3-none-any.whl" +upload-time = 2024-12-01 13:03:00.536117+00:00 +url = "https://files.pythonhosted.org/packages/b4/0f/6e97a3bc38cdde32e3ec49f8c0903fe3559ec9ec9db181782f0bb4417717/wheel_filename-1.4.2-py3-none-any.whl" +size = 6195 + +[packages.wheels.hashes] +sha256 = "3fa599046443d4ca830d06e3d180cd0a675d5871af0a68daa5623318bb4d17e3" + +[tool.nab] +nab-version = "0.0.22" +created-at = 2026-10-26 20:06:10.486122+00:00 +command-line = [ + "nab", + "lock", + "--all-groups", + "--all-extras", + "--no-cache", + "--upgrade", + "pyproject.toml", + "--output", + "pylock.toml", +] +input-path = "pyproject.toml" +mode = "universal" +python-specifier = ">=3.15,<3.16" +platforms = [ + "linux_x86_64", +] diff --git a/pyproject.toml b/pyproject.toml index 5ff751a6..cb889278 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,20 +1,19 @@ [build-system] build-backend = "hatchling.build" requires = [ + "hatch-fancy-pypi-readme>=25.1", "hatch-vcs>=0.5", "hatchling>=1.29", ] [project] name = "filelock" +import-names = [ "filelock" ] description = "A platform independent file lock." -readme = "README.md" keywords = [ - "application", - "cache", - "directory", - "log", - "user", + "file", + "lock", + "synchronization", ] license = "MIT" maintainers = [ @@ -24,7 +23,6 @@ requires-python = ">=3.11" classifiers = [ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", - "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3 :: Only", @@ -33,17 +31,18 @@ classifiers = [ "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", "Programming Language :: Python :: 3.15", - "Topic :: Internet", "Topic :: Software Development :: Libraries", "Topic :: System", ] dynamic = [ + "readme", "version", ] +urls.Changelog = "https://py-filelock.readthedocs.io/en/latest/changelog.html" urls.Documentation = "https://py-filelock.readthedocs.io" -urls.Homepage = "https://github.com/tox-dev/py-filelock" -urls.Source = "https://github.com/tox-dev/py-filelock" -urls.Tracker = "https://github.com/tox-dev/py-filelock/issues" +urls.Homepage = "https://github.com/tox-dev/filelock" +urls.Source = "https://github.com/tox-dev/filelock" +urls.Tracker = "https://github.com/tox-dev/filelock/issues" [dependency-groups] dev = [ @@ -51,7 +50,6 @@ dev = [ { include-group = "docs" }, { include-group = "fix" }, { include-group = "pkg-meta" }, - { include-group = "release" }, { include-group = "test" }, { include-group = "type" }, ] @@ -69,7 +67,6 @@ test = [ type = [ "ty>=0.0.30", { include-group = "docs" }, - { include-group = "release" }, { include-group = "test" }, ] docs = [ @@ -85,6 +82,13 @@ docs = [ "sphinxext-opengraph>=0.13", "towncrier>=24.8", ] +build = [ + "cython>=3", + "hatch-fancy-pypi-readme>=25.1", + "hatch-vcs>=0.5", + "hatchling>=1.29", + "setuptools>=80", +] coverage = [ "covdefaults>=2.3", "coverage[toml]>=7.13.5", @@ -99,11 +103,27 @@ pkg-meta = [ "uv>=0.11.5", ] release = [ - "towncrier>=24.8", + "prek>=0.5", + "release-creator>=4.1", + "towncrier>=26.9", +] +release-news = [ + "release-creator>=4.1", + "towncrier>=26.9", +] +release-notes = [ + "release-creator>=4.1", +] +release-prepare = [ + "prek>=0.5", + "release-creator>=4.1", + "towncrier>=26.9", ] [tool.hatch] version.source = "vcs" +metadata.hooks.fancy-pypi-readme.content-type = "text/markdown" +metadata.hooks.fancy-pypi-readme.fragments = [ { path = "README.md" } ] build.hooks.vcs.version-file = "src/filelock/version.py" build.targets.sdist.include = [ "/docs", @@ -113,6 +133,15 @@ build.targets.sdist.include = [ "/tox.toml", ] +[tool.uv] +dependency-groups.dev = { requires-python = ">=3.12" } +dependency-groups.docs = { requires-python = ">=3.12" } +dependency-groups.release = { requires-python = ">=3.15" } +dependency-groups.release-news = { requires-python = ">=3.15" } +dependency-groups.release-notes = { requires-python = ">=3.15" } +dependency-groups.release-prepare = { requires-python = ">=3.15" } +dependency-groups.type = { requires-python = ">=3.12" } + [tool.ruff] line-length = 120 format.preview = true @@ -247,15 +276,21 @@ html.skip_covered = false name = "filelock" directory = "docs/changelog" filename = "docs/changelog.rst" -template = "docs/changelog/template.jinja2" +template = "release_creator.fragments:template.rst.jinja2" title_format = false issue_format = ":pr:`{issue}`" +underlines = [ "*", "=", "-" ] type = [ - { directory = "breaking", name = "Backward incompatible changes", showcontent = true }, - { directory = "feature", name = "Features", showcontent = true }, - { directory = "bugfix", name = "Bug fixes", showcontent = true }, - { directory = "doc", name = "Improved documentation", showcontent = true }, - { directory = "packaging", name = "Packaging updates", showcontent = true }, + { directory = "feature", name = "Added", showcontent = true }, + { directory = "change", name = "Changes", showcontent = true }, + { directory = "bugfix", name = "Fixed", showcontent = true }, + { directory = "doc", name = "Documentation", showcontent = true }, + { directory = "breaking", name = "Breaking changes", showcontent = true }, + { directory = "security", name = "Security", showcontent = true }, + { directory = "performance", name = "Performance", showcontent = true }, + { directory = "deprecation", name = "Deprecated", showcontent = true }, + { directory = "removal", name = "Removed", showcontent = true }, + { directory = "packaging", name = "Packaging", showcontent = true }, ] [tool.flake8-lazy] @@ -273,3 +308,13 @@ standalone.lazy-exclude-modules = [ # a Python built without it must fail at import, as for filelock._read_write "sqlite3", ] + +[tool.nab] +mode = "universal" +base-group = "project-runtime" +uploaded-prior-to = "P7D" +decision-order = "stable" +matrix.python = ">=3.15,<3.16" +matrix.platforms = [ "linux_x86_64" ] +# temporary, delete after 2026-11-02 +packages.release-creator.uploaded-prior-to = 2026-10-26T18:24:46Z diff --git a/tests/test_async_transition_gate.py b/tests/test_async_transition_gate.py index f7e1d01d..68a277e6 100644 --- a/tests/test_async_transition_gate.py +++ b/tests/test_async_transition_gate.py @@ -2,12 +2,13 @@ import asyncio import contextlib +import time from concurrent.futures import Future as ConcurrentFuture from typing import TYPE_CHECKING, Final import pytest -from filelock._async import _AsyncTransitionGate +from filelock._async import _AsyncTransitionGate, _AsyncTransitionUnavailableError from tests.capability_marks import XFAIL_WITHOUT_COROUTINE_CANCELLATION if TYPE_CHECKING: @@ -43,6 +44,23 @@ async def second() -> None: assert order == ["first-in", "first-out", "second-in"] +@pytest.mark.asyncio +async def test_hold_for_acquire_deadline_preempts_long_poll() -> None: + gate = _AsyncTransitionGate() + + async def wait_for_turn() -> None: + async with contextlib.AsyncExitStack() as stack: + await stack.enter_async_context( + gate.hold_for_acquire( + blocking=True, cancel_check=None, deadline=time.perf_counter() + 0.05, poll_interval=1 + ) + ) + + async with gate.hold(): + with pytest.raises(_AsyncTransitionUnavailableError): + await asyncio.wait_for(wait_for_turn(), timeout=0.2) + + @pytest.mark.asyncio @XFAIL_WITHOUT_COROUTINE_CANCELLATION async def test_hold_canceled_while_waiting_takes_its_ticket_out() -> None: diff --git a/tox.toml b/tox.toml index b68f16eb..31764166 100644 --- a/tox.toml +++ b/tox.toml @@ -104,7 +104,7 @@ depends = [ "3.11", "3.12", "3.13", "3.14", "3.14t", "3.15", "3.15t" ] [env.docs] description = "build documentation" -dependency_groups = [ "docs" ] +dependency_groups = [ "docs", "release" ] commands = [ [ "sphinx-build", @@ -199,7 +199,6 @@ set_env.PYTEST_ADDOPTS = "{env:PYTEST_ADDOPTS:} --timeout-method=thread --timeou set_env.GRAAL_PYTHON_VM_ARGS = "--experimental-options --engine.OSR=false" [env.release] -description = "build the changelog from the news fragments for the given version, e.g. tox r -e release -- 3.30.0" -skip_install = true +description = "preview pending changelog fragments without selecting a release version" dependency_groups = [ "release" ] -commands = [ [ "towncrier", "build", "--yes", "--version", "{posargs}" ] ] +commands = [ [ "towncrier", "build", "--draft" ] ]