diff --git a/.cursor/rules/test-running.mdc b/.cursor/rules/test-running.mdc
deleted file mode 100644
index 201f9af6..00000000
--- a/.cursor/rules/test-running.mdc
+++ /dev/null
@@ -1,12 +0,0 @@
----
-description: run tests with uv tooling
-globs:
-alwaysApply: true
----
-
-use `uv run pytest` to run tests
-use uv to manage dependencies
-
-follow preexisting conventions in the project
-
-- use the fixtures
\ No newline at end of file
diff --git a/.github/workflows/README.md b/.github/workflows/README.md
new file mode 100644
index 00000000..43da3962
--- /dev/null
+++ b/.github/workflows/README.md
@@ -0,0 +1,150 @@
+# Reusable Workflows for Towncrier-based Releases
+
+This directory contains reusable GitHub Actions workflows that other projects can use to implement the same towncrier-based release process.
+
+## Available Reusable Workflows
+
+### `reusable-towncrier-release.yml`
+
+Determines the next version using the `towncrier-fragments` version scheme and builds the changelog.
+
+**Inputs:**
+- `project_name` (required): Name of the project (used for labeling and tag prefix)
+- `project_directory` (required): Directory containing the project (relative to repository root)
+
+**Outputs:**
+- `version`: The determined next version
+- `has_fragments`: Whether fragments were found
+
+**Behavior:**
+- ✅ Strict validation - workflow fails if changelog fragments or version data is missing
+- ✅ No fallback values - ensures data integrity for releases
+- ✅ Clear error messages to guide troubleshooting
+
+**Example usage:**
+
+```yaml
+jobs:
+ determine-version:
+ uses: pypa/setuptools-scm/.github/workflows/reusable-towncrier-release.yml@main
+ with:
+ project_name: my-project
+ project_directory: ./
+```
+
+## Using These Workflows in Your Project
+
+### Prerequisites
+
+1. **Add vcs-versioning dependency** to your project
+2. **Configure towncrier** in your `pyproject.toml`:
+
+```toml
+[tool.towncrier]
+directory = "changelog.d"
+filename = "CHANGELOG.md"
+start_string = "\n"
+template = "changelog.d/template.md"
+title_format = "## {version} ({project_date})"
+
+[[tool.towncrier.type]]
+directory = "removal"
+name = "Removed"
+showcontent = true
+
+[[tool.towncrier.type]]
+directory = "feature"
+name = "Added"
+showcontent = true
+
+[[tool.towncrier.type]]
+directory = "bugfix"
+name = "Fixed"
+showcontent = true
+```
+
+3. **Create changelog structure**:
+ - `changelog.d/` directory
+ - `changelog.d/template.md` (towncrier template)
+ - `CHANGELOG.md` with the start marker
+
+4. **Add the version scheme entry point** (if using vcs-versioning):
+
+The `towncrier-fragments` version scheme is provided by vcs-versioning 0.2.0+.
+
+### Complete Example Workflow
+
+```yaml
+name: Create Release
+
+on:
+ workflow_dispatch:
+ inputs:
+ create_release:
+ description: 'Create release'
+ required: true
+ type: boolean
+ default: false
+
+permissions:
+ contents: write
+ pull-requests: write
+
+jobs:
+ determine-version:
+ uses: pypa/setuptools-scm/.github/workflows/reusable-towncrier-release.yml@main
+ with:
+ project_name: my-project
+ project_directory: ./
+
+ create-release-pr:
+ needs: determine-version
+ if: needs.determine-version.outputs.has_fragments == 'true'
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v5
+
+ - name: Download changelog artifacts
+ uses: actions/download-artifact@v4
+ with:
+ name: changelog-my-project
+
+ - name: Create Pull Request
+ uses: peter-evans/create-pull-request@v7
+ with:
+ commit-message: "Release v${{ needs.determine-version.outputs.version }}"
+ branch: release-${{ needs.determine-version.outputs.version }}
+ title: "Release v${{ needs.determine-version.outputs.version }}"
+ labels: release:my-project
+ body: |
+ Automated release PR for version ${{ needs.determine-version.outputs.version }}
+```
+
+## Architecture
+
+The workflow system is designed with these principles:
+
+1. **Version scheme is single source of truth** - No version calculation in scripts
+2. **Reusable components** - Other projects can use the same workflows
+3. **Manual approval** - Release PRs must be reviewed and merged
+4. **Project-prefixed tags** - Enable monorepo releases (`project-vX.Y.Z`)
+5. **Automated but controlled** - Automation with human approval gates
+6. **Fail fast** - No fallback values; workflows fail explicitly if required data is missing
+7. **No custom scripts** - Uses PR title parsing and built-in tools only
+
+## Version Bump Logic
+
+The `towncrier-fragments` version scheme determines bumps based on fragment types:
+
+| Fragment Type | Version Bump | Example |
+|---------------|--------------|---------|
+| `removal` | Major (X.0.0) | Breaking changes |
+| `feature`, `deprecation` | Minor (0.X.0) | New features |
+| `bugfix`, `doc`, `misc` | Patch (0.0.X) | Bug fixes |
+
+## Support
+
+For issues or questions about these workflows:
+- Open an issue at https://github.com/pypa/setuptools-scm/issues
+- See full documentation in [CONTRIBUTING.md](../../CONTRIBUTING.md)
+
diff --git a/.github/workflows/api-check.yml b/.github/workflows/api-check.yml
index 4db2526b..b39747ae 100644
--- a/.github/workflows/api-check.yml
+++ b/.github/workflows/api-check.yml
@@ -28,52 +28,22 @@ jobs:
with:
python-version: '3.11'
- - name: Install dependencies
- run: |
- pip install -U pip setuptools
- pip install -e .[test]
- pip install griffe
+ - name: Install the latest version of uv
+ uses: astral-sh/setup-uv@v6
- - name: Run griffe API check
- id: griffe-check
- continue-on-error: true
+ - name: Get latest release tag
+ id: latest-tag
run: |
- echo "Running griffe API stability check..."
- if griffe check setuptools_scm -ssrc -f github; then
- echo "api_check_result=success" >> $GITHUB_OUTPUT
- echo "exit_code=0" >> $GITHUB_OUTPUT
- else
- exit_code=$?
- echo "api_check_result=warning" >> $GITHUB_OUTPUT
- echo "exit_code=$exit_code" >> $GITHUB_OUTPUT
- exit $exit_code
- fi
+ # Get the latest git tag (griffe needs a git ref)
+ LATEST_TAG=$(git describe --tags --abbrev=0 origin/main 2>/dev/null || echo "v9.2.1")
+ echo "Latest release tag: $LATEST_TAG"
+ echo "tag=$LATEST_TAG" >> $GITHUB_OUTPUT
- - name: Report API check result
- if: always()
- uses: actions/github-script@v8
- with:
- script: |
- const result = '${{ steps.griffe-check.outputs.api_check_result }}'
- const exitCode = '${{ steps.griffe-check.outputs.exit_code }}'
+ - name: Install dependencies
+ run: uv sync --all-packages --all-groups
- if (result === 'success') {
- core.notice('API stability check passed - no breaking changes detected')
- await core.summary
- .addHeading('✅ API Stability Check: Passed', 2)
- .addRaw('No breaking changes detected in the public API')
- .write()
- } else if (result === 'warning') {
- core.warning(`API stability check detected breaking changes (exit code: ${exitCode}). Please review the API changes above.`)
- await core.summary
- .addHeading('⚠️ API Stability Warning', 2)
- .addRaw('Breaking changes detected in the public API. Please review the changes reported above.')
- .addRaw(`\n\nExit code: ${exitCode}`)
- .write()
- } else {
- core.error('API stability check failed to run properly')
- await core.summary
- .addHeading('❌ API Stability Check: Failed', 2)
- .addRaw('The griffe check failed to execute. This may indicate griffe is not installed or there was an error.')
- .write()
- }
\ No newline at end of file
+ - name: Check API stability against latest release
+ run: |
+ echo "Comparing current code against tag: ${{ steps.latest-tag.outputs.tag }}"
+ # Use local check_api.py script which includes griffe-public-wildcard-imports extension
+ uv run --no-sync python setuptools-scm/check_api.py --against ${{ steps.latest-tag.outputs.tag }}
diff --git a/.github/workflows/create-release-tags.yml b/.github/workflows/create-release-tags.yml
new file mode 100644
index 00000000..66dff040
--- /dev/null
+++ b/.github/workflows/create-release-tags.yml
@@ -0,0 +1,145 @@
+name: Create Release Tags
+
+on:
+ pull_request:
+ types: [closed]
+ branches:
+ - main
+
+permissions:
+ contents: write
+
+jobs:
+ create-tags:
+ # Only run if PR was merged and has release labels
+ if: |
+ github.event.pull_request.merged == true &&
+ (contains(github.event.pull_request.labels.*.name, 'release:setuptools-scm') ||
+ contains(github.event.pull_request.labels.*.name, 'release:vcs-versioning'))
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v5
+ with:
+ fetch-depth: 0
+ ref: ${{ github.event.pull_request.merge_commit_sha }}
+
+ - name: Setup Python
+ uses: actions/setup-python@v6
+ with:
+ python-version: '3.11'
+
+ - name: Configure git
+ run: |
+ git config user.name "github-actions[bot]"
+ git config user.email "github-actions[bot]@users.noreply.github.com"
+
+ - name: Create tags
+ id: create-tags
+ run: |
+ set -e
+
+ TAGS_CREATED=""
+ PR_TITLE="${{ github.event.pull_request.title }}"
+
+ # Check if we should release setuptools-scm
+ if echo "${{ toJson(github.event.pull_request.labels.*.name) }}" | grep -q "release:setuptools-scm"; then
+ # Extract version from PR title: "Release: setuptools-scm v9.3.0, ..."
+ VERSION=$(echo "$PR_TITLE" | grep -oP 'setuptools-scm v\K[0-9]+\.[0-9]+\.[0-9]+')
+
+ if [ -z "$VERSION" ]; then
+ echo "ERROR: Failed to extract setuptools-scm version from PR title"
+ echo "PR title: $PR_TITLE"
+ echo "Expected format: 'Release: setuptools-scm vX.Y.Z'"
+ exit 1
+ fi
+
+ TAG="setuptools-scm-v$VERSION"
+ echo "Creating tag: $TAG"
+
+ git tag -a "$TAG" -m "Release setuptools-scm v$VERSION"
+ git push origin "$TAG"
+
+ TAGS_CREATED="$TAGS_CREATED $TAG"
+ echo "setuptools_scm_tag=$TAG" >> $GITHUB_OUTPUT
+ echo "setuptools_scm_version=$VERSION" >> $GITHUB_OUTPUT
+ fi
+
+ # Check if we should release vcs-versioning
+ if echo "${{ toJson(github.event.pull_request.labels.*.name) }}" | grep -q "release:vcs-versioning"; then
+ # Extract version from PR title: "Release: ..., vcs-versioning v0.2.0"
+ VERSION=$(echo "$PR_TITLE" | grep -oP 'vcs-versioning v\K[0-9]+\.[0-9]+\.[0-9]+')
+
+ if [ -z "$VERSION" ]; then
+ echo "ERROR: Failed to extract vcs-versioning version from PR title"
+ echo "PR title: $PR_TITLE"
+ echo "Expected format: 'Release: vcs-versioning vX.Y.Z'"
+ exit 1
+ fi
+
+ TAG="vcs-versioning-v$VERSION"
+ echo "Creating tag: $TAG"
+
+ git tag -a "$TAG" -m "Release vcs-versioning v$VERSION"
+ git push origin "$TAG"
+
+ TAGS_CREATED="$TAGS_CREATED $TAG"
+ echo "vcs_versioning_tag=$TAG" >> $GITHUB_OUTPUT
+ echo "vcs_versioning_version=$VERSION" >> $GITHUB_OUTPUT
+ fi
+
+ echo "tags_created=$TAGS_CREATED" >> $GITHUB_OUTPUT
+
+ - name: Extract changelog for setuptools-scm
+ if: steps.create-tags.outputs.setuptools_scm_version
+ id: changelog-setuptools-scm
+ run: |
+ VERSION="${{ steps.create-tags.outputs.setuptools_scm_version }}"
+ cd setuptools-scm
+
+ # Extract the changelog section for this version
+ # Read from version heading until next version heading or EOF
+ CHANGELOG=$(awk "/^## $VERSION/,/^## [0-9]/" CHANGELOG.md | sed '1d;$d')
+
+ # Save to file for GitHub release
+ echo "$CHANGELOG" > /tmp/changelog-setuptools-scm.md
+
+ - name: Extract changelog for vcs-versioning
+ if: steps.create-tags.outputs.vcs_versioning_version
+ id: changelog-vcs-versioning
+ run: |
+ VERSION="${{ steps.create-tags.outputs.vcs_versioning_version }}"
+ cd vcs-versioning
+
+ # Extract the changelog section for this version
+ CHANGELOG=$(awk "/^## $VERSION/,/^## [0-9]/" CHANGELOG.md | sed '1d;$d')
+
+ # Save to file for GitHub release
+ echo "$CHANGELOG" > /tmp/changelog-vcs-versioning.md
+
+ - name: Create GitHub Release for setuptools-scm
+ if: steps.create-tags.outputs.setuptools_scm_tag
+ uses: softprops/action-gh-release@v2
+ with:
+ tag_name: ${{ steps.create-tags.outputs.setuptools_scm_tag }}
+ name: setuptools-scm v${{ steps.create-tags.outputs.setuptools_scm_version }}
+ body_path: /tmp/changelog-setuptools-scm.md
+ draft: false
+ prerelease: false
+
+ - name: Create GitHub Release for vcs-versioning
+ if: steps.create-tags.outputs.vcs_versioning_tag
+ uses: softprops/action-gh-release@v2
+ with:
+ tag_name: ${{ steps.create-tags.outputs.vcs_versioning_tag }}
+ name: vcs-versioning v${{ steps.create-tags.outputs.vcs_versioning_version }}
+ body_path: /tmp/changelog-vcs-versioning.md
+ draft: false
+ prerelease: false
+
+ - name: Summary
+ run: |
+ echo "## Tags Created" >> $GITHUB_STEP_SUMMARY
+ echo "${{ steps.create-tags.outputs.tags_created }}" >> $GITHUB_STEP_SUMMARY
+ echo "" >> $GITHUB_STEP_SUMMARY
+ echo "PyPI upload will be triggered automatically by tag push." >> $GITHUB_STEP_SUMMARY
+
diff --git a/.github/workflows/python-tests.yml b/.github/workflows/python-tests.yml
index 17953d55..80886847 100644
--- a/.github/workflows/python-tests.yml
+++ b/.github/workflows/python-tests.yml
@@ -20,8 +20,13 @@ env:
jobs:
package:
- name: Build & inspect our package.
+ name: Build & inspect ${{ matrix.package }}
runs-on: ubuntu-latest
+ strategy:
+ matrix:
+ package:
+ - vcs-versioning
+ - setuptools-scm
env:
# Use no-local-version for package builds to ensure clean versions for PyPI uploads
SETUPTOOLS_SCM_NO_LOCAL: "1"
@@ -31,7 +36,11 @@ jobs:
with:
fetch-depth: 0
- - uses: hynek/build-and-inspect-python-package@v2
+ - name: Build ${{ matrix.package }}
+ uses: hynek/build-and-inspect-python-package@v2
+ with:
+ path: ${{ matrix.package }}
+ upload-name-suffix: -${{ matrix.package }}
test:
needs: [package]
@@ -39,7 +48,7 @@ jobs:
strategy:
fail-fast: false
matrix:
- python_version: [ '3.8', '3.9', '3.10', '3.11', '3.12', '3.13', 'pypy-3.10' ]
+ python_version: [ '3.10', '3.11', '3.12', '3.13', 'pypy-3.10' ]
os: [windows-latest, ubuntu-latest] #, macos-latest]
include:
- os: windows-latest
@@ -95,46 +104,93 @@ jobs:
echo "C:\Program Files\Mercurial\" >> $env:GITHUB_PATH
git config --system gpg.program "C:\Program Files (x86)\gnupg\bin\gpg.exe"
if: runner.os == 'Windows'
- - run: uv sync --group test --group docs --extra rich
- - uses: actions/download-artifact@v5
+ - run: uv sync --all-packages --all-groups
+ - name: Download vcs-versioning packages
+ uses: actions/download-artifact@v4
+ with:
+ name: Packages-vcs-versioning
+ path: dist
+ - name: Download setuptools-scm packages
+ uses: actions/download-artifact@v4
with:
- name: Packages
+ name: Packages-setuptools-scm
path: dist
- - shell: bash
- run: uv pip install "$(echo -n dist/*whl)"
+ - name: Install built wheels
+ shell: bash
+ run: |
+ # Install vcs-versioning first (dependency of setuptools-scm)
+ uv pip install dist/vcs_versioning-*.whl
+ # Then install setuptools-scm
+ uv pip install dist/setuptools_scm-*.whl
- run: |
$(hg debuginstall --template "{pythonexe}") -m pip install hg-git --user
if: matrix.os == 'ubuntu-latest'
# this hopefully helps with os caches, hg init sometimes gets 20s timeouts
- run: hg version
- - run: uv run pytest
+ shell: bash
+ - name: Run tests for both packages
+ run: uv run --no-sync pytest setuptools-scm/testing_scm/ vcs-versioning/testing_vcs/
timeout-minutes: 25
- dist_upload:
+ dist_upload_setuptools_scm:
+ runs-on: ubuntu-latest
+ if: (github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags/setuptools-scm-v')) || (github.event_name == 'release' && github.event.action == 'published' && startsWith(github.event.release.tag_name, 'setuptools-scm-v'))
+ permissions:
+ id-token: write
+ needs: [test]
+ steps:
+ - name: Download setuptools-scm packages
+ uses: actions/download-artifact@v4
+ with:
+ name: Packages-setuptools-scm
+ path: dist
+ - name: Publish setuptools-scm to PyPI
+ uses: pypa/gh-action-pypi-publish@release/v1
+ dist_upload_vcs_versioning:
runs-on: ubuntu-latest
- if: (github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags')) || (github.event_name == 'release' && github.event.action == 'published')
+ if: (github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags/vcs-versioning-v')) || (github.event_name == 'release' && github.event.action == 'published' && startsWith(github.event.release.tag_name, 'vcs-versioning-v'))
permissions:
id-token: write
needs: [test]
steps:
- - uses: actions/download-artifact@v5
+ - name: Download vcs-versioning packages
+ uses: actions/download-artifact@v4
with:
- name: Packages
+ name: Packages-vcs-versioning
path: dist
- - name: Publish package to PyPI
+ - name: Publish vcs-versioning to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
- upload-release-assets:
+ upload-release-assets-setuptools-scm:
runs-on: ubuntu-latest
- if: github.event_name == 'release' && github.event.action == 'published'
+ if: github.event_name == 'release' && github.event.action == 'published' && startsWith(github.event.release.tag_name, 'setuptools-scm-v')
needs: [test]
permissions:
contents: write
steps:
- - uses: actions/download-artifact@v5
+ - name: Download setuptools-scm packages
+ uses: actions/download-artifact@v4
with:
- name: Packages
+ name: Packages-setuptools-scm
+ path: dist
+ - name: Upload release assets
+ uses: softprops/action-gh-release@v2
+ with:
+ files: dist/*
+ fail_on_unmatched_files: true
+
+ upload-release-assets-vcs-versioning:
+ runs-on: ubuntu-latest
+ if: github.event_name == 'release' && github.event.action == 'published' && startsWith(github.event.release.tag_name, 'vcs-versioning-v')
+ needs: [test]
+ permissions:
+ contents: write
+ steps:
+ - name: Download vcs-versioning packages
+ uses: actions/download-artifact@v4
+ with:
+ name: Packages-vcs-versioning
path: dist
- name: Upload release assets
uses: softprops/action-gh-release@v2
@@ -149,9 +205,15 @@ jobs:
permissions:
id-token: write
steps:
- - uses: actions/download-artifact@v5
+ - name: Download vcs-versioning packages
+ uses: actions/download-artifact@v4
+ with:
+ name: Packages-vcs-versioning
+ path: dist
+ - name: Download setuptools-scm packages
+ uses: actions/download-artifact@v4
with:
- name: Packages
+ name: Packages-setuptools-scm
path: dist
- name: Publish package to PyPI
continue-on-error: true
diff --git a/.github/workflows/release-proposal.yml b/.github/workflows/release-proposal.yml
new file mode 100644
index 00000000..ccaca734
--- /dev/null
+++ b/.github/workflows/release-proposal.yml
@@ -0,0 +1,117 @@
+name: Create Release Proposal
+
+on:
+ push:
+ branches:
+ - main
+ - develop
+ pull_request:
+
+permissions:
+ contents: write
+ pull-requests: write
+
+jobs:
+ create-release-pr:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v5
+ with:
+ fetch-depth: 0
+
+ - name: Setup Python
+ uses: actions/setup-python@v6
+ with:
+ python-version: '3.11'
+
+ - name: Install uv
+ uses: astral-sh/setup-uv@v6
+
+ - name: Install dependencies
+ run: |
+ uv sync --all-packages --all-groups
+
+ - name: Configure git
+ if: github.event_name == 'push'
+ run: |
+ git config user.name "github-actions[bot]"
+ git config user.email "github-actions[bot]@users.noreply.github.com"
+
+ - name: Run release proposal
+ id: release
+ run: |
+ uv run create-release-proposal \
+ --event "${{ github.event_name }}" \
+ --branch "${{ github.ref_name }}"
+ env:
+ GITHUB_TOKEN: ${{ github.token }}
+
+ - name: Create or update release branch
+ if: github.event_name == 'push'
+ run: |
+ # Get release branch from script output
+ RELEASE_BRANCH="${{ steps.release.outputs.release_branch }}"
+ RELEASES="${{ steps.release.outputs.releases }}"
+
+ # Checkout release branch (force)
+ git checkout -B "$RELEASE_BRANCH"
+
+ # Commit towncrier changes
+ git add -A
+ git commit -m "Prepare release: $RELEASES" || echo "No changes to commit"
+
+ # Force push
+ git push origin "$RELEASE_BRANCH" --force
+
+ - name: Create or update PR
+ if: github.event_name == 'push'
+ uses: actions/github-script@v8
+ env:
+ RELEASE_BRANCH: ${{ steps.release.outputs.release_branch }}
+ PR_EXISTS: ${{ steps.release.outputs.pr_exists }}
+ PR_NUMBER: ${{ steps.release.outputs.pr_number }}
+ PR_TITLE: ${{ steps.release.outputs.pr_title }}
+ PR_BODY: ${{ steps.release.outputs.pr_body }}
+ LABELS: ${{ steps.release.outputs.labels }}
+ with:
+ script: |
+ const releaseBranch = process.env.RELEASE_BRANCH;
+ const prExists = process.env.PR_EXISTS === 'true';
+ const prNumber = process.env.PR_NUMBER;
+ const prTitle = process.env.PR_TITLE;
+ const prBody = process.env.PR_BODY;
+ const labels = process.env.LABELS.split(',').filter(l => l);
+
+ if (prExists && prNumber) {
+ // Update existing PR
+ console.log(`Updating existing PR #${prNumber}`);
+ await github.rest.pulls.update({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ pull_number: parseInt(prNumber),
+ title: prTitle,
+ body: prBody
+ });
+ } else {
+ // Create new PR
+ console.log('Creating new PR');
+ const { data: pr } = await github.rest.pulls.create({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ title: prTitle,
+ body: prBody,
+ head: releaseBranch,
+ base: 'main'
+ });
+ console.log(`Created PR #${pr.number}`);
+
+ // Add labels
+ if (labels.length > 0) {
+ await github.rest.issues.addLabels({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: pr.number,
+ labels: labels
+ });
+ }
+ }
diff --git a/.github/workflows/reusable-towncrier-release.yml b/.github/workflows/reusable-towncrier-release.yml
new file mode 100644
index 00000000..1867b0c2
--- /dev/null
+++ b/.github/workflows/reusable-towncrier-release.yml
@@ -0,0 +1,121 @@
+name: Reusable Towncrier Release
+
+on:
+ workflow_call:
+ inputs:
+ project_name:
+ description: 'Name of the project (used for labeling and tag prefix)'
+ required: true
+ type: string
+ project_directory:
+ description: 'Directory containing the project (relative to repository root)'
+ required: true
+ type: string
+ outputs:
+ version:
+ description: 'The determined next version'
+ value: ${{ jobs.determine-version.outputs.version }}
+ has_fragments:
+ description: 'Whether fragments were found'
+ value: ${{ jobs.determine-version.outputs.has_fragments }}
+
+jobs:
+ determine-version:
+ runs-on: ubuntu-latest
+ outputs:
+ version: ${{ steps.version.outputs.version }}
+ has_fragments: ${{ steps.check.outputs.has_fragments }}
+ steps:
+ - uses: actions/checkout@v5
+ with:
+ fetch-depth: 0
+
+ - name: Setup Python
+ uses: actions/setup-python@v6
+ with:
+ python-version: '3.11'
+
+ - name: Install uv
+ uses: astral-sh/setup-uv@v6
+
+ - name: Install dependencies
+ run: |
+ uv sync --all-packages --group release
+
+ - name: Check for fragments
+ id: check
+ working-directory: ${{ inputs.project_directory }}
+ run: |
+ if [ ! -d "changelog.d" ]; then
+ echo "ERROR: changelog.d directory not found for ${{ inputs.project_name }}"
+ exit 1
+ fi
+
+ FRAGMENT_COUNT=$(find changelog.d -type f -name "*.*.md" ! -name "template.md" ! -name "README.md" 2>/dev/null | wc -l)
+
+ if [ "$FRAGMENT_COUNT" -eq 0 ]; then
+ echo "ERROR: No changelog fragments found for ${{ inputs.project_name }}"
+ echo "Cannot create release without changelog fragments"
+ exit 1
+ fi
+
+ echo "has_fragments=true" >> $GITHUB_OUTPUT
+ echo "Found $FRAGMENT_COUNT fragment(s) for ${{ inputs.project_name }}"
+
+ - name: Determine version
+ if: steps.check.outputs.has_fragments == 'true'
+ id: version
+ run: |
+ # Use vcs-versioning CLI to get the next version from the version scheme
+ NEXT_VERSION=$(uv run --directory ${{ inputs.project_directory }} python -m vcs_versioning \
+ --root . \
+ --version-scheme towncrier-fragments \
+ --local-scheme no-local-version 2>&1 | grep -oP '^\d+\.\d+\.\d+' || echo "")
+
+ if [ -z "$NEXT_VERSION" ]; then
+ echo "ERROR: Failed to determine version for ${{ inputs.project_name }}"
+ echo "Version scheme did not return a valid version"
+ exit 1
+ fi
+
+ echo "version=$NEXT_VERSION" >> $GITHUB_OUTPUT
+ echo "Determined version: $NEXT_VERSION for ${{ inputs.project_name }}"
+
+ build-changelog:
+ needs: determine-version
+ if: needs.determine-version.outputs.has_fragments == 'true'
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v5
+ with:
+ fetch-depth: 0
+
+ - name: Setup Python
+ uses: actions/setup-python@v6
+ with:
+ python-version: '3.11'
+
+ - name: Install uv
+ uses: astral-sh/setup-uv@v6
+
+ - name: Install dependencies
+ run: |
+ uv sync --all-packages --group release
+
+ - name: Run towncrier
+ working-directory: ${{ inputs.project_directory }}
+ run: |
+ if ! uv run towncrier build --version "${{ needs.determine-version.outputs.version }}" --yes; then
+ echo "ERROR: towncrier build failed for ${{ inputs.project_name }}"
+ exit 1
+ fi
+
+ - name: Upload changelog artifacts
+ uses: actions/upload-artifact@v4
+ with:
+ name: changelog-${{ inputs.project_name }}
+ path: |
+ ${{ inputs.project_directory }}/CHANGELOG.md
+ ${{ inputs.project_directory }}/changelog.d/
+ retention-days: 5
+
diff --git a/.gitignore b/.gitignore
index b790bb39..013742ed 100644
--- a/.gitignore
+++ b/.gitignore
@@ -52,4 +52,7 @@ coverage.xml
# Sphinx documentation
docs/_build/
+# MkDocs documentation
+site/
+
.serena/cache/
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 5f66a9f8..54a0dcba 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -25,6 +25,8 @@ repos:
- importlib_metadata
- typing-extensions>=4.5
- rich
+ - PyGithub>=2.0.0
+ - towncrier>=23.11.0
- repo: https://github.com/scientific-python/cookie
rev: 2025.10.01
diff --git a/.readthedocs.yaml b/.readthedocs.yaml
index 5aa34e7a..a287eb6f 100644
--- a/.readthedocs.yaml
+++ b/.readthedocs.yaml
@@ -3,13 +3,16 @@ version: 2
mkdocs:
configuration: mkdocs.yml
-
build:
os: ubuntu-24.04
tools:
python: "3.13"
jobs:
+ pre_create_environment:
+ - asdf plugin add uv
+ - asdf install uv latest
+ - asdf global uv latest
+ create_environment:
+ - uv venv "${READTHEDOCS_VIRTUALENV_PATH}"
install:
- - pip install -U pip # Official recommended way
- - pip install .
- - pip install --group docs
+ - UV_PROJECT_ENVIRONMENT="${READTHEDOCS_VIRTUALENV_PATH}" uv sync --frozen --group docs
diff --git a/.serena/.gitignore b/.serena/.gitignore
new file mode 100644
index 00000000..14d86ad6
--- /dev/null
+++ b/.serena/.gitignore
@@ -0,0 +1 @@
+/cache
diff --git a/.serena/memories/done_checklist.md b/.serena/memories/done_checklist.md
deleted file mode 100644
index 8e0fc3e2..00000000
--- a/.serena/memories/done_checklist.md
+++ /dev/null
@@ -1,16 +0,0 @@
-Before considering a task done
-
-- Code quality
- - Ruff clean: uv run ruff check .
- - Types clean: uv run mypy
-- Tests
- - All tests green: uv run pytest
- - New/changed behavior covered with tests (use project fixtures)
-- Docs
- - Update docs if user-facing behavior changed
- - Build docs cleanly: uv run mkdocs build --clean --strict
-- Packaging
- - If relevant: uv run python -m build && uv run twine check dist/*
-- Housekeeping
- - Follow existing naming and module structure; keep functions focused and typed
- - Update `CHANGELOG.md` when appropriate
diff --git a/.serena/memories/project_overview.md b/.serena/memories/project_overview.md
deleted file mode 100644
index cf2670d9..00000000
--- a/.serena/memories/project_overview.md
+++ /dev/null
@@ -1,28 +0,0 @@
-Project: setuptools-scm
-
-Purpose
-- Extract and infer Python package versions from SCM metadata (Git/Mercurial) at build/runtime.
-- Provide setuptools integrations (dynamic version, file finders) and fallbacks for archival/PKG-INFO.
-
-Tech Stack
-- Language: Python (3.8–3.13)
-- Packaging/build: setuptools (>=61), packaging; console scripts via entry points
-- Tooling: uv (dependency and run), pytest, mypy (strict), ruff (lint, isort), mkdocs (docs), tox (optional/matrix), wheel/build
-
-Codebase Structure (high level)
-- src/setuptools_scm/: library code
- - _cli.py, __main__.py: CLI entry (`python -m setuptools_scm`, `setuptools-scm`)
- - git.py, hg.py, hg_git.py: VCS parsing
- - _file_finders/: discover files for sdist
- - _integration/: setuptools and pyproject integration
- - version.py and helpers: version schemes/local version logic
- - discover.py, fallbacks.py: inference and archival fallbacks
-- testing/: pytest suite and fixtures
-- docs/: mkdocs documentation
-- pyproject.toml: project metadata, pytest and ruff config
-- tox.ini: alternate CI/matrix, flake8 defaults
-- uv.lock: locked dependencies
-
-Conventions
-- Use uv to run commands (`uv run ...`); tests live under `testing/` per pytest config.
-- Type hints throughout; strict mypy enforced; ruff governs lint rules and import layout (isort in ruff).
diff --git a/.serena/memories/style_and_conventions.md b/.serena/memories/style_and_conventions.md
deleted file mode 100644
index aec4e917..00000000
--- a/.serena/memories/style_and_conventions.md
+++ /dev/null
@@ -1,17 +0,0 @@
-Style and Conventions
-
-- Typing
- - mypy strict is enabled; add precise type hints for public functions/classes.
- - Prefer explicit/clear types; avoid `Any` and unsafe casts.
-- Linting/Imports
- - Ruff is the canonical linter (config in pyproject). Respect its rules and isort settings (single-line imports, ordered, types grouped).
- - Flake8 config exists in tox.ini but ruff linting is primary.
-- Formatting
- - Follow ruff guidance; keep lines <= 88 where applicable (flake8 reference).
-- Testing
- - Pytest with `testing/` as testpath; default 5m timeout; warnings treated as errors.
- - Use existing fixtures; add `@pytest.mark` markers if needed (see pyproject markers).
-- Logging
- - Tests run with log level info/debug; avoid noisy logs in normal library code.
-- General
- - Small, focused functions; early returns; explicit errors. Keep APIs documented with concise docstrings.
diff --git a/.serena/memories/suggested_commands.md b/.serena/memories/suggested_commands.md
deleted file mode 100644
index 8eeeab96..00000000
--- a/.serena/memories/suggested_commands.md
+++ /dev/null
@@ -1,30 +0,0 @@
-Environment
-- Install deps (uses default groups test, docs):
- - uv sync
-
-Core Dev
-- Run tests:
- - uv run pytest
-- Lint (ruff):
- - uv run ruff check .
- - uv run ruff check . --fix # optional autofix
-- Type check (mypy strict):
- - uv run mypy
-- Build docs:
- - uv run mkdocs serve --dev-addr localhost:8000
- - uv run mkdocs build --clean --strict
-
-Entrypoints / Tooling
-- CLI version/debug:
- - uv run python -m setuptools_scm --help
- - uv run python -m setuptools_scm
- - uv run setuptools-scm --help
-- Build dist and verify:
- - uv run python -m build
- - uv run twine check dist/*
-- Optional matrix via tox:
- - uv run tox -q
-
-Git/Linux Utilities (Linux host)
-- git status / git log --oneline --graph --decorate
-- ls -la; find . -name "pattern"; grep -R "text" .
diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 00000000..b1ee29ab
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1,151 @@
+# setuptools-scm Development Guide for AI Assistants
+
+## Project Overview
+
+**setuptools-scm monorepo** - Extract Python package versions from Git/Mercurial metadata.
+
+- **Language**: Python 3.10+
+- **Build**: setuptools, uv for dependency management
+- **Quality**: pre-commit hooks (ruff, mypy strict), pytest with fixtures
+
+### Structure
+```
+setuptools-scm/ # Setuptools integration (file finders, hooks)
+├── src/setuptools_scm/ # Integration code
+└── testing_scm/ # Setuptools-specific tests
+
+vcs-versioning/ # Core VCS versioning (standalone library)
+├── src/vcs_versioning/ # Core version inference
+└── testing_vcs/ # Core functionality tests
+```
+
+## Quick Commands
+
+```bash
+# Setup
+uv sync --all-packages --all-groups
+
+# Tests (use -n12 for parallel execution)
+uv run pytest -n12 # all tests
+uv run pytest setuptools-scm/testing_scm -n12 # setuptools tests only
+uv run pytest vcs-versioning/testing_vcs -n12 # core tests only
+
+# Quality (use pre-commit)
+pre-commit run --all-files # run all quality checks
+git commit # pre-commit runs automatically
+
+# Docs
+uv run mkdocs serve # local preview
+uv run mkdocs build --clean --strict
+
+# CLI
+uv run python -m setuptools_scm # version from current repo
+uv run python -m vcs_versioning --help # core CLI
+```
+
+## Code Conventions
+
+### Typing
+- **Strict mypy** - precise types, avoid `Any`
+- Type all public functions/classes
+
+### Style
+- **Ruff** enforces all rules (lint + isort)
+- Single-line imports, ordered by type
+- Lines ≤88 chars where practical
+
+### Testing
+- Use project fixtures (`WorkDir`, `wd`, etc.)
+- Warnings treated as errors
+- Add `@pytest.mark.issue(id)` when fixing bugs
+
+### Logging
+- Log level info/debug in tests
+- Minimal logging in library code
+
+### General
+- Small, focused functions
+- Early returns preferred
+- Explicit error messages
+- Concise docstrings
+
+## Project Rules
+
+1. **Use `uv run pytest -n12`** to run tests (parallel execution)
+2. **Use uv to manage dependencies** - don't use pip/conda
+3. **Follow preexisting conventions** - match surrounding code style
+4. **Use the fixtures** - `WorkDir`, `wd`, etc. for test repositories
+
+### File Organization
+- `setuptools-scm/testing_scm/` - setuptools integration tests
+- `vcs-versioning/testing_vcs/` - core VCS functionality tests
+- Add tests in the appropriate directory based on what layer you're testing
+
+## Before Considering Done
+
+- [ ] **Tests pass**: `uv run pytest -n12`
+- [ ] **Pre-commit passes**: `pre-commit run --all-files` (ruff, mypy, etc.)
+- [ ] **New behavior has tests** (use project fixtures)
+- [ ] **Update docs** if user-facing changes
+- [ ] **Add changelog fragment** (always use towncrier, never edit CHANGELOG.md directly)
+
+## Key Files
+
+- `CONTRIBUTING.md` - Release process with towncrier
+- `TESTING.md` - Test organization and running
+- `docs/` - User-facing documentation (mkdocs)
+- `pyproject.toml` - Workspace config (pytest, mypy, ruff)
+- `uv.lock` - Locked dependencies
+
+## Common Patterns
+
+### Version Schemes
+Located in `vcs_versioning/_version_schemes.py`. Entry points in `pyproject.toml`.
+
+### File Finders
+In `setuptools_scm/_file_finders/`. Register as `setuptools.file_finders` entry point.
+**Always active when setuptools-scm is installed** - even without version inference.
+
+### Integration Hooks
+- `infer_version()` - finalize_options hook (pyproject.toml projects)
+- `version_keyword()` - setup.py `use_scm_version` parameter
+- File finder - always registered, independent of versioning
+
+## Changelog Management
+
+**ALWAYS use towncrier fragments - NEVER edit CHANGELOG.md directly.**
+
+Create fragments in `{project}/changelog.d/`:
+
+```bash
+# Bug fix (patch bump)
+echo "Fix warning logic" > setuptools-scm/changelog.d/1231.bugfix.md
+
+# New feature (minor bump)
+echo "Add new scheme" > vcs-versioning/changelog.d/123.feature.md
+
+# Breaking change (major bump)
+echo "Remove deprecated API" > setuptools-scm/changelog.d/456.removal.md
+```
+
+**Fragment types**: `feature`, `bugfix`, `deprecation`, `removal`, `doc`, `misc`
+
+## Debugging
+
+```bash
+# Check version inference
+uv run python -m setuptools_scm
+
+# With custom config
+uv run python -m vcs_versioning --root . --version-scheme guess-next-dev
+
+# Debug mode (set in tests or CLI)
+SETUPTOOLS_SCM_DEBUG=1 uv run python -m setuptools_scm
+```
+
+---
+
+**Documentation**: https://setuptools-scm.readthedocs.io/
+**Repository**: https://github.com/pypa/setuptools-scm/
+**Issues**: https://github.com/pypa/setuptools-scm/issues
+
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 00000000..472554aa
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,189 @@
+# Contributing to setuptools-scm and vcs-versioning
+
+Thank you for contributing! This document explains the development workflow, including how to add changelog entries and create releases.
+
+## Development Setup
+
+This is a monorepo containing two packages:
+- `setuptools-scm/` - Setuptools integration for version management
+- `vcs-versioning/` - Core VCS version detection and schemes
+
+### Installation
+
+```bash
+# Install all dependencies
+uv sync --all-packages --all-groups
+
+# Run tests
+uv run pytest -n12
+
+# Run tests for specific package
+uv run pytest setuptools-scm/testing_scm/ -n12
+uv run pytest vcs-versioning/testing_vcs/ -n12
+```
+
+## Changelog Fragments
+
+We use [towncrier](https://towncrier.readthedocs.io/) to manage changelog entries. This ensures that changelog entries are added alongside code changes and reduces merge conflicts.
+
+### Adding a Changelog Fragment
+
+When you make a change that should be noted in the changelog, create a fragment file in the appropriate project's `changelog.d/` directory:
+
+**For setuptools-scm changes:**
+```bash
+# Create a fragment file
+echo "Your changelog entry here" > setuptools-scm/changelog.d/123.feature.md
+```
+
+**For vcs-versioning changes:**
+```bash
+# Create a fragment file
+echo "Your changelog entry here" > vcs-versioning/changelog.d/456.bugfix.md
+```
+
+### Fragment Naming Convention
+
+Fragments follow the naming pattern: `{number}.{type}.md`
+
+- **number**: Usually the GitHub issue or PR number (or any unique identifier)
+- **type**: One of the types below
+- **extension**: Always `.md`
+
+### Fragment Types
+
+The fragment type determines the version bump:
+
+| Type | Description | Version Bump | Example |
+|------|-------------|--------------|---------|
+| `feature` | New features or enhancements | **Minor** (0.X.0) | `123.feature.md` |
+| `bugfix` | Bug fixes | **Patch** (0.0.X) | `456.bugfix.md` |
+| `deprecation` | Deprecation notices | **Minor** (0.X.0) | `789.deprecation.md` |
+| `removal` | Breaking changes/removed features | **Major** (X.0.0) | `321.removal.md` |
+| `doc` | Documentation improvements | **Patch** (0.0.X) | `654.doc.md` |
+| `misc` | Internal changes, refactoring | **Patch** (0.0.X) | `987.misc.md` |
+
+### Fragment Content
+
+Keep fragments concise and user-focused. Do not include issue numbers in the content (they're added automatically).
+
+**Good:**
+```markdown
+Add support for custom version schemes via plugin system
+```
+
+**Bad:**
+```markdown
+Fix #123: Added support for custom version schemes via plugin system in the configuration
+```
+
+## Version Scheme Integration
+
+The `towncrier-fragments` version scheme automatically determines version bumps based on changelog fragments. During development builds, the version will reflect the next release version:
+
+```bash
+# If you have a feature fragment, version might be:
+9.3.0.dev5+g1234567
+
+# If you only have bugfix fragments:
+9.2.2.dev5+g1234567
+```
+
+This ensures that the version you see during development will be the actual release version.
+
+## Release Process
+
+Releases are managed through GitHub Actions workflows with manual approval.
+
+### 1. Create a Release Proposal
+
+Maintainers trigger the release workflow manually:
+
+1. Go to **Actions** → **Create Release Proposal**
+2. Select which projects to release:
+ - ☑ Release setuptools-scm
+ - ☑ Release vcs-versioning
+3. Click **Run workflow**
+
+The workflow will:
+- Analyze changelog fragments in each project
+- Determine the version bump (major/minor/patch) based on fragment types
+- Query the `towncrier-fragments` version scheme for the next version
+- Run `towncrier build` to update the CHANGELOG.md
+- Create a release PR with the changes
+- Label the PR with `release:setuptools-scm` and/or `release:vcs-versioning`
+
+### 2. Review and Approve
+
+Review the release PR:
+- Check that the changelog entries are accurate
+- Verify the version numbers are correct
+- Ensure all tests pass
+
+### 3. Merge to Release
+
+When you merge the PR to `main`:
+- The merge triggers the tag creation workflow automatically
+- Tags are created with the project prefix:
+ - `setuptools-scm-v9.3.0`
+ - `vcs-versioning-v0.2.0`
+- GitHub releases are created with changelog excerpts
+- Tag pushes trigger the PyPI upload workflow
+- Only the package(s) matching the tag prefix are uploaded to PyPI
+
+## Workflow Architecture
+
+The release system is designed to be reusable by other projects:
+
+### Key Components
+
+1. **Version Scheme** (`vcs_versioning._version_schemes._towncrier`)
+ - Analyzes fragments to determine version bump
+ - Used by both development builds and release workflow
+ - No version calculation logic in scripts - single source of truth
+
+2. **Release Proposal Workflow** (`.github/workflows/release-proposal.yml`)
+ - Manual trigger with project selection
+ - Uses `vcs-versioning` CLI to query version scheme
+ - Runs `towncrier build` with the determined version
+ - Creates labeled PR
+
+3. **Tag Creation Workflow** (`.github/workflows/create-release-tags.yml`)
+ - Triggered by PR merge with release labels
+ - Creates project-prefixed tags
+ - Creates GitHub releases
+
+4. **Upload Workflow** (`.github/workflows/python-tests.yml`)
+ - Triggered by tag push (filtered by tag prefix)
+ - Uploads only matching package to PyPI
+
+### Benefits
+
+- ✅ Version determination is consistent (version scheme is single source of truth)
+- ✅ Manual approval via familiar PR review process
+- ✅ Atomic releases tied to merge commits
+- ✅ Project-specific tags prevent accidental releases
+- ✅ Can release one or both projects in a single PR
+- ✅ Fully auditable release process
+- ✅ Reusable workflows for other projects
+
+## Testing Locally
+
+You can test the version scheme locally:
+
+```bash
+# See what version would be generated
+cd setuptools-scm
+uv run python -m vcs_versioning --root .. --version-scheme towncrier-fragments
+
+# Test towncrier build (dry-run)
+cd setuptools-scm
+uv run towncrier build --version 9.3.0 --draft
+```
+
+## Questions?
+
+- Check [TESTING.md](./TESTING.md) for testing guidelines
+- Open an issue for bugs or feature requests
+- Ask in discussions for general questions
+
diff --git a/README.md b/README.md
index f4ca4bf9..1e506f0c 100644
--- a/README.md
+++ b/README.md
@@ -1,132 +1,60 @@
-# setuptools-scm
-[](https://github.com/pypa/setuptools-scm/actions/workflows/python-tests.yml)
-[](https://setuptools-scm.readthedocs.io/en/latest/?badge=latest)
-[ ](https://tidelift.com/subscription/pkg/pypi-setuptools-scm?utm_source=pypi-setuptools-scm&utm_medium=readme)
+# setuptools-scm Monorepo
-## about
+This is the monorepo for the setuptools-scm ecosystem, containing two main projects:
-[setuptools-scm] extracts Python package versions from `git` or `hg` metadata
-instead of declaring them as the version argument
-or in a Source Code Managed (SCM) managed file.
+## Projects
-Additionally [setuptools-scm] provides `setuptools` with a list of
-files that are managed by the SCM
-
-(i.e. it automatically adds all the SCM-managed files to the sdist).
-
-Unwanted files must be excluded via `MANIFEST.in`
-or [configuring Git archive][git-archive-docs].
+### [setuptools-scm](./setuptools-scm/)
-> **⚠️ Important:** Installing setuptools-scm automatically enables a file finder that includes **all SCM-tracked files** in your source distributions. This can be surprising if you have development files tracked in Git/Mercurial that you don't want in your package. Use `MANIFEST.in` to exclude unwanted files. See the [documentation] for details.
+The main package that extracts Python package versions from Git or Mercurial metadata and provides setuptools integration.
-## `pyproject.toml` usage
+**[Read setuptools-scm documentation →](./setuptools-scm/README.md)**
-The preferred way to configure [setuptools-scm] is to author
-settings in a `tool.setuptools_scm` section of `pyproject.toml`.
+### [vcs-versioning](./vcs-versioning/)
-This feature requires setuptools 61 or later (recommended: >=80 for best compatibility).
-First, ensure that [setuptools-scm] is present during the project's
-build step by specifying it as one of the build requirements.
+Core VCS versioning functionality extracted as a standalone library that can be used independently of setuptools.
-```toml title="pyproject.toml"
-[build-system]
-requires = ["setuptools>=80", "setuptools-scm>=8"]
-build-backend = "setuptools.build_meta"
-```
-
-That will be sufficient to require [setuptools-scm] for projects
-that support [PEP 518] like [pip] and [build].
-
-[pip]: https://pypi.org/project/pip
-[build]: https://pypi.org/project/build
-[PEP 518]: https://peps.python.org/pep-0518/
+**[Read vcs-versioning documentation →](./vcs-versioning/README.md)**
+## Development
-To enable version inference, you need to set the version
-dynamically in the `project` section of `pyproject.toml`:
+This workspace uses [uv](https://github.com/astral-sh/uv) for dependency management.
-```toml title="pyproject.toml"
-[project]
-# version = "0.0.1" # Remove any existing version parameter.
-dynamic = ["version"]
+### Setup
-[tool.setuptools_scm]
+```bash
+# Install all packages with all dependency groups (tests, docs, etc.)
+uv sync --all-packages --all-groups
```
-!!! note "Simplified Configuration"
-
- Starting with setuptools-scm 8.1+, if `setuptools_scm` (or `setuptools-scm`) is
- present in your `build-system.requires`, the `[tool.setuptools_scm]` section
- becomes optional! You can now enable setuptools-scm with just:
-
- ```toml title="pyproject.toml"
- [build-system]
- requires = ["setuptools>=80", "setuptools-scm>=8"]
- build-backend = "setuptools.build_meta"
+### Running Tests
- [project]
- dynamic = ["version"]
- ```
+```bash
+# Run all tests
+uv run pytest -n12
- The `[tool.setuptools_scm]` section is only needed if you want to customize
- configuration options.
+# Run tests for setuptools-scm only
+uv run pytest setuptools-scm/testing_scm -n12
-Additionally, a version file can be written by specifying:
-
-```toml title="pyproject.toml"
-[tool.setuptools_scm]
-version_file = "pkg/_version.py"
+# Run tests for vcs-versioning only
+uv run pytest vcs-versioning/testing_vcs -n12
```
-Where `pkg` is the name of your package.
+### Building Documentation
-If you need to confirm which version string is being generated or debug the configuration,
-you can install [setuptools-scm] directly in your working environment and run:
+Documentation is shared across projects:
-```console
-$ python -m setuptools_scm
-# To explore other options, try:
-$ python -m setuptools_scm --help
+```bash
+uv run mkdocs serve
```
-For further configuration see the [documentation].
-
-[setuptools-scm]: https://github.com/pypa/setuptools-scm
-[documentation]: https://setuptools-scm.readthedocs.io/
-[git-archive-docs]: https://setuptools-scm.readthedocs.io/en/stable/usage/#builtin-mechanisms-for-obtaining-version-numbers
-
-
-## Interaction with Enterprise Distributions
-
-Some enterprise distributions like RHEL7
-ship rather old setuptools versions.
-
-In those cases its typically possible to build by using an sdist against `setuptools-scm<2.0`.
-As those old setuptools versions lack sensible types for versions,
-modern [setuptools-scm] is unable to support them sensibly.
-
-It's strongly recommended to build a wheel artifact using modern Python and setuptools,
-then installing the artifact instead of trying to run against old setuptools versions.
-
-!!! note "Legacy Setuptools Support"
- While setuptools-scm recommends setuptools >=80, it maintains compatibility with setuptools 61+
- to support legacy deployments that cannot easily upgrade. Support for setuptools <80 is deprecated
- and will be removed in a future release. This allows enterprise environments and older CI/CD systems
- to continue using setuptools-scm while still encouraging adoption of newer versions.
-
-
-## Code of Conduct
-
-
-Everyone interacting in the [setuptools-scm] project's codebases, issue
-trackers, chat rooms, and mailing lists is expected to follow the
-[PSF Code of Conduct].
+## Links
-[PSF Code of Conduct]: https://github.com/pypa/.github/blob/main/CODE_OF_CONDUCT.md
+- **Documentation**: https://setuptools-scm.readthedocs.io/
+- **Repository**: https://github.com/pypa/setuptools-scm/
+- **Issues**: https://github.com/pypa/setuptools-scm/issues
+## License
-## Security Contact
+Both projects are distributed under the terms of the MIT license.
-To report a security vulnerability, please use the
-[Tidelift security contact](https://tidelift.com/security).
-Tidelift will coordinate the fix and disclosure.
diff --git a/RELEASE_SYSTEM.md b/RELEASE_SYSTEM.md
new file mode 100644
index 00000000..69ccba74
--- /dev/null
+++ b/RELEASE_SYSTEM.md
@@ -0,0 +1,48 @@
+# Release System
+
+Towncrier-based release system for the setuptools-scm monorepo.
+
+## Components
+
+- `towncrier-fragments` version scheme: Determines version bumps from changelog fragment types
+- `changelog.d/` directories per project with fragment templates
+- GitHub workflows for release proposals and tag creation
+- Project-prefixed tags: `setuptools-scm-vX.Y.Z`, `vcs-versioning-vX.Y.Z`
+
+## Version Scheme
+
+Fragment types determine version bumps:
+- `removal` → major bump
+- `feature`, `deprecation` → minor bump
+- `bugfix`, `doc`, `misc` → patch bump
+
+Entry point: `vcs_versioning._version_schemes._towncrier:version_from_fragments`
+
+Tests: `vcs-versioning/testing_vcs/test_version_scheme_towncrier.py`
+
+## Workflows
+
+**Release Proposal** (`.github/workflows/release-proposal.yml`):
+Manual trigger, runs towncrier, creates labeled PR
+
+**Tag Creation** (`.github/workflows/create-release-tags.yml`):
+On PR merge, creates tags from PR title, triggers PyPI upload
+
+**Modified Upload** (`.github/workflows/python-tests.yml`):
+Split per-project upload jobs filtered by tag prefix
+
+## Usage
+
+**Contributors:** Add changelog fragment to `{project}/changelog.d/{number}.{type}.md`
+
+**Maintainers:** Trigger release proposal workflow, review PR, merge to create tags and upload to PyPI
+
+## Design Notes
+
+- Version scheme is single source of truth, no custom scripts
+- Manual approval via PR review
+- Workflows fail explicitly if required data is missing
+- Tag prefix filtering controls package uploads
+
+See [CONTRIBUTING.md](./CONTRIBUTING.md) and [TESTING.md](./TESTING.md) for details.
+
diff --git a/TESTING.md b/TESTING.md
new file mode 100644
index 00000000..863a9cd1
--- /dev/null
+++ b/TESTING.md
@@ -0,0 +1,161 @@
+# Testing Organization
+
+This document describes the test organization in the setuptools-scm monorepo.
+
+## Directory Structure
+
+The repository contains two test suites:
+
+- **`vcs-versioning/testing_vcs/`** - Core VCS versioning functionality tests
+- **`setuptools-scm/testing_scm/`** - Setuptools integration and wrapper tests
+
+## Separation Principle
+
+Tests are organized by architectural layer:
+
+### Core VCS Layer (`vcs-versioning/testing_vcs/`)
+
+Tests for core version control system functionality:
+- VCS backend operations (Git, Mercurial parsing)
+- Version scheme and formatting logic
+- Configuration validation
+- Version inference
+- Error handling
+- Core utility functions
+
+**When to add tests here:** If the functionality is in `vcs_versioning` package and doesn't depend on setuptools.
+
+### Setuptools Integration Layer (`setuptools-scm/testing_scm/`)
+
+Tests for setuptools-specific functionality:
+- Setuptools hooks and entry points
+- `setup.py` integration (` use_scm_version`)
+- `pyproject.toml` reading and Configuration.from_file()
+- File finding for setuptools (sdist integration)
+- Distribution metadata
+- setuptools-scm CLI wrapper
+
+**When to add tests here:** If the functionality is in `setuptools_scm` package or requires setuptools machinery.
+
+## Running Tests
+
+### Run all tests
+```bash
+uv run pytest -n12
+```
+
+### Run core VCS tests only
+```bash
+uv run pytest vcs-versioning/testing_vcs -n12
+```
+
+### Run setuptools integration tests only
+```bash
+uv run pytest setuptools-scm/testing_scm -n12
+```
+
+### Run specific test file
+```bash
+uv run pytest vcs-versioning/testing_vcs/test_version_schemes.py -v
+# Test the towncrier version scheme
+uv run pytest vcs-versioning/testing_vcs/test_version_scheme_towncrier.py -v
+```
+
+## Test Fixtures
+
+Both test suites use `vcs_versioning.test_api` as a pytest plugin, providing common test infrastructure:
+
+- `WorkDir`: Helper for creating temporary test repositories
+- `TEST_SOURCE_DATE`: Consistent test time for reproducibility
+- `DebugMode`: Context manager for debug logging
+- Repository fixtures: `wd`, `repositories_hg_git`, etc.
+
+See `vcs-versioning/src/vcs_versioning/test_api.py` and `vcs-versioning/src/vcs_versioning/_test_utils.py` for details.
+
+## Testing Release Workflows
+
+### Testing the towncrier-fragments Version Scheme
+
+The `towncrier-fragments` version scheme determines version bumps based on changelog fragments:
+
+```bash
+# Create test fragments
+echo "Test feature" > setuptools-scm/changelog.d/1.feature.md
+echo "Test bugfix" > setuptools-scm/changelog.d/2.bugfix.md
+
+# Check what version would be generated
+cd setuptools-scm
+uv run python -m vcs_versioning --root .. --version-scheme towncrier-fragments
+# Should show a minor bump (e.g., 9.3.0.dev...)
+
+# Clean up test fragments
+rm changelog.d/1.feature.md changelog.d/2.bugfix.md
+```
+
+### Testing Towncrier Build
+
+Test changelog generation without committing:
+
+```bash
+cd setuptools-scm
+
+# Dry-run: see what the changelog would look like
+uv run towncrier build --version 9.3.0 --draft
+
+# Build with keeping fragments (for testing)
+uv run towncrier build --version 9.3.0 --keep
+```
+
+### Testing Version Bump Logic
+
+Fragment types determine version bumps:
+
+- **removal** → Major bump (X.0.0)
+- **feature**, **deprecation** → Minor bump (0.X.0)
+- **bugfix**, **doc**, **misc** → Patch bump (0.0.X)
+
+Create different fragment types and verify the version scheme produces the expected version.
+
+### Local Release Workflow Testing
+
+You can test the release process locally (without actually creating tags):
+
+```bash
+# 1. Create test fragments
+echo "Add new feature" > setuptools-scm/changelog.d/999.feature.md
+
+# 2. Query version scheme
+cd setuptools-scm
+NEXT_VERSION=$(uv run python -m vcs_versioning --root .. --version-scheme towncrier-fragments --local-scheme no-local-version 2>/dev/null | grep -oP '^\d+\.\d+\.\d+')
+echo "Next version: $NEXT_VERSION"
+
+# 3. Build changelog (dry-run)
+uv run towncrier build --version "$NEXT_VERSION" --draft
+
+# 4. Clean up
+rm changelog.d/999.feature.md
+cd ..
+```
+
+### Workflow Validation
+
+Before merging workflow changes:
+
+1. Validate YAML syntax:
+ ```bash
+ # If you have actionlint installed
+ actionlint .github/workflows/*.yml
+ ```
+
+2. Check workflow conditions match your expectations:
+ - Tag filters in `python-tests.yml`
+ - Label checks in `create-release-tags.yml`
+
+3. Test in a fork with reduced scope (test project, Test PyPI)
+
+## Migration Notes
+
+File finders remain in setuptools-scm because they're setuptools integration (registered as `setuptools.file_finders` entry points), not core VCS functionality.
+
+For deeper unit test conversions beyond basic reorganization, see `setuptools-scm/testing_scm/INTEGRATION_MIGRATION_PLAN.md`.
+
diff --git a/_own_version_helper.py b/_own_version_helper.py
deleted file mode 100644
index 12ffeb07..00000000
--- a/_own_version_helper.py
+++ /dev/null
@@ -1,76 +0,0 @@
-"""
-this module is a hack only in place to allow for setuptools
-to use the attribute for the versions
-
-it works only if the backend-path of the build-system section
-from pyproject.toml is respected
-"""
-
-from __future__ import annotations
-
-import logging
-import os
-
-from typing import Callable
-
-from setuptools import build_meta as build_meta
-
-from setuptools_scm import Configuration
-from setuptools_scm import _types as _t
-from setuptools_scm import get_version
-from setuptools_scm import git
-from setuptools_scm import hg
-from setuptools_scm.fallbacks import parse_pkginfo
-from setuptools_scm.version import ScmVersion
-from setuptools_scm.version import get_local_node_and_date
-from setuptools_scm.version import get_no_local_node
-from setuptools_scm.version import guess_next_dev_version
-
-log = logging.getLogger("setuptools_scm")
-# todo: take fake entrypoints from pyproject.toml
-try_parse: list[Callable[[_t.PathT, Configuration], ScmVersion | None]] = [
- parse_pkginfo,
- git.parse,
- hg.parse,
- git.parse_archival,
- hg.parse_archival,
-]
-
-
-def parse(root: str, config: Configuration) -> ScmVersion | None:
- for maybe_parse in try_parse:
- try:
- parsed = maybe_parse(root, config)
- except OSError as e:
- log.warning("parse with %s failed with: %s", maybe_parse, e)
- else:
- if parsed is not None:
- return parsed
- return None
-
-
-def scm_version() -> str:
- # Use no-local-version if SETUPTOOLS_SCM_NO_LOCAL is set (for CI uploads)
- local_scheme = (
- get_no_local_node
- if os.environ.get("SETUPTOOLS_SCM_NO_LOCAL")
- else get_local_node_and_date
- )
-
- return get_version(
- relative_to=__file__,
- parse=parse,
- version_scheme=guess_next_dev_version,
- local_scheme=local_scheme,
- )
-
-
-version: str
-
-
-def __getattr__(name: str) -> str:
- if name == "version":
- global version
- version = scm_version()
- return version
- raise AttributeError(name)
diff --git a/docs/changelog.md b/docs/changelog.md
index 15fe40c9..172171bd 100644
--- a/docs/changelog.md
+++ b/docs/changelog.md
@@ -1,3 +1,3 @@
{%
- include-markdown "../CHANGELOG.md"
+ include-markdown "../setuptools-scm/CHANGELOG.md"
%}
diff --git a/docs/config.md b/docs/config.md
index 83d11e2b..a44ead6d 100644
--- a/docs/config.md
+++ b/docs/config.md
@@ -27,12 +27,13 @@ Use the `[tool.setuptools_scm]` section when you need to:
- Configure fallback behavior (`fallback_version`)
- Or any other non-default behavior
-## configuration parameters
+## Core Configuration
+
+These configuration options control version inference and formatting behavior.
Configuration parameters can be configured in `pyproject.toml` or `setup.py`.
Callables or other Python objects have to be passed in `setup.py` (via the `use_scm_version` keyword argument).
-
`root : Path | PathLike[str]`
: Relative path to the SCM root, defaults to `.` and is relative to the file path passed in `relative_to`
@@ -45,42 +46,12 @@ Callables or other Python objects have to be passed in `setup.py` (via the `use_
either an entrypoint name or a callable.
See [Version number construction](extending.md#setuptools_scmlocal_scheme) for predefined implementations.
-
-`version_file: Path | PathLike[str] | None = None`
-: A path to a file that gets replaced with a file containing the current
- version. It is ideal for creating a ``_version.py`` file within the
- package, typically used to avoid using `importlib.metadata`
- (which adds some overhead).
-
- !!! warning ""
-
- Only files with `.py` and `.txt` extensions have builtin templates,
- for other file types it is necessary to provide `version_file_template`.
-
-`version_file_template: str | None = None`
-: A new-style format string taking `version`, `scm_version` and `version_tuple` as parameters.
- `version` is the generated next_version as string,
- `version_tuple` is a tuple of split numbers/strings and
- `scm_version` is the `ScmVersion` instance the current `version` was rendered from
-
-
-`write_to: Pathlike[str] | Path | None = None`
-: (deprecated) legacy option to create a version file relative to the scm root
- it's broken for usage from a sdist and fixing it would be a fatal breaking change,
- use `version_file` instead.
-
-`relative_to: Path|Pathlike[str] = "pyproject.toml"`
-: A file/directory from which the root can be resolved.
- Typically called by a script or module that is not in the root of the
- repository to point `setuptools_scm` at the root of the repository by
- supplying `__file__`.
-
`tag_regex: str|Pattern[str]`
: A Python regex string to extract the version part from any SCM tag.
The regex needs to contain either a single match group, or a group
named `version`, that captures the actual version information.
- Defaults to the value of [setuptools_scm._config.DEFAULT_TAG_REGEX][]
+ Defaults to the value of [vcs_versioning._config.DEFAULT_TAG_REGEX][]
which supports tags with optional "v" prefix (recommended), project prefixes,
and various version formats.
@@ -118,16 +89,31 @@ Callables or other Python objects have to be passed in `setup.py` (via the `use_
available, `fallback_root` is used instead. This allows the same configuration
to work in both scenarios without modification.
-`parse: Callable[[Path, Config], ScmVersion] | None = None`
-: A function that will be used instead of the discovered SCM
- for parsing the version. Use with caution,
- this is a function for advanced use and you should be
- familiar with the `setuptools-scm` internals to use it.
+`normalize`
+: A boolean flag indicating if the version string should be normalized.
+ Defaults to `True`. Setting this to `False` is equivalent to setting
+ `version_cls` to [vcs_versioning.NonNormalizedVersion][]
+
+`version_cls: type|str = packaging.version.Version`
+: An optional class used to parse, verify and possibly normalize the version
+ string. Its constructor should receive a single string argument, and its
+ `str` should return the normalized version string to use.
+ This option can also receive a class qualified name as a string.
+
+ The [vcs_versioning.NonNormalizedVersion][] convenience class is
+ provided to disable the normalization step done by
+ `packaging.version.Version`. If this is used while `setuptools-scm`
+ is integrated in a setuptools packaging process, the non-normalized
+ version number will appear in all files (see `version_file` note).
+
+ !!! note "normalization still applies to artifact filenames"
+ Setuptools will still normalize it to create the final distribution,
+ so as to stay compliant with the python packaging standards.
`scm.git.describe_command`
: This command will be used instead the default `git describe --long` command.
- Defaults to the value set by [setuptools_scm.git.DEFAULT_DESCRIBE][]
+ Defaults to the value set by [vcs_versioning._backends._git.DEFAULT_DESCRIBE][]
`scm.git.pre_parse`
: A string specifying which git pre-parse function to use before parsing version information.
@@ -148,29 +134,49 @@ Callables or other Python objects have to be passed in `setup.py` (via the `use_
This field is maintained for backward compatibility but will issue a deprecation warning when used.
-`normalize`
-: A boolean flag indicating if the version string should be normalized.
- Defaults to `True`. Setting this to `False` is equivalent to setting
- `version_cls` to [setuptools_scm.NonNormalizedVersion][]
+`relative_to: Path|Pathlike[str] = "pyproject.toml"`
+: A file/directory from which the root can be resolved.
+ Typically called by a script or module that is not in the root of the
+ repository to point to the root of the repository by
+ supplying `__file__`.
-`version_cls: type|str = packaging.version.Version`
-: An optional class used to parse, verify and possibly normalize the version
- string. Its constructor should receive a single string argument, and its
- `str` should return the normalized version string to use.
- This option can also receive a class qualified name as a string.
+`parse: Callable[[Path, Config], ScmVersion] | None = None`
+: A function that will be used instead of the discovered SCM
+ for parsing the version. Use with caution,
+ this is a function for advanced use and you should be
+ familiar with the vcs-versioning internals to use it.
- The [setuptools_scm.NonNormalizedVersion][] convenience class is
- provided to disable the normalization step done by
- `packaging.version.Version`. If this is used while `setuptools-scm`
- is integrated in a setuptools packaging process, the non-normalized
- version number will appear in all files (see `version_file` note).
+`version_file: Path | PathLike[str] | None = None`
+: A path to a file that gets replaced with a file containing the current
+ version. It is ideal for creating a ``_version.py`` file within the
+ package, typically used to avoid using `importlib.metadata`
+ (which adds some overhead).
- !!! note "normalization still applies to artifact filenames"
- Setuptools will still normalize it to create the final distribution,
- so as to stay compliant with the python packaging standards.
+ !!! warning ""
+
+ Only files with `.py` and `.txt` extensions have builtin templates,
+ for other file types it is necessary to provide `version_file_template`.
+
+`version_file_template: str | None = None`
+: A new-style format string taking `version`, `scm_version` and `version_tuple` as parameters.
+ `version` is the generated next_version as string,
+ `version_tuple` is a tuple of split numbers/strings and
+ `scm_version` is the `ScmVersion` instance the current `version` was rendered from
+
+## setuptools-scm Specific Configuration
+
+These options control setuptools integration behavior.
+`write_to: Pathlike[str] | Path | None = None`
+: (deprecated) legacy option to create a version file relative to the scm root
+ it's broken for usage from a sdist and fixing it would be a fatal breaking change,
+ use `version_file` instead.
-## environment variables
+## Environment Variables
+
+### Version Detection Overrides
+
+These environment variables override version detection behavior.
`SETUPTOOLS_SCM_PRETEND_VERSION`
: used as the primary source for the version number
@@ -193,14 +199,25 @@ Callables or other Python objects have to be passed in `setup.py` (via the `use_
this will take precedence over ``SETUPTOOLS_SCM_PRETEND_VERSION``
+`SETUPTOOLS_SCM_PRETEND_METADATA`
+: A TOML inline table for overriding individual version metadata fields.
+ See the [overrides documentation](overrides.md#pretend-metadata-core) for details.
+
+`SETUPTOOLS_SCM_PRETEND_METADATA_FOR_${DIST_NAME}`
+: Same as above but specific to a package (recommended over the generic version).
+
`SETUPTOOLS_SCM_DEBUG`
-: enable the debug logging
+: Enable debug logging for version detection and processing.
`SOURCE_DATE_EPOCH`
-: used as the timestamp from which the
+: Used as the timestamp from which the
``node-and-date`` and ``node-and-timestamp`` local parts are
- derived, otherwise the current time is used
- (https://reproducible-builds.org/docs/source-date-epoch/)
+ derived, otherwise the current time is used.
+ Standard environment variable from [reproducible-builds.org](https://reproducible-builds.org/docs/source-date-epoch/).
+
+### setuptools-scm Overrides
+
+These environment variables control setuptools-scm specific behavior.
`SETUPTOOLS_SCM_IGNORE_VCS_ROOTS`
: a ``os.pathsep`` separated list
@@ -211,11 +228,15 @@ Callables or other Python objects have to be passed in `setup.py` (via the `use_
for example, set this to ``chg`` to reduce start-up overhead of Mercurial
+`SETUPTOOLS_SCM_OVERRIDES_FOR_${DIST_NAME}`
+: A TOML inline table to override configuration from `pyproject.toml`.
+ See the [overrides documentation](overrides.md#config-overrides) for details.
+`SETUPTOOLS_SCM_SUBPROCESS_TIMEOUT`
+: Override the subprocess timeout (default: 40 seconds).
+ See the [overrides documentation](overrides.md#subprocess-timeouts) for details.
-
-
-## automatic file inclusion
+## Automatic File Inclusion
!!! warning "Setuptools File Finder Integration"
@@ -274,21 +295,20 @@ tar -tzf dist/package-*.tar.gz
The file finder cannot be disabled through configuration - it's automatically active when setuptools-scm is installed. If you need to disable it completely, you must remove setuptools-scm from your build environment (which also means you can't use it for versioning).
-
-## api reference
+## API Reference
### constants
-::: setuptools_scm._config.DEFAULT_TAG_REGEX
+::: vcs_versioning._config.DEFAULT_TAG_REGEX
options:
heading_level: 4
-::: setuptools_scm.git.DEFAULT_DESCRIBE
+::: vcs_versioning._backends._git.DEFAULT_DESCRIBE
options:
heading_level: 4
### the configuration class
-::: setuptools_scm.Configuration
+::: vcs_versioning.Configuration
options:
heading_level: 4
diff --git a/docs/customizing.md b/docs/customizing.md
index 18ee8765..4c74cded 100644
--- a/docs/customizing.md
+++ b/docs/customizing.md
@@ -68,4 +68,4 @@ setup(use_scm_version={'local_scheme': clean_scheme})
## alternative version classes
-::: setuptools_scm.NonNormalizedVersion
+::: vcs_versioning.NonNormalizedVersion
diff --git a/docs/examples/version_scheme_code/setup.py b/docs/examples/version_scheme_code/setup.py
index 69f903f2..e0c3bb86 100644
--- a/docs/examples/version_scheme_code/setup.py
+++ b/docs/examples/version_scheme_code/setup.py
@@ -3,7 +3,6 @@
from __future__ import annotations
from setuptools import setup
-
from setuptools_scm import ScmVersion
diff --git a/docs/extending.md b/docs/extending.md
index c4cc2e03..945ae92f 100644
--- a/docs/extending.md
+++ b/docs/extending.md
@@ -14,8 +14,8 @@
entrypoint's name. E.g. for the built-in entrypoint for Git the
entrypoint is named `.git` and references `setuptools_scm.git:parse`
- The return value MUST be a [`setuptools_scm.version.ScmVersion`][] instance
- created by the function [`setuptools_scm.version.meta`][].
+ The return value MUST be a [`vcs_versioning.ScmVersion`][] instance
+ created by the function [`vcs_versioning._version_schemes.meta`][].
`setuptools_scm.files_command`
: Either a string containing a shell command that prints all SCM managed
@@ -27,25 +27,21 @@
### api reference for scm version objects
-::: setuptools_scm.version.ScmVersion
+::: vcs_versioning.ScmVersion
options:
show_root_heading: yes
heading_level: 4
-::: setuptools_scm.version.meta
+::: vcs_versioning._version_schemes.meta
options:
show_root_heading: yes
heading_level: 4
## Version number construction
-
-
-
-
### `setuptools_scm.version_scheme`
Configures how the version number is constructed given a
-[ScmVersion][setuptools_scm.version.ScmVersion] instance and should return a string
+[ScmVersion][vcs_versioning.ScmVersion] instance and should return a string
representing the version.
### Available implementations
@@ -130,7 +126,7 @@ representing the version.
### `setuptools_scm.local_scheme`
Configures how the local part of a version is rendered given a
-[ScmVersion][setuptools_scm.version.ScmVersion] instance and should return a string
+[ScmVersion][vcs_versioning.ScmVersion] instance and should return a string
representing the local version.
Dates and times are in Coordinated Universal Time (UTC), because as part
of the version, they should be location independent.
diff --git a/docs/index.md b/docs/index.md
index c86f93ce..b5774e03 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -22,6 +22,26 @@ or [configuring Git archive][git-archive-docs].
[git-archive-docs]: usage.md#builtin-mechanisms-for-obtaining-version-numbers
+## Architecture
+
+`setuptools-scm` is built on top of [`vcs-versioning`](https://pypi.org/project/vcs-versioning/),
+a standalone library that provides the core VCS version extraction and formatting functionality.
+
+**vcs-versioning** (core library):
+: Handles version extraction from Git and Mercurial repositories, version scheme logic,
+ tag parsing, and version formatting. These are universal concepts that work across
+ different build systems and integrations.
+
+**setuptools-scm** (integration layer):
+: Provides setuptools-specific features like build-time integration, automatic file
+ finder registration, and version file generation during package builds.
+
+!!! info "Understanding the documentation"
+
+ Most configuration options documented here are **core vcs-versioning features** that
+ work universally. Features specific to setuptools-scm integration (like automatic
+ file finders or version file writing) are clearly marked throughout the documentation.
+
## Basic usage
### With setuptools
diff --git a/docs/integrators.md b/docs/integrators.md
new file mode 100644
index 00000000..b59d5731
--- /dev/null
+++ b/docs/integrators.md
@@ -0,0 +1,1002 @@
+# Integrator Guide
+
+This guide is for developers building tools that integrate vcs-versioning (like hatch-vcs, custom build backends, or other version management tools).
+
+## Overview
+
+vcs-versioning provides a flexible override system that allows integrators to:
+
+- Use custom environment variable prefixes (e.g., `HATCH_VCS_*` instead of `SETUPTOOLS_SCM_*`)
+- Automatically fall back to `VCS_VERSIONING_*` variables for universal configuration
+- Apply global overrides once at entry points using a context manager pattern
+- Access override values throughout the execution via thread-safe accessor functions
+
+## Quick Start
+
+The simplest way to use the overrides system is with the `GlobalOverrides` context manager:
+
+```python
+from vcs_versioning.overrides import GlobalOverrides
+from vcs_versioning import infer_version_string
+
+# Use your own prefix
+with GlobalOverrides.from_env("HATCH_VCS"):
+ # All modules now use HATCH_VCS_* env vars with VCS_VERSIONING_* fallback
+ version = infer_version_string(
+ dist_name="my-package",
+ pyproject_data=pyproject_data,
+ )
+```
+
+That's it! The context manager:
+1. Reads all global override values from environment variables
+2. Makes them available to all vcs-versioning internal modules
+3. Automatically cleans up when exiting the context
+
+## GlobalOverrides Context Manager
+
+### Basic Usage
+
+```python
+from vcs_versioning.overrides import GlobalOverrides
+
+with GlobalOverrides.from_env("YOUR_TOOL"):
+ # Your version detection code here
+ pass
+```
+
+### What Gets Configured
+
+The `GlobalOverrides` context manager reads and applies these configuration values, and automatically configures logging:
+
+| Field | Environment Variables | Default | Description |
+|-------|----------------------|---------|-------------|
+| `debug` | `{TOOL}_DEBUG`
`VCS_VERSIONING_DEBUG` | `False` (WARNING level) | Debug logging level (int) or False |
+| `subprocess_timeout` | `{TOOL}_SUBPROCESS_TIMEOUT`
`VCS_VERSIONING_SUBPROCESS_TIMEOUT` | `40` | Timeout for subprocess commands in seconds |
+| `hg_command` | `{TOOL}_HG_COMMAND`
`VCS_VERSIONING_HG_COMMAND` | `"hg"` | Command to use for Mercurial operations |
+| `source_date_epoch` | `SOURCE_DATE_EPOCH` | `None` | Unix timestamp for reproducible builds |
+
+**Note:** Logging is automatically configured when entering the `GlobalOverrides` context. The debug level is used to set the log level for all vcs-versioning and setuptools-scm loggers.
+
+### Debug Logging Levels
+
+The `debug` field supports multiple formats:
+
+```bash
+# Boolean flag - enables DEBUG level
+export HATCH_VCS_DEBUG=1
+
+# Explicit log level (int from logging module)
+export HATCH_VCS_DEBUG=10 # DEBUG
+export HATCH_VCS_DEBUG=20 # INFO
+export HATCH_VCS_DEBUG=30 # WARNING
+
+# Omitted or empty - uses WARNING level (default)
+```
+
+### Accessing Override Values
+
+Within the context, you can access override values:
+
+```python
+from vcs_versioning.overrides import GlobalOverrides, get_active_overrides
+
+with GlobalOverrides.from_env("HATCH_VCS") as overrides:
+ # Direct access
+ print(f"Debug level: {overrides.debug}")
+ print(f"Timeout: {overrides.subprocess_timeout}")
+
+ # Or via accessor function
+ current = get_active_overrides()
+ log_level = current.log_level() # Returns int from logging module
+```
+
+### Creating Modified Overrides
+
+Use `from_active()` to create a modified version of the currently active overrides:
+
+```python
+from vcs_versioning.overrides import GlobalOverrides
+import logging
+
+with GlobalOverrides.from_env("TOOL"):
+ # Original context with default settings
+
+ # Create a nested context with modified values
+ with GlobalOverrides.from_active(debug=logging.INFO, subprocess_timeout=100):
+ # This context has INFO logging and 100s timeout
+ # Other fields (hg_command, source_date_epoch, tool) are preserved
+ pass
+```
+
+This is particularly useful in tests where you want to modify specific overrides without affecting others:
+
+```python
+def test_with_custom_timeout():
+ # Start with standard test overrides
+ with GlobalOverrides.from_active(subprocess_timeout=5):
+ # Test with short timeout
+ pass
+```
+
+### Exporting Overrides
+
+Use `export()` to export overrides to environment variables or pytest monkeypatch:
+
+```python
+from vcs_versioning.overrides import GlobalOverrides
+
+# Export to environment dict
+overrides = GlobalOverrides.from_env("TOOL", env={"TOOL_DEBUG": "INFO"})
+env = {}
+overrides.export(env)
+# env now contains: {"TOOL_DEBUG": "20", "TOOL_SUBPROCESS_TIMEOUT": "40", ...}
+
+# Export via pytest monkeypatch
+def test_subprocess(monkeypatch):
+ overrides = GlobalOverrides.from_active(debug=logging.DEBUG)
+ overrides.export(monkeypatch)
+ # Environment is now set for subprocess calls
+ result = subprocess.run(["my-command"], env=os.environ)
+```
+
+This is useful when you need to:
+- Pass overrides to subprocesses
+- Set up environment for integration tests
+- Export configuration for external tools
+
+## Automatic Fallback Behavior
+
+The overrides system checks environment variables in this order:
+
+1. **Tool-specific prefix**: `{YOUR_TOOL}_*`
+2. **VCS_VERSIONING prefix**: `VCS_VERSIONING_*` (universal fallback)
+3. **Default value**: Hard-coded defaults
+
+### Example
+
+```python
+with GlobalOverrides.from_env("HATCH_VCS"):
+ # Checks in order:
+ # 1. HATCH_VCS_DEBUG
+ # 2. VCS_VERSIONING_DEBUG
+ # 3. Default: False (WARNING level)
+ pass
+```
+
+This means:
+- Users can set `VCS_VERSIONING_DEBUG=1` to enable debug mode for all tools
+- Or set `HATCH_VCS_DEBUG=1` to enable it only for hatch-vcs
+- The tool-specific setting takes precedence
+
+## Distribution-Specific Overrides
+
+For dist-specific overrides like pretend versions and metadata, use `EnvReader`:
+
+```python
+from vcs_versioning.overrides import EnvReader
+import os
+
+# Read pretend version for a specific distribution
+reader = EnvReader(
+ tools_names=("HATCH_VCS", "VCS_VERSIONING"),
+ env=os.environ,
+ dist_name="my-package",
+)
+pretend_version = reader.read("PRETEND_VERSION")
+
+# This checks in order:
+# 1. HATCH_VCS_PRETEND_VERSION_FOR_MY_PACKAGE
+# 2. VCS_VERSIONING_PRETEND_VERSION_FOR_MY_PACKAGE
+# 3. HATCH_VCS_PRETEND_VERSION (generic)
+# 4. VCS_VERSIONING_PRETEND_VERSION (generic)
+```
+
+### Distribution Name Normalization
+
+Distribution names are normalized following PEP 503 semantics, then converted to environment variable format:
+
+```python
+"my-package" → "MY_PACKAGE"
+"My.Package_123" → "MY_PACKAGE_123"
+"pkg--name___v2" → "PKG_NAME_V2"
+```
+
+The normalization:
+1. Uses `packaging.utils.canonicalize_name()` (PEP 503)
+2. Replaces `-` with `_`
+3. Converts to uppercase
+
+## EnvReader: Advanced Environment Variable Reading
+
+The `EnvReader` class is the core utility for reading environment variables with automatic fallback between tool prefixes. While `GlobalOverrides` handles the standard global overrides automatically, `EnvReader` is useful when you need to read custom or distribution-specific environment variables.
+
+### Basic Usage
+
+```python
+from vcs_versioning.overrides import EnvReader
+import os
+
+# Create reader with tool prefix fallback
+reader = EnvReader(
+ tools_names=("HATCH_VCS", "VCS_VERSIONING"),
+ env=os.environ,
+)
+
+# Read simple values
+debug = reader.read("DEBUG")
+timeout = reader.read("SUBPROCESS_TIMEOUT")
+custom = reader.read("MY_CUSTOM_VAR")
+
+# Returns None if not found
+value = reader.read("NONEXISTENT") # None
+```
+
+### Reading Distribution-Specific Variables
+
+When you provide a `dist_name`, `EnvReader` automatically checks distribution-specific variants first:
+
+```python
+reader = EnvReader(
+ tools_names=("HATCH_VCS", "VCS_VERSIONING"),
+ env=os.environ,
+ dist_name="my-package",
+)
+
+# Reading "PRETEND_VERSION" checks in order:
+# 1. HATCH_VCS_PRETEND_VERSION_FOR_MY_PACKAGE (tool + dist)
+# 2. VCS_VERSIONING_PRETEND_VERSION_FOR_MY_PACKAGE (fallback + dist)
+# 3. HATCH_VCS_PRETEND_VERSION (tool generic)
+# 4. VCS_VERSIONING_PRETEND_VERSION (fallback generic)
+pretend = reader.read("PRETEND_VERSION")
+```
+
+### Reading TOML Configuration
+
+For structured configuration, use `read_toml()` with TypedDict schemas:
+
+```python
+from typing import TypedDict
+from vcs_versioning.overrides import EnvReader
+
+class MyConfigSchema(TypedDict, total=False):
+ """Schema for configuration validation."""
+ local_scheme: str
+ version_scheme: str
+ timeout: int
+ enabled: bool
+
+reader = EnvReader(
+ tools_names=("MY_TOOL", "VCS_VERSIONING"),
+ env={
+ "MY_TOOL_CONFIG": '{local_scheme = "no-local-version", timeout = 120}'
+ }
+)
+
+# Parse TOML with schema validation
+config = reader.read_toml("CONFIG", schema=MyConfigSchema)
+# Result: {'local_scheme': 'no-local-version', 'timeout': 120}
+
+# Invalid fields are automatically removed and logged as warnings
+```
+
+**TOML Format Support:**
+
+- **Inline maps**: `{key = "value", number = 42}`
+- **Full documents**: Multi-line TOML with proper structure
+- **Type coercion**: TOML types are preserved (int, bool, datetime, etc.)
+
+### Error Handling and Diagnostics
+
+`EnvReader` provides helpful diagnostics for common mistakes:
+
+#### Alternative Normalizations
+
+If you use a slightly different normalization, you'll get a warning:
+
+```python
+reader = EnvReader(
+ tools_names=("TOOL",),
+ env={"TOOL_VAR_FOR_MY-PACKAGE": "value"}, # Using dashes
+ dist_name="my-package"
+)
+
+value = reader.read("VAR")
+# Warning: Found environment variable 'TOOL_VAR_FOR_MY-PACKAGE' for dist name 'my-package',
+# but expected 'TOOL_VAR_FOR_MY_PACKAGE'. Consider using the standard normalized name.
+# Returns: "value" (still works!)
+```
+
+#### Typo Detection
+
+If you have a typo in the distribution name suffix, you'll get suggestions:
+
+```python
+reader = EnvReader(
+ tools_names=("TOOL",),
+ env={"TOOL_VAR_FOR_MY_PACKGE": "value"}, # Typo: PACKAGE
+ dist_name="my-package"
+)
+
+value = reader.read("VAR")
+# Warning: Environment variable 'TOOL_VAR_FOR_MY_PACKAGE' not found for dist name 'my-package'
+# (canonicalized as 'my-package'). Did you mean one of these? ['TOOL_VAR_FOR_MY_PACKGE']
+# Returns: None (doesn't match)
+```
+
+### Common Patterns
+
+#### Pattern: Reading Pretend Metadata (TOML)
+
+```python
+from vcs_versioning._overrides import PretendMetadataDict
+from vcs_versioning.overrides import EnvReader
+
+reader = EnvReader(
+ tools_names=("MY_TOOL", "VCS_VERSIONING"),
+ env=os.environ,
+ dist_name="my-package"
+)
+
+# Read TOML metadata
+metadata = reader.read_toml("PRETEND_METADATA", schema=PretendMetadataDict)
+# Example result: {'node': 'g1337beef', 'distance': 4, 'dirty': False}
+```
+
+#### Pattern: Reading Configuration Overrides
+
+```python
+from vcs_versioning._overrides import ConfigOverridesDict
+from vcs_versioning.overrides import EnvReader
+
+reader = EnvReader(
+ tools_names=("MY_TOOL", "VCS_VERSIONING"),
+ env=os.environ,
+ dist_name="my-package"
+)
+
+# Read config overrides
+overrides = reader.read_toml("OVERRIDES", schema=ConfigOverridesDict)
+# Example: {'local_scheme': 'no-local-version', 'version_scheme': 'release-branch-semver'}
+```
+
+#### Pattern: Reusing Reader for Multiple Reads
+
+```python
+reader = EnvReader(
+ tools_names=("MY_TOOL", "VCS_VERSIONING"),
+ env=os.environ,
+ dist_name="my-package"
+)
+
+# Efficient: reuse reader for multiple variables
+pretend_version = reader.read("PRETEND_VERSION")
+pretend_metadata = reader.read_toml("PRETEND_METADATA", schema=PretendMetadataDict)
+config_overrides = reader.read_toml("OVERRIDES", schema=ConfigOverridesDict)
+custom_setting = reader.read("CUSTOM_SETTING")
+```
+
+### When to Use EnvReader
+
+**Use `EnvReader` when you need to:**
+
+- Read custom environment variables beyond the standard global overrides
+- Support distribution-specific configuration
+- Parse structured TOML data from environment variables
+- Implement your own override system on top of vcs-versioning
+
+**Don't use `EnvReader` for:**
+
+- Standard global overrides (debug, timeout, etc.) - use `GlobalOverrides` instead
+- One-time reads - it's designed for efficiency with multiple reads
+
+### EnvReader vs GlobalOverrides
+
+| Feature | `GlobalOverrides` | `EnvReader` |
+|---------|------------------|-------------|
+| **Purpose** | Manage standard global overrides | Read any custom env vars |
+| **Context Manager** | ✅ Yes | ❌ No |
+| **Auto-configures logging** | ✅ Yes | ❌ No |
+| **Tool fallback** | ✅ Automatic | ✅ Automatic |
+| **Dist-specific vars** | ❌ No | ✅ Yes |
+| **TOML parsing** | ❌ No | ✅ Yes |
+| **Use case** | Entry point setup | Custom config reading |
+
+**Typical usage together:**
+
+```python
+from vcs_versioning.overrides import GlobalOverrides, EnvReader
+import os
+
+# Apply global overrides
+with GlobalOverrides.from_env("MY_TOOL"):
+ # Read custom configuration
+ reader = EnvReader(
+ tools_names=("MY_TOOL", "VCS_VERSIONING"),
+ env=os.environ,
+ dist_name="my-package"
+ )
+
+ custom_config = reader.read_toml("CUSTOM_CONFIG", schema=MySchema)
+
+ # Both global overrides and custom config are now available
+ version = detect_version_with_config(custom_config)
+```
+
+## Environment Variable Patterns
+
+### Global Override Patterns
+
+| Override | Environment Variables | Example |
+|----------|----------------------|---------|
+| Debug | `{TOOL}_DEBUG`
`VCS_VERSIONING_DEBUG` | `HATCH_VCS_DEBUG=1` |
+| Subprocess Timeout | `{TOOL}_SUBPROCESS_TIMEOUT`
`VCS_VERSIONING_SUBPROCESS_TIMEOUT` | `HATCH_VCS_SUBPROCESS_TIMEOUT=120` |
+| Mercurial Command | `{TOOL}_HG_COMMAND`
`VCS_VERSIONING_HG_COMMAND` | `HATCH_VCS_HG_COMMAND=chg` |
+| Source Date Epoch | `SOURCE_DATE_EPOCH` | `SOURCE_DATE_EPOCH=1672531200` |
+
+### Distribution-Specific Patterns
+
+| Override | Environment Variables | Example |
+|----------|----------------------|---------|
+| Pretend Version (specific) | `{TOOL}_PRETEND_VERSION_FOR_{DIST}`
`VCS_VERSIONING_PRETEND_VERSION_FOR_{DIST}` | `HATCH_VCS_PRETEND_VERSION_FOR_MY_PKG=1.0.0` |
+| Pretend Version (generic) | `{TOOL}_PRETEND_VERSION`
`VCS_VERSIONING_PRETEND_VERSION` | `HATCH_VCS_PRETEND_VERSION=1.0.0` |
+| Pretend Metadata (specific) | `{TOOL}_PRETEND_METADATA_FOR_{DIST}`
`VCS_VERSIONING_PRETEND_METADATA_FOR_{DIST}` | `HATCH_VCS_PRETEND_METADATA_FOR_MY_PKG='{node="g123", distance=4}'` |
+| Pretend Metadata (generic) | `{TOOL}_PRETEND_METADATA`
`VCS_VERSIONING_PRETEND_METADATA` | `HATCH_VCS_PRETEND_METADATA='{dirty=true}'` |
+| Config Overrides (specific) | `{TOOL}_OVERRIDES_FOR_{DIST}`
`VCS_VERSIONING_OVERRIDES_FOR_{DIST}` | `HATCH_VCS_OVERRIDES_FOR_MY_PKG='{"local_scheme": "no-local-version"}'` |
+
+## Complete Integration Example
+
+Here's a complete example of integrating vcs-versioning into a build backend:
+
+```python
+# my_build_backend.py
+from __future__ import annotations
+
+from typing import Any
+
+from vcs_versioning.overrides import GlobalOverrides
+from vcs_versioning import infer_version_string
+
+
+def get_version_for_build(
+ dist_name: str,
+ pyproject_data: dict[str, Any],
+ config_overrides: dict[str, Any] | None = None,
+) -> str:
+ """Get version for build, using MYBUILD_* environment variables.
+
+ Args:
+ dist_name: The distribution/package name (e.g., "my-package")
+ pyproject_data: Parsed pyproject.toml data
+ config_overrides: Optional configuration overrides
+
+ Returns:
+ The computed version string
+ """
+
+ # Apply global overrides with custom prefix
+ # Logging is automatically configured based on MYBUILD_DEBUG
+ with GlobalOverrides.from_env("MYBUILD"):
+ # Get version - all subprocess calls and logging respect MYBUILD_* vars
+ # dist_name is used for distribution-specific env var lookups
+ version = infer_version_string(
+ dist_name=dist_name,
+ pyproject_data=pyproject_data,
+ overrides=config_overrides,
+ )
+
+ return version
+```
+
+### Usage
+
+The function is called with the distribution name, enabling package-specific overrides:
+
+```python
+# Example: Using in a build backend
+version = get_version_for_build(
+ dist_name="my-package",
+ pyproject_data=parsed_pyproject,
+ config_overrides={"local_scheme": "no-local-version"},
+)
+```
+
+Environment variables can override behavior per package:
+
+```bash
+# Enable debug logging for this tool only
+export MYBUILD_DEBUG=1
+
+# Or use universal VCS_VERSIONING prefix
+export VCS_VERSIONING_DEBUG=1
+
+# Override subprocess timeout
+export MYBUILD_SUBPROCESS_TIMEOUT=120
+
+# Pretend version for specific package (dist_name="my-package")
+export MYBUILD_PRETEND_VERSION_FOR_MY_PACKAGE=1.2.3.dev4
+
+# Or generic pretend version (applies to all packages)
+export MYBUILD_PRETEND_VERSION=1.2.3
+
+python -m build
+```
+
+## Testing with Custom Prefixes
+
+When testing your integration, you can mock the environment:
+
+```python
+import pytest
+from vcs_versioning.overrides import GlobalOverrides
+
+
+def test_with_custom_overrides():
+ """Test version detection with custom override prefix."""
+ mock_env = {
+ "MYTEST_DEBUG": "1",
+ "MYTEST_SUBPROCESS_TIMEOUT": "60",
+ "SOURCE_DATE_EPOCH": "1672531200",
+ }
+
+ with GlobalOverrides.from_env("MYTEST", env=mock_env) as overrides:
+ # Verify overrides loaded correctly
+ assert overrides.debug != False
+ assert overrides.subprocess_timeout == 60
+ assert overrides.source_date_epoch == 1672531200
+
+ # Test your version detection logic
+ version = detect_version_somehow()
+ assert version is not None
+
+
+def test_with_vcs_versioning_fallback():
+ """Test that VCS_VERSIONING prefix works as fallback."""
+ mock_env = {
+ "VCS_VERSIONING_DEBUG": "1",
+ # No MYTEST_ variables
+ }
+
+ with GlobalOverrides.from_env("MYTEST", env=mock_env) as overrides:
+ # Should use VCS_VERSIONING fallback
+ assert overrides.debug != False
+```
+
+## Advanced Usage
+
+### Inspecting Active Overrides
+
+```python
+from vcs_versioning import get_active_overrides
+
+# Outside any context
+overrides = get_active_overrides()
+assert overrides is None
+
+# Inside a context
+with GlobalOverrides.from_env("HATCH_VCS"):
+ overrides = get_active_overrides()
+ assert overrides is not None
+ assert overrides.tool == "HATCH_VCS"
+```
+
+### Using Accessor Functions Directly
+
+```python
+from vcs_versioning import (
+ get_debug_level,
+ get_subprocess_timeout,
+ get_hg_command,
+ get_source_date_epoch,
+)
+
+with GlobalOverrides.from_env("HATCH_VCS"):
+ # These functions return values from the active context
+ debug = get_debug_level()
+ timeout = get_subprocess_timeout()
+ hg_cmd = get_hg_command()
+ epoch = get_source_date_epoch()
+```
+
+Outside a context, these functions fall back to reading `os.environ` directly for backward compatibility.
+
+### Custom Distribution-Specific Overrides
+
+If you need to read custom dist-specific overrides:
+
+```python
+from vcs_versioning.overrides import EnvReader
+import os
+
+# Read a custom override
+reader = EnvReader(
+ tools_names=("HATCH_VCS", "VCS_VERSIONING"),
+ env=os.environ,
+ dist_name="my-package",
+)
+custom_value = reader.read("MY_CUSTOM_SETTING")
+
+# This checks in order:
+# 1. HATCH_VCS_MY_CUSTOM_SETTING_FOR_MY_PACKAGE
+# 2. VCS_VERSIONING_MY_CUSTOM_SETTING_FOR_MY_PACKAGE
+# 3. HATCH_VCS_MY_CUSTOM_SETTING
+# 4. VCS_VERSIONING_MY_CUSTOM_SETTING
+```
+
+`EnvReader` includes fuzzy matching and helpful warnings if users specify distribution names incorrectly.
+
+## Best Practices
+
+### 1. Choose Descriptive Prefixes
+
+Use clear, tool-specific prefixes:
+- ✅ `HATCH_VCS`, `MYBUILD`, `POETRY_VCS`
+- ❌ `TOOL`, `MY`, `X`
+
+### 2. Apply Context at Entry Points
+
+Apply the `GlobalOverrides` context once at your tool's entry point, not repeatedly:
+
+```python
+# ✅ Good - apply once at entry point
+def main():
+ with GlobalOverrides.from_env("HATCH_VCS"):
+ # All operations here have access to overrides
+ build_project()
+
+# ❌ Bad - repeated context application
+def build_project():
+ with GlobalOverrides.from_env("HATCH_VCS"):
+ get_version()
+
+ with GlobalOverrides.from_env("HATCH_VCS"): # Wasteful
+ write_version_file()
+```
+
+### 3. Document Your Environment Variables
+
+Document the environment variables your tool supports, including the fallback behavior:
+
+```markdown
+## Environment Variables
+
+- `HATCH_VCS_DEBUG`: Enable debug logging (falls back to `VCS_VERSIONING_DEBUG`)
+- `HATCH_VCS_PRETEND_VERSION_FOR_{DIST}`: Override version for distribution
+```
+
+### 4. Test Both Prefixes
+
+Test that both your custom prefix and the `VCS_VERSIONING_*` fallback work:
+
+```python
+def test_custom_prefix():
+ with GlobalOverrides.from_env("MYTOOL", env={"MYTOOL_DEBUG": "1"}):
+ ...
+
+def test_fallback_prefix():
+ with GlobalOverrides.from_env("MYTOOL", env={"VCS_VERSIONING_DEBUG": "1"}):
+ ...
+```
+
+### 5. Avoid Nesting Contexts
+
+Don't nest `GlobalOverrides` contexts - it's rarely needed and can be confusing:
+
+```python
+# ❌ Avoid this
+with GlobalOverrides.from_env("TOOL1"):
+ with GlobalOverrides.from_env("TOOL2"): # Inner context shadows outer
+ ...
+```
+
+## Thread Safety
+
+The override system uses `contextvars.ContextVar` for thread-local storage, making it safe for concurrent execution:
+
+```python
+import concurrent.futures
+from vcs_versioning.overrides import GlobalOverrides
+
+def build_package(tool_prefix: str) -> str:
+ with GlobalOverrides.from_env(tool_prefix):
+ return get_version()
+
+# Each thread has its own override context
+with concurrent.futures.ThreadPoolExecutor() as executor:
+ futures = [
+ executor.submit(build_package, "TOOL1"),
+ executor.submit(build_package, "TOOL2"),
+ ]
+ results = [f.result() for f in futures]
+```
+
+## Migration from Direct Environment Reads
+
+If you're migrating code that directly reads environment variables:
+
+```python
+# Before
+import os
+
+def my_function():
+ debug = os.environ.get("SETUPTOOLS_SCM_DEBUG")
+ timeout = int(os.environ.get("SETUPTOOLS_SCM_SUBPROCESS_TIMEOUT", "40"))
+ # ...
+
+# After
+from vcs_versioning.overrides import GlobalOverrides
+
+def main():
+ with GlobalOverrides.from_env("MYTOOL"):
+ my_function() # Now uses override context automatically
+
+def my_function():
+ # No changes needed! Internal vcs-versioning code uses the context
+ pass
+```
+
+All internal vcs-versioning modules automatically use the active override context, so you don't need to change their usage.
+
+## Experimental Integrator API
+
+!!! warning "Experimental"
+ This API is marked as experimental and may change in future versions.
+ Use with caution in production code.
+
+vcs-versioning provides helper functions for integrators to build configurations with proper override priority handling.
+
+### Overview
+
+The experimental API provides:
+- `PyProjectData`: Public class for composing pyproject.toml data
+- `build_configuration_from_pyproject()`: Substantial orchestration helper for building Configuration
+
+### Priority Order
+
+When building configurations, overrides are applied in this priority order (highest to lowest):
+
+1. **Environment TOML overrides** - `TOOL_OVERRIDES_FOR_DIST`, `TOOL_OVERRIDES`
+2. **Integrator overrides** - Python arguments passed by the integrator
+3. **Config file** - `pyproject.toml` `[tool.vcs-versioning]` section
+4. **Defaults** - vcs-versioning defaults
+
+This ensures that:
+- Users can always override via environment variables
+- Integrators can provide their own defaults/transformations
+- Config file settings are respected
+- Sensible defaults are always available
+
+### Basic Workflow
+
+```python
+from vcs_versioning import (
+ PyProjectData,
+ build_configuration_from_pyproject,
+ infer_version_string,
+)
+from vcs_versioning.overrides import GlobalOverrides
+
+def get_version_for_my_tool(pyproject_path="pyproject.toml", dist_name=None):
+ """Complete integrator workflow."""
+ # 1. Setup global overrides context (handles env vars, logging, etc.)
+ with GlobalOverrides.from_env("MY_TOOL", dist_name=dist_name):
+
+ # 2. Load pyproject data
+ pyproject = PyProjectData.from_file(pyproject_path)
+
+ # 3. Build configuration with proper override priority
+ config = build_configuration_from_pyproject(
+ pyproject_data=pyproject,
+ dist_name=dist_name,
+ # Optional: integrator overrides (override config file, not env)
+ # local_scheme="no-local-version",
+ )
+
+ # 4. Infer version
+ version = infer_version_string(
+ dist_name=dist_name or pyproject.project_name,
+ pyproject_data=pyproject,
+ )
+
+ return version
+```
+
+### PyProjectData Composition
+
+Integrators can create `PyProjectData` in two ways:
+
+#### 1. From File (Recommended)
+
+```python
+from vcs_versioning import PyProjectData
+
+# Load from pyproject.toml (reads tool.vcs-versioning section)
+pyproject = PyProjectData.from_file("pyproject.toml")
+```
+
+#### 2. Manual Composition
+
+If your tool already has its own TOML reading logic:
+
+```python
+from pathlib import Path
+from vcs_versioning import PyProjectData
+
+pyproject = PyProjectData(
+ path=Path("pyproject.toml"),
+ tool_name="vcs-versioning",
+ project={"name": "my-pkg", "dynamic": ["version"]},
+ section={"local_scheme": "no-local-version"},
+ is_required=True,
+ section_present=True,
+ project_present=True,
+ build_requires=["vcs-versioning"],
+)
+```
+
+### Building Configuration with Overrides
+
+The `build_configuration_from_pyproject()` function orchestrates the complete configuration workflow:
+
+```python
+from vcs_versioning import build_configuration_from_pyproject
+
+config = build_configuration_from_pyproject(
+ pyproject_data=pyproject,
+ dist_name="my-package", # Optional: override project.name
+ # Integrator overrides (middle priority):
+ version_scheme="release-branch-semver",
+ local_scheme="no-local-version",
+)
+```
+
+**What it does:**
+1. Extracts config from `pyproject_data.section`
+2. Determines `dist_name` (argument > config > project.name)
+3. Merges integrator overrides (kwargs)
+4. Reads and applies environment TOML overrides
+5. Builds and validates `Configuration` instance
+
+### Environment TOML Overrides
+
+Users can override configuration via environment variables:
+
+```bash
+# Inline TOML format
+export MY_TOOL_OVERRIDES='{local_scheme = "no-local-version"}'
+
+# Distribution-specific
+export MY_TOOL_OVERRIDES_FOR_MY_PACKAGE='{version_scheme = "guess-next-dev"}'
+```
+
+These always have the highest priority, even over integrator overrides.
+
+### Complete Example: Hatch Integration
+
+```python
+# In your hatch plugin
+from pathlib import Path
+from vcs_versioning import (
+ PyProjectData,
+ build_configuration_from_pyproject,
+ infer_version_string,
+)
+from vcs_versioning.overrides import GlobalOverrides
+
+
+class HatchVCSVersion:
+ """Hatch version source plugin using vcs-versioning."""
+
+ def get_version_data(self):
+ """Get version from VCS."""
+ # Setup global context with HATCH_VCS prefix
+ with GlobalOverrides.from_env("HATCH_VCS", dist_name=self.config["dist-name"]):
+
+ # Load pyproject data
+ pyproject_path = Path(self.root) / "pyproject.toml"
+ pyproject = PyProjectData.from_file(pyproject_path)
+
+ # Build configuration
+ # Hatch-specific transformations can go here as kwargs
+ config = build_configuration_from_pyproject(
+ pyproject_data=pyproject,
+ dist_name=self.config["dist-name"],
+ root=self.root, # Hatch provides the root
+ )
+
+ # Get version
+ version = infer_version_string(
+ dist_name=self.config["dist-name"],
+ pyproject_data=pyproject,
+ )
+
+ return {"version": version}
+```
+
+### Tool Section Naming
+
+**Important:** The public experimental API only accepts `tool.vcs-versioning` sections.
+
+```toml
+# ✅ Correct - use tool.vcs-versioning
+[tool.vcs-versioning]
+version_scheme = "guess-next-dev"
+local_scheme = "no-local-version"
+
+# ❌ Wrong - tool.setuptools_scm not supported in public API
+[tool.setuptools_scm]
+version_scheme = "guess-next-dev"
+```
+
+Only `setuptools_scm` should use `tool.setuptools_scm` (for backward compatibility during transition).
+
+### API Reference
+
+#### `PyProjectData.from_file()`
+
+```python
+@classmethod
+def from_file(
+ cls,
+ path: str | os.PathLike = "pyproject.toml",
+ *,
+ _tool_names: list[str] | None = None,
+) -> PyProjectData:
+ """Load PyProjectData from pyproject.toml.
+
+ Public API reads tool.vcs-versioning section.
+ Internal: pass _tool_names for multi-tool support.
+ """
+```
+
+#### `build_configuration_from_pyproject()`
+
+```python
+def build_configuration_from_pyproject(
+ pyproject_data: PyProjectData,
+ *,
+ dist_name: str | None = None,
+ **integrator_overrides: Any,
+) -> Configuration:
+ """Build Configuration with full workflow orchestration.
+
+ Priority order:
+ 1. Environment TOML overrides (highest)
+ 2. Integrator **integrator_overrides
+ 3. pyproject_data.section configuration
+ 4. Configuration defaults (lowest)
+ """
+```
+
+### Migration from Direct API Usage
+
+If you were previously using internal APIs directly:
+
+**Before:**
+```python
+from vcs_versioning._config import Configuration
+
+config = Configuration.from_file("pyproject.toml", dist_name="my-pkg")
+```
+
+**After (Experimental API):**
+```python
+from vcs_versioning import PyProjectData, build_configuration_from_pyproject
+from vcs_versioning.overrides import GlobalOverrides
+
+with GlobalOverrides.from_env("MY_TOOL", dist_name="my-pkg"):
+ pyproject = PyProjectData.from_file("pyproject.toml")
+ config = build_configuration_from_pyproject(
+ pyproject_data=pyproject,
+ dist_name="my-pkg",
+ )
+```
+
+The experimental API provides better separation of concerns and proper override priority handling.
+
+## See Also
+
+- [Overrides Documentation](overrides.md) - User-facing documentation for setuptools-scm
+- [Configuration Guide](config.md) - Configuring vcs-versioning behavior
+- [Extending Guide](extending.md) - Creating custom version schemes and plugins
+
diff --git a/docs/overrides.md b/docs/overrides.md
index 4d136db2..72e57d3a 100644
--- a/docs/overrides.md
+++ b/docs/overrides.md
@@ -1,26 +1,58 @@
# Overrides
-## pretend versions
+!!! info "For Integrators"
-setuptools-scm provides a mechanism to override the version number build time.
+ If you're building a tool that integrates vcs-versioning (like hatch-vcs), see the [Integrator Guide](integrators.md) for using the overrides API with custom prefixes and the `GlobalOverrides` context manager.
-the environment variable `SETUPTOOLS_SCM_PRETEND_VERSION` is used
+## About Overrides
+
+Environment variables provide runtime configuration overrides, primarily useful in CI/CD
+environments where you need different behavior without modifying `pyproject.toml` or code.
+
+All environment variables support both `SETUPTOOLS_SCM_*` and `VCS_VERSIONING_*` prefixes. The `VCS_VERSIONING_*` prefix serves as a universal fallback that works across all tools using vcs-versioning.
+
+## Version Detection Overrides
+
+### Pretend Versions
+
+Override the version number at build time.
+
+**setuptools-scm usage:**
+
+The environment variable `SETUPTOOLS_SCM_PRETEND_VERSION` (or `VCS_VERSIONING_PRETEND_VERSION`) is used
as the override source for the version number unparsed string.
-to be specific about the package this applies for, one can use `SETUPTOOLS_SCM_PRETEND_VERSION_FOR_${DIST_NAME}`
-where the dist name normalization follows adapted PEP 503 semantics.
+!!! warning ""
-## pretend metadata
+ it is strongly recommended to use distribution-specific pretend versions
+ (see below).
-setuptools-scm provides a mechanism to override individual version metadata fields at build time.
+`SETUPTOOLS_SCM_PRETEND_VERSION_FOR_${DIST_NAME}` or `VCS_VERSIONING_PRETEND_VERSION_FOR_${DIST_NAME}`
+: Used as the primary source for the version number,
+ in which case it will be an unparsed string.
+ Specifying distribution-specific pretend versions will
+ avoid possible collisions with third party distributions
+ also using vcs-versioning.
-The environment variable `SETUPTOOLS_SCM_PRETEND_METADATA` accepts a TOML inline table
-with field overrides for the ScmVersion object.
+ The dist name normalization follows adapted PEP 503 semantics, with one or
+ more of ".-\_" being replaced by a single "\_", and the name being upper-cased.
-To be specific about the package this applies for, one can use `SETUPTOOLS_SCM_PRETEND_METADATA_FOR_${DIST_NAME}`
-where the dist name normalization follows adapted PEP 503 semantics.
+ This will take precedence over the generic ``SETUPTOOLS_SCM_PRETEND_VERSION`` or ``VCS_VERSIONING_PRETEND_VERSION``.
-### Supported fields
+### Pretend Metadata
+
+Override individual version metadata fields at build time.
+
+**setuptools-scm usage:**
+
+`SETUPTOOLS_SCM_PRETEND_METADATA`
+: Accepts a TOML inline table with field overrides for the ScmVersion object.
+
+`SETUPTOOLS_SCM_PRETEND_METADATA_FOR_${DIST_NAME}`
+: Same as above but specific to a package (recommended over the generic version).
+ The dist name normalization follows adapted PEP 503 semantics.
+
+#### Supported fields
The following ScmVersion fields can be overridden:
@@ -33,7 +65,7 @@ The following ScmVersion fields can be overridden:
- `preformatted` (bool): Whether the version string is preformatted
- `tag`: The version tag (can be string or version object)
-### Examples
+#### Examples
Override commit hash and distance:
```bash
@@ -59,7 +91,7 @@ export SETUPTOOLS_SCM_PRETEND_METADATA_FOR_MY_PACKAGE='{node="g1234567", distanc
This ensures consistency with setuptools-scm's automatic node ID formatting.
-### Use case: CI/CD environments
+#### Use case: CI/CD environments
This is particularly useful for solving issues where version file templates need access to
commit metadata that may not be available in certain build environments:
@@ -80,14 +112,65 @@ export SETUPTOOLS_SCM_PRETEND_VERSION="1.2.3.dev4+g1337beef"
export SETUPTOOLS_SCM_PRETEND_METADATA='{node="g1337beef", distance=4}'
```
-## config overrides
+### Debug Logging
+
+Enable debug output from vcs-versioning.
+
+**setuptools-scm usage:**
+
+`SETUPTOOLS_SCM_DEBUG` or `VCS_VERSIONING_DEBUG`
+: Enable debug logging for version detection and processing.
+ Can be set to:
+ - `1` or any non-empty value to enable DEBUG level logging
+ - A level name: `DEBUG`, `INFO`, `WARNING`, `ERROR`, or `CRITICAL` (case-insensitive)
+ - A specific log level integer: `10` (DEBUG), `20` (INFO), `30` (WARNING), etc.
+ - `0` to disable debug logging
+
+### Reproducible Builds
+
+Control timestamps for reproducible builds (from [reproducible-builds.org](https://reproducible-builds.org/docs/source-date-epoch/)).
+
+`SOURCE_DATE_EPOCH`
+: Used as the timestamp from which the ``node-and-date`` and ``node-and-timestamp``
+ local parts are derived, otherwise the current time is used.
+ This is a standard environment variable supported by many build tools.
+
+## setuptools-scm Overrides
+
+### Configuration Overrides
+
+`SETUPTOOLS_SCM_OVERRIDES_FOR_${DIST_NAME}`
+: A TOML inline table to override configuration from `pyproject.toml`.
+ This allows overriding any configuration option at build time, which is particularly useful
+ in CI/CD environments where you might want different behavior without modifying `pyproject.toml`.
+
+ **Example:**
+ ```bash
+ # Override local_scheme for CI builds
+ export SETUPTOOLS_SCM_OVERRIDES_FOR_MYPACKAGE='{"local_scheme": "no-local-version"}'
+ ```
+
+### SCM Root Discovery
+
+`SETUPTOOLS_SCM_IGNORE_VCS_ROOTS`
+: A ``os.pathsep`` separated list of directory names to ignore for root finding.
+
+### Mercurial Command
+
+`SETUPTOOLS_SCM_HG_COMMAND`
+: Command used for running Mercurial (defaults to ``hg``).
+ For example, set this to ``chg`` to reduce start-up overhead of Mercurial.
-setuptools-scm parses the environment variable `SETUPTOOLS_SCM_OVERRIDES_FOR_${DIST_NAME}`
-as a toml inline map to override the configuration data from `pyproject.toml`.
+### Subprocess Timeouts
-## subprocess timeouts
+`SETUPTOOLS_SCM_SUBPROCESS_TIMEOUT`
+: Override the subprocess timeout (default: 40 seconds).
+ The default should work for most needs. However, users with git lfs + windows reported
+ situations where this was not enough.
-The environment variable `SETUPTOOLS_SCM_SUBPROCESS_TIMEOUT` allows to override the subprocess timeout.
-The default is 40 seconds and should work for most needs. However, users with git lfs + windows reported
-situations where this was not enough.
+ **Example:**
+ ```bash
+ # Increase timeout to 120 seconds
+ export SETUPTOOLS_SCM_SUBPROCESS_TIMEOUT=120
+ ```
diff --git a/docs/usage.md b/docs/usage.md
index 53f70445..efa65169 100644
--- a/docs/usage.md
+++ b/docs/usage.md
@@ -139,6 +139,9 @@ $ python -m setuptools_scm --help
## At runtime
+!!! tip "Recommended Approach"
+ Use standard Python metadata (`importlib.metadata`) for runtime version access. This is the standard, recommended approach that works with any packaging system.
+
### Python Metadata
The standard method to retrieve the version number at runtime is via
@@ -174,6 +177,9 @@ print(v.version_tuple)
### Via setuptools_scm (strongly discouraged)
+!!! warning "Discouraged API"
+ Direct use of `setuptools_scm.get_version()` at runtime is strongly discouraged. Use `importlib.metadata` instead.
+
While the most simple **looking** way to use `setuptools_scm` at
runtime is:
diff --git a/mkdocs.yml b/mkdocs.yml
index 5b1f42ef..477a5e03 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -8,12 +8,14 @@ nav:
- integrations.md
- extending.md
- overrides.md
+ - integrators.md
- changelog.md
theme:
name: material
watch:
-- src/setuptools_scm
+- setuptools-scm/src/setuptools_scm
+- vcs-versioning/src/vcs_versioning
- docs
markdown_extensions:
- def_list
@@ -31,10 +33,13 @@ plugins:
default_handler: python
handlers:
python:
- paths: [ src ]
-
+ paths: [ setuptools-scm/src, vcs-versioning/src ]
+ import:
+ - https://docs.python.org/3/objects.inv
options:
separate_signature: true
show_signature_annotations: true
allow_inspection: true
show_root_heading: true
+ extensions:
+ - griffe_public_wildcard_imports
diff --git a/mypy.ini b/mypy.ini
deleted file mode 100644
index ef383f74..00000000
--- a/mypy.ini
+++ /dev/null
@@ -1,6 +0,0 @@
-[mypy]
-python_version = 3.8
-warn_return_any = True
-warn_unused_configs = True
-mypy_path = $MYPY_CONFIG_FILE_DIR/src
-strict = true
diff --git a/nextgen/vcs-versioning/pyproject.toml b/nextgen/vcs-versioning/pyproject.toml
deleted file mode 100644
index c34c5dd1..00000000
--- a/nextgen/vcs-versioning/pyproject.toml
+++ /dev/null
@@ -1,66 +0,0 @@
-[build-system]
-build-backend = "hatchling.build"
-requires = [
- "hatchling",
-]
-
-[project]
-name = "vcs-versioning"
-description = "the blessed package to manage your versions by vcs metadata"
-readme = "README.md"
-keywords = [
-]
-license = "MIT"
-authors = [
- { name = "Ronny Pfannschmidt", email = "opensource@ronnypfannschmidt.de" },
-]
-requires-python = ">=3.8"
-classifiers = [
- "Development Status :: 1 - Planning",
- "Programming Language :: Python",
- "Programming Language :: Python :: 3 :: Only",
- "Programming Language :: Python :: 3.8",
- "Programming Language :: Python :: 3.9",
- "Programming Language :: Python :: 3.10",
- "Programming Language :: Python :: 3.11",
- "Programming Language :: Python :: 3.12",
- "Programming Language :: Python :: 3.13",
-]
-dynamic = [
- "version",
-]
-dependencies = [
-]
-[project.urls]
-Documentation = "https://github.com/unknown/vcs-versioning#readme"
-Issues = "https://github.com/unknown/vcs-versioning/issues"
-Source = "https://github.com/unknown/vcs-versioning"
-
-[tool.hatch.version]
-path = "vcs_versioning/__about__.py"
-
-[tool.hatch.envs.default]
-dependencies = [
- "pytest",
- "pytest-cov",
-]
-[tool.hatch.envs.default.scripts]
-cov = "pytest --cov-report=term-missing --cov-config=pyproject.toml --cov=vcs_versioning --cov=tests {args}"
-no-cov = "cov --no-cov {args}"
-
-[[tool.hatch.envs.test.matrix]]
-python = [ "38", "39", "310", "311", "312", "313" ]
-
-[tool.coverage.run]
-branch = true
-parallel = true
-omit = [
- "vcs_versioning/__about__.py",
-]
-
-[tool.coverage.report]
-exclude_lines = [
- "no cov",
- "if __name__ == .__main__.:",
- "if TYPE_CHECKING:",
-]
diff --git a/nextgen/vcs-versioning/tests/__init__.py b/nextgen/vcs-versioning/tests/__init__.py
deleted file mode 100644
index 9d48db4f..00000000
--- a/nextgen/vcs-versioning/tests/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-from __future__ import annotations
diff --git a/nextgen/vcs-versioning/vcs_versioning/__init__.py b/nextgen/vcs-versioning/vcs_versioning/__init__.py
deleted file mode 100644
index 9d48db4f..00000000
--- a/nextgen/vcs-versioning/vcs_versioning/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-from __future__ import annotations
diff --git a/pyproject.toml b/pyproject.toml
index 4fdd0568..f392d66b 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,175 +1,99 @@
-
-
-[build-system]
-build-backend = "_own_version_helper:build_meta"
-requires = [
- "setuptools>=61",
- 'tomli<=2.0.2; python_version < "3.11"',
-]
-backend-path = [
- ".",
- "src",
-]
+# uv sync --all-packages --all-groups # Install all packages with all dependency groups
[project]
-name = "setuptools-scm"
-description = "the blessed package to manage your versions by scm tags"
-readme = "README.md"
-license.file = "LICENSE"
-authors = [
- {name="Ronny Pfannschmidt", email="opensource@ronnypfannschmidt.de"}
-]
-requires-python = ">=3.8"
-classifiers = [
- "Development Status :: 5 - Production/Stable",
- "Intended Audience :: Developers",
- "License :: OSI Approved :: MIT License",
- "Programming Language :: Python",
- "Programming Language :: Python :: 3 :: Only",
- "Programming Language :: Python :: 3.8",
- "Programming Language :: Python :: 3.9",
- "Programming Language :: Python :: 3.10",
- "Programming Language :: Python :: 3.11",
- "Programming Language :: Python :: 3.12",
- "Programming Language :: Python :: 3.13",
- "Topic :: Software Development :: Libraries",
- "Topic :: Software Development :: Version Control",
- "Topic :: System :: Software Distribution",
- "Topic :: Utilities",
-]
-dynamic = [
- "version",
-]
+name = "vcs-versioning.workspace"
+description = "workspace configuration for this monorepo"
+version = "0.1+private"
+requires-python = ">=3.10"
dependencies = [
- "packaging>=20",
- # https://github.com/pypa/setuptools-scm/issues/1112 - re-pin in a breaking release
- "setuptools", # >= 61",
- 'tomli>=1; python_version < "3.11"',
- 'typing-extensions; python_version < "3.10"',
+ "vcs-versioning",
+ "setuptools-scm",
+
]
-[project.optional-dependencies]
-rich = ["rich"]
-simple = []
-toml = []
+
+[project.scripts]
+create-release-proposal = "vcs_versioning_workspace.create_release_proposal:main"
[dependency-groups]
docs = [
- #"entangled-cli~=2.0",
"mkdocs",
"mkdocs-entangled-plugin",
"mkdocs-include-markdown-plugin",
"mkdocs-material",
"mkdocstrings[python]",
"pygments",
+ "griffe-public-wildcard-imports",
]
-test = [
- "pip",
- "build",
- "pytest",
- "pytest-timeout", # Timeout protection for CI/CD
- "pytest-xdist",
- "rich",
- "ruff",
- "mypy~=1.13.0", # pinned to old for python 3.8
- 'typing-extensions; python_version < "3.11"',
- "wheel",
- "griffe",
- "flake8",
+typing = [
+ "types-setuptools",
+]
+release = [
+ "towncrier>=23.11.0",
+ "PyGithub>=2.0.0; sys_platform != 'win32'",
]
-[project.urls]
-documentation = "https://setuptools-scm.readthedocs.io/"
-repository = "https://github.com/pypa/setuptools-scm/"
-
-[project.entry-points.console_scripts]
-setuptools-scm = "setuptools_scm._cli:main"
-
-[project.entry-points."distutils.setup_keywords"]
-use_scm_version = "setuptools_scm._integration.setuptools:version_keyword"
-
-[project.entry-points."pipx.run"]
-setuptools-scm = "setuptools_scm._cli:main"
-setuptools_scm = "setuptools_scm._cli:main"
-
-[project.entry-points."setuptools.file_finders"]
-setuptools_scm = "setuptools_scm._file_finders:find_files"
-
-[project.entry-points."setuptools.finalize_distribution_options"]
-setuptools_scm = "setuptools_scm._integration.setuptools:infer_version"
-
-[project.entry-points."setuptools_scm.files_command"]
-".git" = "setuptools_scm._file_finders.git:git_find_files"
-".hg" = "setuptools_scm._file_finders.hg:hg_find_files"
-
-[project.entry-points."setuptools_scm.files_command_fallback"]
-".git_archival.txt" = "setuptools_scm._file_finders.git:git_archive_find_files"
-".hg_archival.txt" = "setuptools_scm._file_finders.hg:hg_archive_find_files"
-
-[project.entry-points."setuptools_scm.local_scheme"]
-dirty-tag = "setuptools_scm.version:get_local_dirty_tag"
-no-local-version = "setuptools_scm.version:get_no_local_node"
-node-and-date = "setuptools_scm.version:get_local_node_and_date"
-node-and-timestamp = "setuptools_scm.version:get_local_node_and_timestamp"
-[project.entry-points."setuptools_scm.parse_scm"]
-".git" = "setuptools_scm.git:parse"
-".hg" = "setuptools_scm.hg:parse"
+[tool.pytest.ini_options]
+minversion = "8.2"
+testpaths = ["setuptools-scm/testing_scm", "vcs-versioning/testing_vcs"]
+xfail_strict = true
+addopts = ["-ra", "--strict-config", "--strict-markers", "-p", "vcs_versioning.test_api"]
+markers = [
+ "issue(id): reference to github issue",
+ "skip_commit: allows to skip committing in the helpers",
+]
-[project.entry-points."setuptools_scm.parse_scm_fallback"]
-".git_archival.txt" = "setuptools_scm.git:parse_archival"
-".hg_archival.txt" = "setuptools_scm.hg:parse_archival"
-PKG-INFO = "setuptools_scm.fallbacks:parse_pkginfo"
-"pyproject.toml" = "setuptools_scm.fallbacks:fallback_version"
-"setup.py" = "setuptools_scm.fallbacks:fallback_version"
+[tool.mypy]
-[project.entry-points."setuptools_scm.version_scheme"]
-"calver-by-date" = "setuptools_scm.version:calver_by_date"
-"guess-next-dev" = "setuptools_scm.version:guess_next_dev_version"
-"no-guess-dev" = "setuptools_scm.version:no_guess_dev_version"
-"only-version" = "setuptools_scm.version:only_version"
-"post-release" = "setuptools_scm.version:postrelease_version"
-"python-simplified-semver" = "setuptools_scm.version:simplified_semver_version"
-"release-branch-semver" = "setuptools_scm.version:release_branch_semver_version"
+enable_error_code = [
+ "ignore-without-code",
+ "redundant-expr",
+ "truthy-bool",
+]
+strict = true
+warn_unused_configs = true
+warn_redundant_casts = true
+warn_unused_ignores = true
+warn_return_any = true
+warn_unreachable = true
+disallow_untyped_defs = true
+disallow_any_generics = true
+check_untyped_defs = true
+no_implicit_reexport = true
+
+# Workspace-specific paths
+mypy_path = "vcs-versioning/src:setuptools-scm/src"
+explicit_package_bases = true
+
+[[tool.mypy.overrides]]
+module = [
+ "vcs-versioning._own_version_of_vcs_versioning",
+ "setuptools-scm._own_version_helper",
+]
+# Version helper files use imports before they're installed
+ignore_errors = true
-[tool.setuptools.packages.find]
-where = ["src"]
-namespaces = false
+[tool.uv]
+package = true
-[tool.setuptools.dynamic]
-version = { attr = "_own_version_helper.version"}
+[tool.uv.workspace]
+members = ["vcs-versioning", "setuptools-scm"]
-[tool.setuptools_scm]
+[tool.uv.sources]
+vcs-versioning = { workspace = true }
+setuptools-scm = { workspace = true }
[tool.ruff]
-lint.extend-select = ["YTT", "B", "C4", "DTZ", "ISC", "LOG", "G", "PIE", "PYI", "PT", "FLY", "I", "C90", "PERF", "W", "PGH", "PLE", "UP", "FURB", "RUF"]
-lint.ignore = ["B028", "LOG015", "PERF203"]
-lint.preview = true
-
-[tool.ruff.lint.isort]
-force-single-line = true
-from-first = false
-lines-between-types = 1
-order-by-type = true
-[tool.repo-review]
-ignore = ["PP305", "GH103", "GH212", "MY100", "PC111", "PC160", "PC170", "PC180", "PC901"]
+lint.extend-select = ["UP", "I", "B"]
-[tool.pytest.ini_options]
-minversion = "8"
-testpaths = ["testing"]
-timeout = 300 # 5 minutes timeout per test for CI protection
-filterwarnings = [
- "error",
- "ignore:.*tool\\.setuptools_scm.*",
- "ignore:.*git archive did not support describe output.*:UserWarning",
-]
-log_level = "debug"
-log_cli_level = "info"
-# disable unraisable until investigated
-addopts = ["-ra", "--strict-config", "--strict-markers"]
-markers = [
- "issue(id): reference to github issue",
- "skip_commit: allows to skip committing in the helpers",
-]
-
-[tool.uv]
-default-groups = ["test", "docs"]
+[tool.repo-review]
+ignore = [
+ "PP002", # this is a uv workspace
+ "PP003", # this tool is drunk
+ "PP304", # log_cli shoudltn be the default
+ "PP309", # filterwarnings is evil unless needed, it should never be error
+ "PY007", # no to tasks
+ "PY005", # no tests folder for the workspace
+ "PY003", # each subproject has one
+
+ "GH103", "GH212", "MY100", "PC111", "PC160", "PC170", "PC180", "PC901"]
\ No newline at end of file
diff --git a/CHANGELOG.md b/setuptools-scm/CHANGELOG.md
similarity index 98%
rename from CHANGELOG.md
rename to setuptools-scm/CHANGELOG.md
index a8d8964f..f0c0a3bc 100644
--- a/CHANGELOG.md
+++ b/setuptools-scm/CHANGELOG.md
@@ -1,5 +1,16 @@
# Changelog
+
+
+## v9.2.2
+
+### Fixed
+
+- fix #1231: don't warn about `tool.setuptools.dynamic.version` when only using file finder.
+ The warning about combining version guessing with setuptools dynamic versions should only
+ be issued when setuptools-scm is performing version inference, not when it's only being
+ used for its file finder functionality.
+
## v9.2.1
diff --git a/LICENSE b/setuptools-scm/LICENSE
similarity index 100%
rename from LICENSE
rename to setuptools-scm/LICENSE
diff --git a/MANIFEST.in b/setuptools-scm/MANIFEST.in
similarity index 52%
rename from MANIFEST.in
rename to setuptools-scm/MANIFEST.in
index b793e6c0..5eb47594 100644
--- a/MANIFEST.in
+++ b/setuptools-scm/MANIFEST.in
@@ -4,24 +4,16 @@ exclude changelog.d/*
exclude .git_archival.txt
exclude .readthedocs.yaml
include *.py
-include testing/*.py
+include testing_scm/*.py
include tox.ini
include *.rst
include LICENSE
include *.toml
include mypy.ini
-include testing/Dockerfile.*
+include testing_scm/Dockerfile.*
include src/setuptools_scm/.git_archival.txt
include README.md
include CHANGELOG.md
-
-recursive-include testing *.bash
-prune nextgen
-prune .cursor
-
-recursive-include docs *.md
-include docs/examples/version_scheme_code/*.py
-include docs/examples/version_scheme_code/*.toml
-include mkdocs.yml
-include uv.lock
\ No newline at end of file
+recursive-include testing_scm *.bash
+prune .cursor
\ No newline at end of file
diff --git a/setuptools-scm/README.md b/setuptools-scm/README.md
new file mode 100644
index 00000000..f4ca4bf9
--- /dev/null
+++ b/setuptools-scm/README.md
@@ -0,0 +1,132 @@
+# setuptools-scm
+[](https://github.com/pypa/setuptools-scm/actions/workflows/python-tests.yml)
+[](https://setuptools-scm.readthedocs.io/en/latest/?badge=latest)
+[ ](https://tidelift.com/subscription/pkg/pypi-setuptools-scm?utm_source=pypi-setuptools-scm&utm_medium=readme)
+
+## about
+
+[setuptools-scm] extracts Python package versions from `git` or `hg` metadata
+instead of declaring them as the version argument
+or in a Source Code Managed (SCM) managed file.
+
+Additionally [setuptools-scm] provides `setuptools` with a list of
+files that are managed by the SCM
+
+(i.e. it automatically adds all the SCM-managed files to the sdist).
+
+Unwanted files must be excluded via `MANIFEST.in`
+or [configuring Git archive][git-archive-docs].
+
+> **⚠️ Important:** Installing setuptools-scm automatically enables a file finder that includes **all SCM-tracked files** in your source distributions. This can be surprising if you have development files tracked in Git/Mercurial that you don't want in your package. Use `MANIFEST.in` to exclude unwanted files. See the [documentation] for details.
+
+## `pyproject.toml` usage
+
+The preferred way to configure [setuptools-scm] is to author
+settings in a `tool.setuptools_scm` section of `pyproject.toml`.
+
+This feature requires setuptools 61 or later (recommended: >=80 for best compatibility).
+First, ensure that [setuptools-scm] is present during the project's
+build step by specifying it as one of the build requirements.
+
+```toml title="pyproject.toml"
+[build-system]
+requires = ["setuptools>=80", "setuptools-scm>=8"]
+build-backend = "setuptools.build_meta"
+```
+
+That will be sufficient to require [setuptools-scm] for projects
+that support [PEP 518] like [pip] and [build].
+
+[pip]: https://pypi.org/project/pip
+[build]: https://pypi.org/project/build
+[PEP 518]: https://peps.python.org/pep-0518/
+
+
+To enable version inference, you need to set the version
+dynamically in the `project` section of `pyproject.toml`:
+
+```toml title="pyproject.toml"
+[project]
+# version = "0.0.1" # Remove any existing version parameter.
+dynamic = ["version"]
+
+[tool.setuptools_scm]
+```
+
+!!! note "Simplified Configuration"
+
+ Starting with setuptools-scm 8.1+, if `setuptools_scm` (or `setuptools-scm`) is
+ present in your `build-system.requires`, the `[tool.setuptools_scm]` section
+ becomes optional! You can now enable setuptools-scm with just:
+
+ ```toml title="pyproject.toml"
+ [build-system]
+ requires = ["setuptools>=80", "setuptools-scm>=8"]
+ build-backend = "setuptools.build_meta"
+
+ [project]
+ dynamic = ["version"]
+ ```
+
+ The `[tool.setuptools_scm]` section is only needed if you want to customize
+ configuration options.
+
+Additionally, a version file can be written by specifying:
+
+```toml title="pyproject.toml"
+[tool.setuptools_scm]
+version_file = "pkg/_version.py"
+```
+
+Where `pkg` is the name of your package.
+
+If you need to confirm which version string is being generated or debug the configuration,
+you can install [setuptools-scm] directly in your working environment and run:
+
+```console
+$ python -m setuptools_scm
+# To explore other options, try:
+$ python -m setuptools_scm --help
+```
+
+For further configuration see the [documentation].
+
+[setuptools-scm]: https://github.com/pypa/setuptools-scm
+[documentation]: https://setuptools-scm.readthedocs.io/
+[git-archive-docs]: https://setuptools-scm.readthedocs.io/en/stable/usage/#builtin-mechanisms-for-obtaining-version-numbers
+
+
+## Interaction with Enterprise Distributions
+
+Some enterprise distributions like RHEL7
+ship rather old setuptools versions.
+
+In those cases its typically possible to build by using an sdist against `setuptools-scm<2.0`.
+As those old setuptools versions lack sensible types for versions,
+modern [setuptools-scm] is unable to support them sensibly.
+
+It's strongly recommended to build a wheel artifact using modern Python and setuptools,
+then installing the artifact instead of trying to run against old setuptools versions.
+
+!!! note "Legacy Setuptools Support"
+ While setuptools-scm recommends setuptools >=80, it maintains compatibility with setuptools 61+
+ to support legacy deployments that cannot easily upgrade. Support for setuptools <80 is deprecated
+ and will be removed in a future release. This allows enterprise environments and older CI/CD systems
+ to continue using setuptools-scm while still encouraging adoption of newer versions.
+
+
+## Code of Conduct
+
+
+Everyone interacting in the [setuptools-scm] project's codebases, issue
+trackers, chat rooms, and mailing lists is expected to follow the
+[PSF Code of Conduct].
+
+[PSF Code of Conduct]: https://github.com/pypa/.github/blob/main/CODE_OF_CONDUCT.md
+
+
+## Security Contact
+
+To report a security vulnerability, please use the
+[Tidelift security contact](https://tidelift.com/security).
+Tidelift will coordinate the fix and disclosure.
diff --git a/setuptools-scm/_own_version_helper.py b/setuptools-scm/_own_version_helper.py
new file mode 100644
index 00000000..5274b10d
--- /dev/null
+++ b/setuptools-scm/_own_version_helper.py
@@ -0,0 +1,44 @@
+"""
+Version helper for setuptools-scm package.
+
+This module allows setuptools-scm to use VCS metadata for its own version.
+It works only if the backend-path of the build-system section from
+pyproject.toml is respected.
+
+Tag prefix configuration:
+- Currently: No prefix (for backward compatibility with existing tags)
+- Future: Will migrate to 'setuptools-scm-' prefix
+"""
+
+from __future__ import annotations
+
+import os
+
+from setuptools import build_meta as build_meta
+
+from setuptools_scm import get_version
+
+
+def scm_version() -> str:
+ # Use no-local-version if SETUPTOOLS_SCM_NO_LOCAL is set (for CI uploads)
+ local_scheme = (
+ "no-local-version"
+ if os.environ.get("SETUPTOOLS_SCM_NO_LOCAL")
+ else "node-and-date"
+ )
+
+ # Note: tag_regex is currently NOT set to allow backward compatibility
+ # with existing tags. To migrate to 'setuptools-scm-' prefix, uncomment:
+ # tag_regex=r"^setuptools-scm-(?P[vV]?\d+(?:\.\d+){0,2}[^\+]*)(?:\+.*)?$",
+
+ # Use relative_to parent to find git root (one level up from setuptools-scm/)
+ import pathlib
+
+ return get_version(
+ root=pathlib.Path(__file__).parent.parent,
+ version_scheme="guess-next-dev",
+ local_scheme=local_scheme,
+ )
+
+
+version: str = scm_version()
diff --git a/setuptools-scm/changelog.d/.gitkeep b/setuptools-scm/changelog.d/.gitkeep
new file mode 100644
index 00000000..c199cfd6
--- /dev/null
+++ b/setuptools-scm/changelog.d/.gitkeep
@@ -0,0 +1,8 @@
+# Changelog fragments directory
+# Add your changelog fragments here following the naming convention:
+# {issue_number}.{type}.md
+#
+# Where type is one of: feature, bugfix, deprecation, removal, doc, misc
+#
+# Example: 123.feature.md
+
diff --git a/setuptools-scm/changelog.d/1231.bugfix.md b/setuptools-scm/changelog.d/1231.bugfix.md
new file mode 100644
index 00000000..027e5c46
--- /dev/null
+++ b/setuptools-scm/changelog.d/1231.bugfix.md
@@ -0,0 +1,2 @@
+Fix issue #1231: Don't warn about tool.setuptools.dynamic.version conflict when only using file finder without version inference.
+
diff --git a/setuptools-scm/changelog.d/README.md b/setuptools-scm/changelog.d/README.md
new file mode 100644
index 00000000..3ea52129
--- /dev/null
+++ b/setuptools-scm/changelog.d/README.md
@@ -0,0 +1,32 @@
+# Changelog Fragments
+
+This directory contains changelog fragments that will be assembled into the CHANGELOG.md file during release.
+
+## Fragment Types
+
+- **feature**: New features or enhancements
+- **bugfix**: Bug fixes
+- **deprecation**: Deprecation warnings
+- **removal**: Removed features (breaking changes)
+- **doc**: Documentation improvements
+- **misc**: Internal changes, refactoring, etc.
+
+## Naming Convention
+
+Fragments should be named: `{issue_number}.{type}.md`
+
+Examples:
+- `123.feature.md` - New feature related to issue #123
+- `456.bugfix.md` - Bug fix for issue #456
+- `789.doc.md` - Documentation update for issue #789
+
+## Content
+
+Each fragment should contain a brief description of the change:
+
+```markdown
+Add support for custom version schemes via plugin system
+```
+
+Do not include issue numbers in the content - they will be added automatically.
+
diff --git a/setuptools-scm/changelog.d/internal-refactor.misc.md b/setuptools-scm/changelog.d/internal-refactor.misc.md
new file mode 100644
index 00000000..82dd5976
--- /dev/null
+++ b/setuptools-scm/changelog.d/internal-refactor.misc.md
@@ -0,0 +1,2 @@
+Internal refactoring: modernized type annotations, improved CLI type safety, and enhanced release automation infrastructure.
+
diff --git a/setuptools-scm/changelog.d/py310.removal.md b/setuptools-scm/changelog.d/py310.removal.md
new file mode 100644
index 00000000..930517bd
--- /dev/null
+++ b/setuptools-scm/changelog.d/py310.removal.md
@@ -0,0 +1,2 @@
+Drop Python 3.8 and 3.9 support. Minimum Python version is now 3.10.
+
diff --git a/setuptools-scm/changelog.d/should-infer-function.misc.md b/setuptools-scm/changelog.d/should-infer-function.misc.md
new file mode 100644
index 00000000..c6c0b9cb
--- /dev/null
+++ b/setuptools-scm/changelog.d/should-infer-function.misc.md
@@ -0,0 +1,2 @@
+Refactored should_infer from method to standalone function for better code organization.
+
diff --git a/setuptools-scm/changelog.d/template.md b/setuptools-scm/changelog.d/template.md
new file mode 100644
index 00000000..41a46689
--- /dev/null
+++ b/setuptools-scm/changelog.d/template.md
@@ -0,0 +1,21 @@
+{% for section, _ in sections.items() %}
+{% set underline = underlines[0] %}{% if section %}{{section}}
+{{ underline * section|length }}{% set underline = underlines[1] %}
+
+{% endif %}
+{% if sections[section] %}
+{% for category, val in definitions.items() if category in sections[section]%}
+
+### {{ definitions[category]['name'] }}
+
+{% for text, values in sections[section][category].items() %}
+- {{ text }} ({{ values|join(', ') }})
+{% endfor %}
+
+{% endfor %}
+{% else %}
+No significant changes.
+
+{% endif %}
+{% endfor %}
+
diff --git a/setuptools-scm/changelog.d/test-mypy.misc.md b/setuptools-scm/changelog.d/test-mypy.misc.md
new file mode 100644
index 00000000..ed5eb9ed
--- /dev/null
+++ b/setuptools-scm/changelog.d/test-mypy.misc.md
@@ -0,0 +1,2 @@
+Updated mypy version template test to use uvx with mypy 1.11.2 for Python 3.8 compatibility checking.
+
diff --git a/setuptools-scm/changelog.d/test-parametrize.misc.md b/setuptools-scm/changelog.d/test-parametrize.misc.md
new file mode 100644
index 00000000..8bc5b570
--- /dev/null
+++ b/setuptools-scm/changelog.d/test-parametrize.misc.md
@@ -0,0 +1,2 @@
+Refactored TestBuildPackageWithExtra into parametrized function with custom INI-based decorator for cleaner test data specification.
+
diff --git a/setuptools-scm/changelog.d/vcs-versioning-dep.feature.md b/setuptools-scm/changelog.d/vcs-versioning-dep.feature.md
new file mode 100644
index 00000000..39c03e39
--- /dev/null
+++ b/setuptools-scm/changelog.d/vcs-versioning-dep.feature.md
@@ -0,0 +1,2 @@
+setuptools-scm now depends on vcs-versioning for core version inference logic. This enables other build backends to use the same version inference without setuptools dependency.
+
diff --git a/setuptools-scm/check_api.py b/setuptools-scm/check_api.py
new file mode 100755
index 00000000..84c041f0
--- /dev/null
+++ b/setuptools-scm/check_api.py
@@ -0,0 +1,49 @@
+#!/usr/bin/env python3
+"""
+Local script to check API stability using griffe.
+
+Usage:
+ uv run check_api.py [--against TAG]
+
+This script runs griffe with the public-wildcard-imports extension enabled
+to properly detect re-exported symbols from vcs-versioning.
+"""
+
+from __future__ import annotations
+
+import subprocess
+import sys
+
+from pathlib import Path
+
+
+def main() -> int:
+ """Run griffe API check with proper configuration."""
+ # Parse arguments
+ against = "v9.2.1" # Default baseline
+ if len(sys.argv) > 1:
+ if sys.argv[1] == "--against" and len(sys.argv) > 2:
+ against = sys.argv[2]
+ else:
+ against = sys.argv[1]
+
+ # Ensure we're in the right directory
+ repo_root = Path(__file__).parent.parent
+
+ # Build griffe command
+ cmd = [
+ *("griffe", "check", "--verbose", "setuptools_scm"),
+ "-ssrc",
+ "-ssetuptools-scm/src",
+ "-svcs-versioning/src",
+ *("--extensions", "griffe_public_wildcard_imports"),
+ *("--against", against),
+ ]
+
+ result = subprocess.run(cmd, cwd=repo_root)
+
+ return result.returncode
+
+
+if __name__ == "__main__":
+ sys.exit(main())
diff --git a/hatch.toml b/setuptools-scm/hatch.toml
similarity index 100%
rename from hatch.toml
rename to setuptools-scm/hatch.toml
diff --git a/setuptools-scm/mypy.ini b/setuptools-scm/mypy.ini
new file mode 100644
index 00000000..f37b10e1
--- /dev/null
+++ b/setuptools-scm/mypy.ini
@@ -0,0 +1,6 @@
+[mypy]
+python_version = 3.10
+warn_return_any = True
+warn_unused_configs = True
+mypy_path = $MYPY_CONFIG_FILE_DIR/src:$MYPY_CONFIG_FILE_DIR/nextgen/vcs-versioning/src
+strict = true
diff --git a/setuptools-scm/pyproject.toml b/setuptools-scm/pyproject.toml
new file mode 100644
index 00000000..440e4588
--- /dev/null
+++ b/setuptools-scm/pyproject.toml
@@ -0,0 +1,199 @@
+
+
+[build-system]
+build-backend = "_own_version_helper:build_meta"
+requires = [
+ "setuptools>=77.0.3",
+ "vcs-versioning>=0.1.1",
+ 'tomli<=2.0.2; python_version < "3.11"',
+ 'typing-extensions; python_version < "3.11"',
+]
+backend-path = [
+ ".",
+ "./src",
+ "../vcs-versioning/src"
+]
+[tools.uv.sources]
+vcs-versioning= "../vcs-versioning"
+[project]
+name = "setuptools-scm"
+description = "the blessed package to manage your versions by scm tags"
+readme = "README.md"
+license = "MIT"
+authors = [
+ {name="Ronny Pfannschmidt", email="opensource@ronnypfannschmidt.de"}
+]
+requires-python = ">=3.10"
+classifiers = [
+ "Development Status :: 5 - Production/Stable",
+ "Intended Audience :: Developers",
+ "Programming Language :: Python",
+ "Programming Language :: Python :: 3 :: Only",
+ "Programming Language :: Python :: 3.10",
+ "Programming Language :: Python :: 3.11",
+ "Programming Language :: Python :: 3.12",
+ "Programming Language :: Python :: 3.13",
+ "Topic :: Software Development :: Libraries",
+ "Topic :: Software Development :: Version Control",
+ "Topic :: System :: Software Distribution",
+ "Topic :: Utilities",
+]
+dynamic = [
+ "version",
+]
+dependencies = [
+ # Core VCS functionality - workspace dependency
+ "vcs-versioning",
+ "packaging>=20",
+ # https://github.com/pypa/setuptools-scm/issues/1112 - re-pin in a breaking release
+ "setuptools", # >= 61",
+ 'tomli>=1; python_version < "3.11"',
+ 'typing-extensions; python_version < "3.11"',
+]
+
+
+[project.optional-dependencies]
+rich = ["rich"]
+simple = []
+toml = []
+
+[dependency-groups]
+docs = [
+ "mkdocs",
+ "mkdocs-entangled-plugin",
+ "mkdocs-include-markdown-plugin",
+ "mkdocs-material",
+ "mkdocstrings[python]",
+ "pygments",
+ "griffe-public-wildcard-imports",
+]
+test = [
+ "pip",
+ "build",
+ "pytest",
+ "pytest-timeout", # Timeout protection for CI/CD
+ "pytest-xdist",
+ "rich",
+ "ruff",
+ "mypy",
+ 'typing-extensions; python_version < "3.11"',
+ "griffe",
+ "griffe-public-wildcard-imports",
+ "flake8",
+]
+
+[project.urls]
+documentation = "https://setuptools-scm.readthedocs.io/"
+repository = "https://github.com/pypa/setuptools-scm/"
+
+[project.entry-points.console_scripts]
+setuptools-scm = "vcs_versioning._cli:main"
+
+[project.entry-points."distutils.setup_keywords"]
+use_scm_version = "setuptools_scm._integration.setuptools:version_keyword"
+
+[project.entry-points."pipx.run"]
+setuptools-scm = "vcs_versioning._cli:main"
+setuptools_scm = "vcs_versioning._cli:main"
+
+[project.entry-points."setuptools.file_finders"]
+setuptools_scm = "vcs_versioning._file_finders:find_files"
+
+[project.entry-points."setuptools.finalize_distribution_options"]
+setuptools_scm = "setuptools_scm._integration.setuptools:infer_version"
+
+[project.entry-points."setuptools_scm.files_command"]
+".git" = "vcs_versioning._file_finders._git:git_find_files"
+".hg" = "vcs_versioning._file_finders._hg:hg_find_files"
+
+[project.entry-points."setuptools_scm.files_command_fallback"]
+".git_archival.txt" = "vcs_versioning._file_finders._git:git_archive_find_files"
+".hg_archival.txt" = "vcs_versioning._file_finders._hg:hg_archive_find_files"
+
+# VCS-related entry points are now provided by vcs-versioning package
+# Only file-finder entry points remain in setuptools_scm
+
+[tool.setuptools.packages.find]
+where = ["src"]
+namespaces = false
+
+[tool.setuptools.dynamic]
+version = { attr = "_own_version_helper.version"}
+
+[tool.setuptools_scm]
+root = ".."
+version_scheme = "towncrier-fragments"
+tag_regex = "^setuptools-scm-(?Pv?\\d+(?:\\.\\d+){0,2}[^\\+]*)(?:\\+.*)?$"
+fallback_version = "9.2.2" # we trnasion to towncrier informed here
+scm.git.describe_command = ["git", "describe", "--dirty", "--tags", "--long", "--match", "setuptools-scm-*"]
+
+[tool.ruff]
+lint.extend-select = ["YTT", "B", "C4", "DTZ", "ISC", "LOG", "G", "PIE", "PYI", "PT", "FLY", "I", "C90", "PERF", "W", "PGH", "PLE", "UP", "FURB", "RUF"]
+lint.ignore = ["B028", "LOG015", "PERF203"]
+lint.preview = true
+
+[tool.ruff.lint.isort]
+force-single-line = true
+from-first = false
+lines-between-types = 1
+order-by-type = true
+
+[tool.pytest.ini_options]
+minversion = "8"
+testpaths = ["testing_scm"]
+addopts = ["-ra", "--strict-config", "--strict-markers", "-p", "vcs_versioning.test_api"]
+timeout = 300 # 5 minutes timeout per test for CI protection
+filterwarnings = [
+ "error",
+ "ignore:.*tool\\.setuptools_scm.*",
+ "ignore:.*git archive did not support describe output.*:UserWarning",
+]
+log_level = "debug"
+log_cli_level = "info"
+# disable unraisable until investigated
+markers = [
+ "issue(id): reference to github issue",
+ "skip_commit: allows to skip committing in the helpers",
+]
+
+[tool.uv]
+default-groups = ["test"]
+
+[tool.towncrier]
+directory = "changelog.d"
+filename = "CHANGELOG.md"
+start_string = "\n"
+template = "changelog.d/template.md"
+title_format = "## {version} ({project_date})"
+issue_format = "[#{issue}](https://github.com/pypa/setuptools-scm/issues/{issue})"
+underlines = ["", "", ""]
+
+[[tool.towncrier.type]]
+directory = "removal"
+name = "Removed"
+showcontent = true
+
+[[tool.towncrier.type]]
+directory = "deprecation"
+name = "Deprecated"
+showcontent = true
+
+[[tool.towncrier.type]]
+directory = "feature"
+name = "Added"
+showcontent = true
+
+[[tool.towncrier.type]]
+directory = "bugfix"
+name = "Fixed"
+showcontent = true
+
+[[tool.towncrier.type]]
+directory = "doc"
+name = "Documentation"
+showcontent = true
+
+[[tool.towncrier.type]]
+directory = "misc"
+name = "Miscellaneous"
+showcontent = true
diff --git a/src/setuptools_scm/.git_archival.txt b/setuptools-scm/src/setuptools_scm/.git_archival.txt
similarity index 100%
rename from src/setuptools_scm/.git_archival.txt
rename to setuptools-scm/src/setuptools_scm/.git_archival.txt
diff --git a/setuptools-scm/src/setuptools_scm/__init__.py b/setuptools-scm/src/setuptools_scm/__init__.py
new file mode 100644
index 00000000..4be2f671
--- /dev/null
+++ b/setuptools-scm/src/setuptools_scm/__init__.py
@@ -0,0 +1,30 @@
+"""
+:copyright: 2010-2023 by Ronny Pfannschmidt
+:license: MIT
+"""
+
+from __future__ import annotations
+
+from vcs_versioning import Configuration
+from vcs_versioning import NonNormalizedVersion
+from vcs_versioning import ScmVersion
+from vcs_versioning import Version
+from vcs_versioning._config import DEFAULT_LOCAL_SCHEME
+from vcs_versioning._config import DEFAULT_VERSION_SCHEME
+from vcs_versioning._dump_version import dump_version # soft deprecated
+from vcs_versioning._get_version_impl import _get_version
+from vcs_versioning._get_version_impl import get_version # soft deprecated
+
+# Public API
+__all__ = [
+ "DEFAULT_LOCAL_SCHEME",
+ "DEFAULT_VERSION_SCHEME",
+ "Configuration",
+ "NonNormalizedVersion",
+ "ScmVersion",
+ "Version",
+ "_get_version",
+ "dump_version",
+ # soft deprecated imports, left for backward compatibility
+ "get_version",
+]
diff --git a/setuptools-scm/src/setuptools_scm/__main__.py b/setuptools-scm/src/setuptools_scm/__main__.py
new file mode 100644
index 00000000..439fa674
--- /dev/null
+++ b/setuptools-scm/src/setuptools_scm/__main__.py
@@ -0,0 +1,6 @@
+"""Backward compatibility shim for __main__.py"""
+
+from vcs_versioning._cli import main
+
+if __name__ == "__main__":
+ raise SystemExit(main())
diff --git a/src/setuptools_scm/_integration/__init__.py b/setuptools-scm/src/setuptools_scm/_integration/__init__.py
similarity index 100%
rename from src/setuptools_scm/_integration/__init__.py
rename to setuptools-scm/src/setuptools_scm/_integration/__init__.py
diff --git a/src/setuptools_scm/_integration/deprecation.py b/setuptools-scm/src/setuptools_scm/_integration/deprecation.py
similarity index 100%
rename from src/setuptools_scm/_integration/deprecation.py
rename to setuptools-scm/src/setuptools_scm/_integration/deprecation.py
diff --git a/setuptools-scm/src/setuptools_scm/_integration/pyproject_reading.py b/setuptools-scm/src/setuptools_scm/_integration/pyproject_reading.py
new file mode 100644
index 00000000..453d39e7
--- /dev/null
+++ b/setuptools-scm/src/setuptools_scm/_integration/pyproject_reading.py
@@ -0,0 +1,147 @@
+from __future__ import annotations
+
+import logging
+
+from collections.abc import Sequence
+from pathlib import Path
+
+from vcs_versioning._pyproject_reading import DEFAULT_PYPROJECT_PATH
+from vcs_versioning._pyproject_reading import GivenPyProjectResult
+from vcs_versioning._pyproject_reading import PyProjectData
+from vcs_versioning._pyproject_reading import get_args_for_pyproject
+from vcs_versioning._pyproject_reading import read_pyproject as _vcs_read_pyproject
+from vcs_versioning._requirement_cls import Requirement
+from vcs_versioning._requirement_cls import extract_package_name
+from vcs_versioning._toml import TOML_RESULT
+
+log = logging.getLogger(__name__)
+
+_ROOT = "root"
+
+__all__ = [
+ "PyProjectData",
+ "get_args_for_pyproject",
+ "has_build_package_with_extra",
+ "read_pyproject",
+ "should_infer",
+]
+
+
+def should_infer(pyproject_data: PyProjectData) -> bool:
+ """
+ Determine if setuptools_scm should infer version based on configuration.
+
+ Infer when:
+ 1. An explicit [tool.setuptools_scm] section is present, OR
+ 2. setuptools-scm[simple] is in build-system.requires AND
+ version is in project.dynamic
+
+ Args:
+ pyproject_data: The PyProjectData instance to check
+
+ Returns:
+ True if version should be inferred, False otherwise
+ """
+ # Original behavior: explicit tool section
+ if pyproject_data.section_present:
+ return True
+
+ # New behavior: simple extra + dynamic version
+ if pyproject_data.project_present:
+ dynamic_fields = pyproject_data.project.get("dynamic", [])
+ if "version" in dynamic_fields:
+ if has_build_package_with_extra(
+ pyproject_data.build_requires, "setuptools-scm", "simple"
+ ):
+ return True
+
+ return False
+
+
+def has_build_package_with_extra(
+ requires: Sequence[str], canonical_build_package_name: str, extra_name: str
+) -> bool:
+ """Check if a build dependency has a specific extra.
+
+ Args:
+ requires: List of requirement strings from build-system.requires
+ canonical_build_package_name: The canonical package name to look for
+ extra_name: The extra name to check for (e.g., "simple")
+
+ Returns:
+ True if the package is found with the specified extra
+ """
+ for requirement_string in requires:
+ try:
+ requirement = Requirement(requirement_string)
+ package_name = extract_package_name(requirement_string)
+ if package_name == canonical_build_package_name:
+ if extra_name in requirement.extras:
+ return True
+ except Exception:
+ # If parsing fails, continue to next requirement
+ continue
+ return False
+
+
+def _check_setuptools_dynamic_version_conflict(
+ path: Path, pyproject_data: PyProjectData
+) -> None:
+ """Warn if tool.setuptools.dynamic.version conflicts with setuptools-scm.
+
+ Only warns if setuptools-scm is being used for version inference (not just file finding).
+ When only file finders are used, it's valid to use tool.setuptools.dynamic.version.
+ """
+ # Only warn if setuptools-scm is performing version inference
+ if not should_infer(pyproject_data):
+ return
+
+ # Check if tool.setuptools.dynamic.version exists
+ tool = pyproject_data.definition.get("tool", {})
+ if not isinstance(tool, dict):
+ return
+
+ setuptools_config = tool.get("setuptools", {})
+ if not isinstance(setuptools_config, dict):
+ return
+
+ dynamic_config = setuptools_config.get("dynamic", {})
+ if not isinstance(dynamic_config, dict):
+ return
+
+ if "version" in dynamic_config:
+ from .deprecation import warn_pyproject_setuptools_dynamic_version
+
+ warn_pyproject_setuptools_dynamic_version(path)
+
+
+def read_pyproject(
+ path: Path = DEFAULT_PYPROJECT_PATH,
+ tool_name: str = "setuptools_scm",
+ canonical_build_package_name: str = "setuptools-scm",
+ _given_result: GivenPyProjectResult = None,
+ _given_definition: TOML_RESULT | None = None,
+) -> PyProjectData:
+ """Read and parse pyproject configuration with setuptools-specific extensions.
+
+ This wraps vcs_versioning's read_pyproject and adds setuptools-specific behavior.
+ Uses internal multi-tool support to read both setuptools_scm and vcs-versioning sections.
+ """
+ # Use vcs_versioning's reader with multi-tool support (internal API)
+ # This allows setuptools_scm to transition to vcs-versioning section
+ pyproject_data = _vcs_read_pyproject(
+ path,
+ canonical_build_package_name=canonical_build_package_name,
+ _given_result=_given_result,
+ _given_definition=_given_definition,
+ tool_names=[
+ "setuptools_scm",
+ "vcs-versioning",
+ ], # Try both, setuptools_scm first
+ )
+
+ # Check for conflicting tool.setuptools.dynamic configuration
+ # Use the definition from pyproject_data (read by vcs_versioning)
+ _check_setuptools_dynamic_version_conflict(path, pyproject_data)
+
+ return pyproject_data
diff --git a/src/setuptools_scm/_integration/setup_cfg.py b/setuptools-scm/src/setuptools_scm/_integration/setup_cfg.py
similarity index 100%
rename from src/setuptools_scm/_integration/setup_cfg.py
rename to setuptools-scm/src/setuptools_scm/_integration/setup_cfg.py
diff --git a/src/setuptools_scm/_integration/setuptools.py b/setuptools-scm/src/setuptools_scm/_integration/setuptools.py
similarity index 68%
rename from src/setuptools_scm/_integration/setuptools.py
rename to setuptools-scm/src/setuptools_scm/_integration/setuptools.py
index aa1c645a..a84b77ce 100644
--- a/src/setuptools_scm/_integration/setuptools.py
+++ b/setuptools-scm/src/setuptools_scm/_integration/setuptools.py
@@ -3,20 +3,25 @@
import logging
import warnings
+from collections.abc import Callable
from typing import Any
-from typing import Callable
import setuptools
-from .. import _types as _t
+from vcs_versioning._pyproject_reading import GivenPyProjectResult
+from vcs_versioning._toml import InvalidTomlError
+from vcs_versioning.overrides import GlobalOverrides
+from vcs_versioning.overrides import ensure_context
+
from .pyproject_reading import PyProjectData
from .pyproject_reading import read_pyproject
from .setup_cfg import SetuptoolsBasicData
from .setup_cfg import extract_from_legacy
-from .toml import InvalidTomlError
+from .version_inference import GetVersionInferenceConfig
from .version_inference import get_version_inference_config
log = logging.getLogger(__name__)
+_setuptools_scm_logger = logging.getLogger("setuptools_scm")
def _warn_on_old_setuptools(_version: str = setuptools.__version__) -> None:
@@ -64,19 +69,19 @@ def get_keyword_overrides(
return value
+@ensure_context("SETUPTOOLS_SCM", additional_loggers=_setuptools_scm_logger)
def version_keyword(
dist: setuptools.Distribution,
keyword: str,
value: bool | dict[str, Any] | Callable[[], dict[str, Any]],
*,
- _given_pyproject_data: _t.GivenPyProjectResult = None,
+ _given_pyproject_data: GivenPyProjectResult = None,
_given_legacy_data: SetuptoolsBasicData | None = None,
- _get_version_inference_config: _t.GetVersionInferenceConfig = get_version_inference_config,
+ _get_version_inference_config: GetVersionInferenceConfig = get_version_inference_config,
) -> None:
"""apply version infernce when setup(use_scm_version=...) is used
this takes priority over the finalize_options based version
"""
-
_log_hookstart("version_keyword", dist)
# Parse overrides (integration point responsibility)
@@ -100,7 +105,7 @@ def version_keyword(
pyproject_data = read_pyproject(_given_result=_given_pyproject_data)
except FileNotFoundError:
log.debug("pyproject.toml not found, proceeding with empty configuration")
- pyproject_data = PyProjectData.empty()
+ pyproject_data = PyProjectData.empty(tool_name="setuptools_scm")
except InvalidTomlError as e:
log.debug("Configuration issue in pyproject.toml: %s", e)
return
@@ -112,22 +117,24 @@ def version_keyword(
else (legacy_data.version or pyproject_data.project_version)
)
- result = _get_version_inference_config(
- dist_name=dist_name,
- current_version=current_version,
- pyproject_data=pyproject_data,
- overrides=overrides,
- )
-
- result.apply(dist)
+ # Always use from_active to inherit current context settings
+ with GlobalOverrides.from_active(dist_name=dist_name):
+ result = _get_version_inference_config(
+ dist_name=dist_name,
+ current_version=current_version,
+ pyproject_data=pyproject_data,
+ overrides=overrides,
+ )
+ result.apply(dist)
+@ensure_context("SETUPTOOLS_SCM", additional_loggers=_setuptools_scm_logger)
def infer_version(
dist: setuptools.Distribution,
*,
- _given_pyproject_data: _t.GivenPyProjectResult = None,
+ _given_pyproject_data: GivenPyProjectResult = None,
_given_legacy_data: SetuptoolsBasicData | None = None,
- _get_version_inference_config: _t.GetVersionInferenceConfig = get_version_inference_config,
+ _get_version_inference_config: GetVersionInferenceConfig = get_version_inference_config,
) -> None:
"""apply version inference from the finalize_options hook
this is the default for pyproject.toml based projects that don't use the use_scm_version keyword
@@ -135,12 +142,31 @@ def infer_version(
if the version keyword is used, it will override the version from this hook
as user might have passed custom code version schemes
"""
-
_log_hookstart("infer_version", dist)
legacy_data = extract_from_legacy(dist, _given_legacy_data=_given_legacy_data)
- dist_name = legacy_data.name
+ dist_name: str | None = legacy_data.name
+
+ # Always use from_active to inherit current context settings
+ with GlobalOverrides.from_active(dist_name=dist_name):
+ _infer_version_impl(
+ dist,
+ dist_name=dist_name,
+ legacy_data=legacy_data,
+ _given_pyproject_data=_given_pyproject_data,
+ _get_version_inference_config=_get_version_inference_config,
+ )
+
+def _infer_version_impl(
+ dist: setuptools.Distribution,
+ *,
+ dist_name: str | None,
+ legacy_data: SetuptoolsBasicData,
+ _given_pyproject_data: GivenPyProjectResult = None,
+ _get_version_inference_config: GetVersionInferenceConfig = get_version_inference_config,
+) -> None:
+ """Internal implementation of infer_version."""
try:
pyproject_data = read_pyproject(_given_result=_given_pyproject_data)
except FileNotFoundError:
diff --git a/src/setuptools_scm/_integration/version_inference.py b/setuptools-scm/src/setuptools_scm/_integration/version_inference.py
similarity index 62%
rename from src/setuptools_scm/_integration/version_inference.py
rename to setuptools-scm/src/setuptools_scm/_integration/version_inference.py
index 6258d90b..381fd37b 100644
--- a/src/setuptools_scm/_integration/version_inference.py
+++ b/setuptools-scm/src/setuptools_scm/_integration/version_inference.py
@@ -1,18 +1,38 @@
from __future__ import annotations
+import logging
+
from dataclasses import dataclass
-from typing import TYPE_CHECKING
from typing import Any
-from typing import Union
+from typing import Protocol
+from typing import TypeAlias
from setuptools import Distribution
+from vcs_versioning._pyproject_reading import PyProjectData
+
+from .pyproject_reading import should_infer
+
+log = logging.getLogger(__name__)
+
-from .. import _log
+class VersionInferenceApplicable(Protocol):
+ """A result object from version inference decision that can be applied to a dist."""
-if TYPE_CHECKING:
- from .pyproject_reading import PyProjectData
+ def apply(self, dist: Distribution) -> None: # pragma: no cover - structural type
+ ...
-log = _log.log.getChild("version_inference")
+
+class GetVersionInferenceConfig(Protocol):
+ """Callable protocol for the decision function used by integration points."""
+
+ def __call__(
+ self,
+ dist_name: str | None,
+ current_version: str | None,
+ pyproject_data: PyProjectData,
+ overrides: dict[str, object] | None = None,
+ ) -> VersionInferenceApplicable: # pragma: no cover - structural type
+ ...
@dataclass
@@ -39,16 +59,16 @@ def apply(self, dist: Distribution) -> None:
@dataclass
-class VersionInferenceWarning:
- """Error message for user."""
+class VersionAlreadySetWarning:
+ """Warning that version was already set, inference would override it."""
- message: str
+ dist_name: str | None
def apply(self, dist: Distribution) -> None:
- """Apply error handling to the distribution."""
+ """Warn user that version is already set."""
import warnings
- warnings.warn(self.message)
+ warnings.warn(f"version of {self.dist_name} already set")
@dataclass(frozen=True)
@@ -59,11 +79,11 @@ def apply(self, dist: Distribution) -> None:
"""Apply no-op to the distribution."""
-VersionInferenceResult = Union[
- VersionInferenceConfig, # Proceed with inference
- VersionInferenceWarning, # Show warning
- VersionInferenceNoOp, # Don't infer (silent)
-]
+VersionInferenceResult: TypeAlias = (
+ VersionInferenceConfig # Proceed with inference
+ | VersionAlreadySetWarning # Warn: version already set
+ | VersionInferenceNoOp # Don't infer (silent)
+)
def infer_version_string(
@@ -87,20 +107,17 @@ def infer_version_string(
Returns:
The computed version string.
"""
- from .. import _config as _config_module
- from .._get_version_impl import _get_version
- from .._get_version_impl import _version_missing
-
- config = _config_module.Configuration.from_file(
- dist_name=dist_name, pyproject_data=pyproject_data, **(overrides or {})
+ from vcs_versioning._version_inference import (
+ infer_version_string as _vcs_infer_version_string,
)
- maybe_version = _get_version(
- config, force_write_version_files=force_write_version_files
+ # Delegate to vcs_versioning implementation
+ return _vcs_infer_version_string(
+ dist_name,
+ pyproject_data,
+ overrides,
+ force_write_version_files=force_write_version_files,
)
- if maybe_version is None:
- _version_missing(config)
- return maybe_version
def get_version_inference_config(
@@ -128,14 +145,12 @@ def get_version_inference_config(
overrides=overrides,
)
- inference_implied = pyproject_data.should_infer() or overrides is not None
+ inference_implied = should_infer(pyproject_data) or overrides is not None
if inference_implied:
if current_version is None:
return config
else:
- return VersionInferenceWarning(
- f"version of {dist_name} already set",
- )
+ return VersionAlreadySetWarning(dist_name)
else:
return VersionInferenceNoOp()
diff --git a/setuptools-scm/src/setuptools_scm/discover.py b/setuptools-scm/src/setuptools_scm/discover.py
new file mode 100644
index 00000000..d5a0d90c
--- /dev/null
+++ b/setuptools-scm/src/setuptools_scm/discover.py
@@ -0,0 +1,18 @@
+"""Re-export discover from vcs_versioning for backward compatibility"""
+
+from __future__ import annotations
+
+from vcs_versioning._discover import (
+ iter_matching_entrypoints as iter_matching_entrypoints,
+)
+from vcs_versioning._discover import log as log
+from vcs_versioning._discover import match_entrypoint as match_entrypoint
+from vcs_versioning._discover import walk_potential_roots as walk_potential_roots
+
+__all__ = [
+ # Functions
+ "iter_matching_entrypoints",
+ "log",
+ "match_entrypoint",
+ "walk_potential_roots",
+]
diff --git a/setuptools-scm/src/setuptools_scm/fallbacks.py b/setuptools-scm/src/setuptools_scm/fallbacks.py
new file mode 100644
index 00000000..a0a846ed
--- /dev/null
+++ b/setuptools-scm/src/setuptools_scm/fallbacks.py
@@ -0,0 +1,14 @@
+"""Re-export fallbacks from vcs_versioning for backward compatibility"""
+
+from __future__ import annotations
+
+from vcs_versioning._fallbacks import fallback_version as fallback_version
+from vcs_versioning._fallbacks import log as log
+from vcs_versioning._fallbacks import parse_pkginfo as parse_pkginfo
+
+__all__ = [
+ # Functions
+ "fallback_version",
+ "log",
+ "parse_pkginfo",
+]
diff --git a/setuptools-scm/src/setuptools_scm/git.py b/setuptools-scm/src/setuptools_scm/git.py
new file mode 100644
index 00000000..acfc1b56
--- /dev/null
+++ b/setuptools-scm/src/setuptools_scm/git.py
@@ -0,0 +1,48 @@
+"""Re-export git backend from vcs_versioning for backward compatibility
+
+NOTE: The git backend is private in vcs_versioning and accessed via entry points.
+This module provides backward compatibility for code that imported from setuptools_scm.git
+"""
+
+from __future__ import annotations
+
+from vcs_versioning._backends._git import DEFAULT_DESCRIBE as DEFAULT_DESCRIBE
+from vcs_versioning._backends._git import DESCRIBE_UNSUPPORTED as DESCRIBE_UNSUPPORTED
+from vcs_versioning._backends._git import REF_TAG_RE as REF_TAG_RE
+from vcs_versioning._backends._git import GitPreParse as GitPreParse
+from vcs_versioning._backends._git import GitWorkdir as GitWorkdir
+from vcs_versioning._backends._git import archival_to_version as archival_to_version
+from vcs_versioning._backends._git import (
+ fail_on_missing_submodules as fail_on_missing_submodules,
+)
+from vcs_versioning._backends._git import fail_on_shallow as fail_on_shallow
+from vcs_versioning._backends._git import fetch_on_shallow as fetch_on_shallow
+from vcs_versioning._backends._git import get_working_directory as get_working_directory
+from vcs_versioning._backends._git import log as log
+from vcs_versioning._backends._git import parse as parse
+from vcs_versioning._backends._git import parse_archival as parse_archival
+from vcs_versioning._backends._git import run_git as run_git
+from vcs_versioning._backends._git import version_from_describe as version_from_describe
+from vcs_versioning._backends._git import warn_on_shallow as warn_on_shallow
+
+__all__ = [
+ # Constants
+ "DEFAULT_DESCRIBE",
+ "DESCRIBE_UNSUPPORTED",
+ "REF_TAG_RE",
+ # Classes
+ "GitPreParse",
+ "GitWorkdir",
+ # Functions
+ "archival_to_version",
+ "fail_on_missing_submodules",
+ "fail_on_shallow",
+ "fetch_on_shallow",
+ "get_working_directory",
+ "log",
+ "parse",
+ "parse_archival",
+ "run_git",
+ "version_from_describe",
+ "warn_on_shallow",
+]
diff --git a/setuptools-scm/src/setuptools_scm/hg.py b/setuptools-scm/src/setuptools_scm/hg.py
new file mode 100644
index 00000000..475382a9
--- /dev/null
+++ b/setuptools-scm/src/setuptools_scm/hg.py
@@ -0,0 +1,25 @@
+"""Re-export hg backend from vcs_versioning for backward compatibility
+
+NOTE: The hg backend is private in vcs_versioning and accessed via entry points.
+This module provides backward compatibility for code that imported from setuptools_scm.hg
+"""
+
+from __future__ import annotations
+
+from vcs_versioning._backends._hg import HgWorkdir as HgWorkdir
+from vcs_versioning._backends._hg import archival_to_version as archival_to_version
+from vcs_versioning._backends._hg import log as log
+from vcs_versioning._backends._hg import parse as parse
+from vcs_versioning._backends._hg import parse_archival as parse_archival
+from vcs_versioning._backends._hg import run_hg as run_hg
+
+__all__ = [
+ # Classes
+ "HgWorkdir",
+ # Functions
+ "archival_to_version",
+ "log",
+ "parse",
+ "parse_archival",
+ "run_hg",
+]
diff --git a/setuptools-scm/src/setuptools_scm/hg_git.py b/setuptools-scm/src/setuptools_scm/hg_git.py
new file mode 100644
index 00000000..7c5409ec
--- /dev/null
+++ b/setuptools-scm/src/setuptools_scm/hg_git.py
@@ -0,0 +1,16 @@
+"""Re-export hg_git from vcs_versioning for backward compatibility
+
+NOTE: The hg_git module is private in vcs_versioning.
+This module provides backward compatibility for code that imported from setuptools_scm.hg_git
+"""
+
+from __future__ import annotations
+
+from vcs_versioning._backends._hg_git import GitWorkdirHgClient as GitWorkdirHgClient
+from vcs_versioning._backends._hg_git import log as log
+
+__all__ = [
+ # Classes
+ "GitWorkdirHgClient",
+ "log",
+]
diff --git a/setuptools-scm/src/setuptools_scm/integration.py b/setuptools-scm/src/setuptools_scm/integration.py
new file mode 100644
index 00000000..93b8b46f
--- /dev/null
+++ b/setuptools-scm/src/setuptools_scm/integration.py
@@ -0,0 +1,12 @@
+"""Re-export integration from vcs_versioning for backward compatibility"""
+
+from __future__ import annotations
+
+from vcs_versioning._integration import data_from_mime as data_from_mime
+from vcs_versioning._integration import log as log
+
+__all__ = [
+ # Functions
+ "data_from_mime",
+ "log",
+]
diff --git a/src/setuptools_scm/py.typed b/setuptools-scm/src/setuptools_scm/py.typed
similarity index 100%
rename from src/setuptools_scm/py.typed
rename to setuptools-scm/src/setuptools_scm/py.typed
diff --git a/setuptools-scm/src/setuptools_scm/scm_workdir.py b/setuptools-scm/src/setuptools_scm/scm_workdir.py
new file mode 100644
index 00000000..aa88b8c9
--- /dev/null
+++ b/setuptools-scm/src/setuptools_scm/scm_workdir.py
@@ -0,0 +1,21 @@
+"""Re-export scm_workdir from vcs_versioning for backward compatibility
+
+NOTE: The scm_workdir module is private in vcs_versioning.
+This module provides backward compatibility for code that imported from setuptools_scm.scm_workdir
+"""
+
+from __future__ import annotations
+
+from vcs_versioning._backends._scm_workdir import Workdir as Workdir
+from vcs_versioning._backends._scm_workdir import (
+ get_latest_file_mtime as get_latest_file_mtime,
+)
+from vcs_versioning._backends._scm_workdir import log as log
+
+__all__ = [
+ # Classes
+ "Workdir",
+ # Functions
+ "get_latest_file_mtime",
+ "log",
+]
diff --git a/setuptools-scm/src/setuptools_scm/version.py b/setuptools-scm/src/setuptools_scm/version.py
new file mode 100644
index 00000000..64937f66
--- /dev/null
+++ b/setuptools-scm/src/setuptools_scm/version.py
@@ -0,0 +1,76 @@
+"""Re-export version schemes from vcs_versioning for backward compatibility"""
+
+from __future__ import annotations
+
+from vcs_versioning._version_schemes import SEMVER_LEN as SEMVER_LEN
+from vcs_versioning._version_schemes import SEMVER_MINOR as SEMVER_MINOR
+from vcs_versioning._version_schemes import SEMVER_PATCH as SEMVER_PATCH
+from vcs_versioning._version_schemes import ScmVersion as ScmVersion
+from vcs_versioning._version_schemes import (
+ callable_or_entrypoint as callable_or_entrypoint,
+)
+from vcs_versioning._version_schemes import calver_by_date as calver_by_date
+from vcs_versioning._version_schemes import date_ver_match as date_ver_match
+from vcs_versioning._version_schemes import format_version as format_version
+from vcs_versioning._version_schemes import get_local_dirty_tag as get_local_dirty_tag
+from vcs_versioning._version_schemes import (
+ get_local_node_and_date as get_local_node_and_date,
+)
+from vcs_versioning._version_schemes import (
+ get_local_node_and_timestamp as get_local_node_and_timestamp,
+)
+from vcs_versioning._version_schemes import get_no_local_node as get_no_local_node
+from vcs_versioning._version_schemes import guess_next_date_ver as guess_next_date_ver
+from vcs_versioning._version_schemes import (
+ guess_next_dev_version as guess_next_dev_version,
+)
+from vcs_versioning._version_schemes import (
+ guess_next_simple_semver as guess_next_simple_semver,
+)
+from vcs_versioning._version_schemes import guess_next_version as guess_next_version
+from vcs_versioning._version_schemes import log as log
+from vcs_versioning._version_schemes import meta as meta
+from vcs_versioning._version_schemes import no_guess_dev_version as no_guess_dev_version
+from vcs_versioning._version_schemes import only_version as only_version
+from vcs_versioning._version_schemes import postrelease_version as postrelease_version
+from vcs_versioning._version_schemes import (
+ release_branch_semver as release_branch_semver,
+)
+from vcs_versioning._version_schemes import (
+ release_branch_semver_version as release_branch_semver_version,
+)
+from vcs_versioning._version_schemes import (
+ simplified_semver_version as simplified_semver_version,
+)
+from vcs_versioning._version_schemes import tag_to_version as tag_to_version
+
+__all__ = [
+ # Constants
+ "SEMVER_LEN",
+ "SEMVER_MINOR",
+ "SEMVER_PATCH",
+ # Classes
+ "ScmVersion",
+ # Functions
+ "callable_or_entrypoint",
+ "calver_by_date",
+ "date_ver_match",
+ "format_version",
+ "get_local_dirty_tag",
+ "get_local_node_and_date",
+ "get_local_node_and_timestamp",
+ "get_no_local_node",
+ "guess_next_date_ver",
+ "guess_next_dev_version",
+ "guess_next_simple_semver",
+ "guess_next_version",
+ "log",
+ "meta",
+ "no_guess_dev_version",
+ "only_version",
+ "postrelease_version",
+ "release_branch_semver",
+ "release_branch_semver_version",
+ "simplified_semver_version",
+ "tag_to_version",
+]
diff --git a/testing/Dockerfile.busted-buster b/setuptools-scm/testing_scm/Dockerfile.busted-buster
similarity index 100%
rename from testing/Dockerfile.busted-buster
rename to setuptools-scm/testing_scm/Dockerfile.busted-buster
diff --git a/testing/Dockerfile.rawhide-git b/setuptools-scm/testing_scm/Dockerfile.rawhide-git
similarity index 100%
rename from testing/Dockerfile.rawhide-git
rename to setuptools-scm/testing_scm/Dockerfile.rawhide-git
diff --git a/testing/INTEGRATION_MIGRATION_PLAN.md b/setuptools-scm/testing_scm/INTEGRATION_MIGRATION_PLAN.md
similarity index 100%
rename from testing/INTEGRATION_MIGRATION_PLAN.md
rename to setuptools-scm/testing_scm/INTEGRATION_MIGRATION_PLAN.md
diff --git a/testing/__init__.py b/setuptools-scm/testing_scm/__init__.py
similarity index 100%
rename from testing/__init__.py
rename to setuptools-scm/testing_scm/__init__.py
diff --git a/setuptools-scm/testing_scm/conftest.py b/setuptools-scm/testing_scm/conftest.py
new file mode 100644
index 00000000..f404e971
--- /dev/null
+++ b/setuptools-scm/testing_scm/conftest.py
@@ -0,0 +1,83 @@
+"""Pytest configuration for setuptools_scm tests.
+
+Uses vcs_versioning.test_api as a pytest plugin for common test infrastructure.
+"""
+
+from __future__ import annotations
+
+import os
+
+from pathlib import Path
+from typing import Any
+
+import pytest
+
+
+# Re-export for convenience
+from vcs_versioning.test_api import TEST_SOURCE_DATE
+from vcs_versioning.test_api import TEST_SOURCE_DATE_EPOCH
+from vcs_versioning.test_api import TEST_SOURCE_DATE_FORMATTED
+from vcs_versioning.test_api import TEST_SOURCE_DATE_TIMESTAMP
+from vcs_versioning.test_api import DebugMode
+from vcs_versioning.test_api import WorkDir
+
+# Use vcs_versioning test infrastructure as a pytest plugin
+# Moved to pyproject.toml addopts to avoid non-top-level conftest issues
+# pytest_plugins = ["vcs_versioning.test_api"]
+
+__all__ = [
+ "TEST_SOURCE_DATE",
+ "TEST_SOURCE_DATE_EPOCH",
+ "TEST_SOURCE_DATE_FORMATTED",
+ "TEST_SOURCE_DATE_TIMESTAMP",
+ "DebugMode",
+ "WorkDir",
+]
+
+
+def pytest_configure(config: pytest.Config) -> None:
+ """Additional configuration for setuptools_scm tests."""
+ # Set both debug env vars for backward compatibility
+ os.environ["SETUPTOOLS_SCM_DEBUG"] = "1"
+
+
+VERSION_PKGS = [
+ "setuptools",
+ "setuptools_scm",
+ "vcs-versioning",
+ "packaging",
+ "build",
+ "wheel",
+]
+
+
+def pytest_report_header() -> list[str]:
+ """Report package versions at test start."""
+ from importlib.metadata import version
+
+ res = []
+ for pkg in VERSION_PKGS:
+ try:
+ pkg_version = version(pkg)
+ module_name = pkg.replace("-", "_")
+ path = __import__(module_name).__file__
+ if path and "site-packages" in path:
+ # Replace everything up to and including site-packages with site::
+ parts = path.split("site-packages", 1)
+ if len(parts) > 1:
+ path = "site::" + parts[1]
+ elif path and str(Path.cwd()) in path:
+ # Replace current working directory with CWD::
+ path = path.replace(str(Path.cwd()), "CWD::")
+ res.append(f"{pkg} version {pkg_version} from {path}")
+ except Exception:
+ pass
+ return res
+
+
+def pytest_addoption(parser: Any) -> None:
+ """Add setuptools_scm-specific test options."""
+ group = parser.getgroup("setuptools_scm")
+ group.addoption(
+ "--test-legacy", dest="scm_test_virtualenv", default=False, action="store_true"
+ )
diff --git a/testing/play_out_381.bash b/setuptools-scm/testing_scm/play_out_381.bash
similarity index 100%
rename from testing/play_out_381.bash
rename to setuptools-scm/testing_scm/play_out_381.bash
diff --git a/testing/test_basic_api.py b/setuptools-scm/testing_scm/test_basic_api.py
similarity index 96%
rename from testing/test_basic_api.py
rename to setuptools-scm/testing_scm/test_basic_api.py
index 7847b352..1d639c73 100644
--- a/testing/test_basic_api.py
+++ b/setuptools-scm/testing_scm/test_basic_api.py
@@ -8,15 +8,17 @@
import pytest
+from vcs_versioning._overrides import PRETEND_KEY
+from vcs_versioning._run_cmd import run
+from vcs_versioning.test_api import WorkDir
+
import setuptools_scm
from setuptools_scm import Configuration
from setuptools_scm import dump_version
-from setuptools_scm._run_cmd import run
from setuptools_scm.integration import data_from_mime
from setuptools_scm.version import ScmVersion
from setuptools_scm.version import meta
-from testing.wd_wrapper import WorkDir
c = Configuration()
@@ -59,7 +61,10 @@ def assertion(config: Configuration) -> ScmVersion:
return ScmVersion(Version("1.0"), config=config)
- monkeypatch.setattr(setuptools_scm._get_version_impl, "parse_version", assertion)
+ # Patch at vcs_versioning level since that's where the implementation lives
+ import vcs_versioning._get_version_impl
+
+ monkeypatch.setattr(vcs_versioning._get_version_impl, "parse_version", assertion)
def test_root_parameter_creation(monkeypatch: pytest.MonkeyPatch) -> None:
@@ -150,7 +155,7 @@ def test_get_version_blank_tag_regex() -> None:
"version", ["1.0", "1.2.3.dev1+ge871260", "1.2.3.dev15+ge871260.d20180625", "2345"]
)
def test_pretended(version: str, monkeypatch: pytest.MonkeyPatch) -> None:
- monkeypatch.setenv(setuptools_scm._overrides.PRETEND_KEY, version)
+ monkeypatch.setenv(PRETEND_KEY, version)
assert setuptools_scm.get_version() == version
diff --git a/testing/test_cli.py b/setuptools-scm/testing_scm/test_cli.py
similarity index 95%
rename from testing/test_cli.py
rename to setuptools-scm/testing_scm/test_cli.py
index 46a0f3aa..1392c4f8 100644
--- a/testing/test_cli.py
+++ b/setuptools-scm/testing_scm/test_cli.py
@@ -6,11 +6,12 @@
import pytest
-from setuptools_scm._cli import main
+from vcs_versioning._cli import main
+from vcs_versioning.test_api import WorkDir
+
from setuptools_scm._integration.pyproject_reading import PyProjectData
from .conftest import DebugMode
-from .wd_wrapper import WorkDir
@pytest.fixture
@@ -27,16 +28,24 @@ def wd(wd: WorkDir, monkeypatch: pytest.MonkeyPatch, debug_mode: DebugMode) -> W
PYPROJECT_ROOT = '[tool.setuptools_scm]\nroot=".."'
# PyProjectData constants for testing
-PYPROJECT_DATA_SIMPLE = PyProjectData.for_testing(section_present=True)
+PYPROJECT_DATA_SIMPLE = PyProjectData.for_testing(
+ tool_name="setuptools_scm", section_present=True
+)
PYPROJECT_DATA_WITH_PROJECT = PyProjectData.for_testing(
- section_present=True, project_present=True, project_name="test"
+ tool_name="setuptools_scm",
+ section_present=True,
+ project_present=True,
+ project_name="test",
)
def _create_version_file_pyproject_data() -> PyProjectData:
"""Create PyProjectData with version_file configuration for testing."""
data = PyProjectData.for_testing(
- section_present=True, project_present=True, project_name="test"
+ tool_name="setuptools_scm",
+ section_present=True,
+ project_present=True,
+ project_name="test",
)
data.section["version_file"] = "ver.py"
return data
diff --git a/testing/test_config.py b/setuptools-scm/testing_scm/test_config.py
similarity index 57%
rename from testing/test_config.py
rename to setuptools-scm/testing_scm/test_config.py
index d0f06bd6..2d47f18c 100644
--- a/testing/test_config.py
+++ b/setuptools-scm/testing_scm/test_config.py
@@ -1,6 +1,10 @@
+"""Tests for setuptools-scm specific Configuration functionality.
+
+Core Configuration tests have been moved to vcs-versioning/testing_vcs/test_config.py
+"""
+
from __future__ import annotations
-import re
import textwrap
from pathlib import Path
@@ -10,28 +14,6 @@
from setuptools_scm import Configuration
-@pytest.mark.parametrize(
- ("tag", "expected_version"),
- [
- ("apache-arrow-0.9.0", "0.9.0"),
- ("arrow-0.9.0", "0.9.0"),
- ("arrow-0.9.0-rc", "0.9.0-rc"),
- ("arrow-1", "1"),
- ("arrow-1+", "1"),
- ("arrow-1+foo", "1"),
- ("arrow-1.1+foo", "1.1"),
- ("v1.1", "v1.1"),
- ("V1.1", "V1.1"),
- ],
-)
-def test_tag_regex(tag: str, expected_version: str) -> None:
- config = Configuration()
- match = config.tag_regex.match(tag)
- assert match
- version = match.group("version")
- assert version == expected_version
-
-
def test_config_from_pyproject(tmp_path: Path) -> None:
fn = tmp_path / "pyproject.toml"
fn.write_text(
@@ -45,13 +27,7 @@ def test_config_from_pyproject(tmp_path: Path) -> None:
),
encoding="utf-8",
)
- assert Configuration.from_file(str(fn))
-
-
-def test_config_regex_init() -> None:
- tag_regex = re.compile(r"v(\d+)")
- conf = Configuration(tag_regex=tag_regex)
- assert conf.tag_regex is tag_regex
+ Configuration.from_file(str(fn))
def test_config_from_file_protects_relative_to(tmp_path: Path) -> None:
@@ -74,7 +50,7 @@ def test_config_from_file_protects_relative_to(tmp_path: Path) -> None:
"ignoring value relative_to='dont_use_me'"
" as its always relative to the config file",
):
- assert Configuration.from_file(str(fn))
+ Configuration.from_file(str(fn))
def test_config_overrides(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
@@ -98,23 +74,3 @@ def test_config_overrides(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> No
assert pristine.root != overridden.root
assert pristine.fallback_root != overridden.fallback_root
-
-
-@pytest.mark.parametrize(
- "tag_regex",
- [
- r".*",
- r"(.+)(.+)",
- r"((.*))",
- ],
-)
-def test_config_bad_regex(tag_regex: str) -> None:
- with pytest.raises(
- ValueError,
- match=(
- f"Expected tag_regex '{re.escape(tag_regex)}' to contain a single match"
- " group or a group named 'version' to identify the version part of any"
- " tag."
- ),
- ):
- Configuration(tag_regex=re.compile(tag_regex))
diff --git a/testing/test_deprecation.py b/setuptools-scm/testing_scm/test_deprecation.py
similarity index 100%
rename from testing/test_deprecation.py
rename to setuptools-scm/testing_scm/test_deprecation.py
diff --git a/setuptools-scm/testing_scm/test_functions.py b/setuptools-scm/testing_scm/test_functions.py
new file mode 100644
index 00000000..9dd069d8
--- /dev/null
+++ b/setuptools-scm/testing_scm/test_functions.py
@@ -0,0 +1,178 @@
+"""Tests for setuptools-scm specific dump_version functionality.
+
+Core version scheme tests have been moved to vcs-versioning/testing_vcs/test_version_schemes.py
+"""
+
+from __future__ import annotations
+
+import shutil
+import subprocess
+
+from datetime import datetime
+from datetime import timezone
+from pathlib import Path
+
+import pytest
+
+from vcs_versioning._overrides import PRETEND_KEY
+
+from setuptools_scm import Configuration
+from setuptools_scm import dump_version
+from setuptools_scm import get_version
+from setuptools_scm.version import meta
+
+c = Configuration()
+
+# Use explicit time to avoid triggering auto-creation of GlobalOverrides at import time
+VERSIONS = {
+ "exact": meta(
+ "1.1",
+ distance=0,
+ dirty=False,
+ config=c,
+ time=datetime(2009, 2, 13, 23, 31, 30, tzinfo=timezone.utc),
+ ),
+}
+
+
+def test_dump_version_doesnt_bail_on_value_error(tmp_path: Path) -> None:
+ write_to = "VERSION"
+ version = str(VERSIONS["exact"].tag)
+ scm_version = meta(VERSIONS["exact"].tag, config=c)
+ with pytest.raises(ValueError, match=r"^bad file format:"):
+ dump_version(tmp_path, version, write_to, scm_version=scm_version)
+
+
+@pytest.mark.parametrize(
+ "version", ["1.0", "1.2.3.dev1+ge871260", "1.2.3.dev15+ge871260.d20180625"]
+)
+def test_dump_version_works_with_pretend(
+ version: str, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
+) -> None:
+ monkeypatch.setenv(PRETEND_KEY, version)
+ name = "VERSION.txt"
+ target = tmp_path.joinpath(name)
+ get_version(root=tmp_path, write_to=name)
+ assert target.read_text(encoding="utf-8") == version
+
+
+def test_dump_version_modern(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
+ version = "1.2.3"
+ monkeypatch.setenv(PRETEND_KEY, version)
+ name = "VERSION.txt"
+
+ project = tmp_path.joinpath("project")
+ target = project.joinpath(name)
+ project.mkdir()
+
+ get_version(root="..", relative_to=target, version_file=name)
+ assert target.read_text(encoding="utf-8") == version
+
+
+def dump_a_version(tmp_path: Path) -> None:
+ from vcs_versioning._dump_version import write_version_to_path
+
+ version = "1.2.3"
+ scm_version = meta(version, config=c)
+ write_version_to_path(
+ tmp_path / "VERSION.py", template=None, version=version, scm_version=scm_version
+ )
+
+
+def test_dump_version_on_old_python(tmp_path: Path) -> None:
+ python37 = shutil.which("python3.7")
+ if python37 is None:
+ pytest.skip("python3.7 not found")
+ dump_a_version(tmp_path)
+ subprocess.run(
+ [python37, "-c", "import VERSION;print(VERSION.version)"],
+ cwd=tmp_path,
+ check=True,
+ )
+
+
+def test_dump_version_mypy(tmp_path: Path) -> None:
+ uvx = shutil.which("uvx")
+ if uvx is None:
+ pytest.skip("uvx not found")
+ dump_a_version(tmp_path)
+ # Use mypy 1.11.2 - last version supporting Python 3.8
+ subprocess.run(
+ [uvx, "mypy==1.11.2", "--python-version=3.8", "--strict", "VERSION.py"],
+ cwd=tmp_path,
+ check=True,
+ )
+
+
+def test_dump_version_flake8(tmp_path: Path) -> None:
+ flake8 = shutil.which("flake8")
+ if flake8 is None:
+ pytest.skip("flake8 not found")
+ dump_a_version(tmp_path)
+ subprocess.run([flake8, "VERSION.py"], cwd=tmp_path, check=True)
+
+
+def test_dump_version_ruff(tmp_path: Path) -> None:
+ ruff = shutil.which("ruff")
+ if ruff is None:
+ pytest.skip("ruff not found")
+ dump_a_version(tmp_path)
+ subprocess.run([ruff, "check", "--no-fix", "VERSION.py"], cwd=tmp_path, check=True)
+
+
+def test_write_version_to_path_deprecation_warning_none(tmp_path: Path) -> None:
+ """Test that write_version_to_path warns when scm_version=None is passed."""
+ from vcs_versioning._dump_version import write_version_to_path
+
+ target_file = tmp_path / "version.py"
+
+ # This should raise a deprecation warning when scm_version=None is explicitly passed
+ with pytest.warns(
+ DeprecationWarning, match="write_version_to_path called without scm_version"
+ ):
+ write_version_to_path(
+ target=target_file,
+ template=None, # Use default template
+ version="1.2.3",
+ scm_version=None, # Explicitly passing None should warn
+ )
+
+ # Verify the file was created and contains the expected content
+ assert target_file.exists()
+ content = target_file.read_text(encoding="utf-8")
+
+ # Check that the version is correctly formatted
+ assert "__version__ = version = '1.2.3'" in content
+ assert "__version_tuple__ = version_tuple = (1, 2, 3)" in content
+
+ # Check that commit_id is set to None when scm_version is None
+ assert "__commit_id__ = commit_id = None" in content
+
+
+def test_write_version_to_path_deprecation_warning_missing(tmp_path: Path) -> None:
+ """Test that write_version_to_path warns when scm_version parameter is not provided."""
+ from vcs_versioning._dump_version import write_version_to_path
+
+ target_file = tmp_path / "version.py"
+
+ # This should raise a deprecation warning when scm_version is not provided
+ with pytest.warns(
+ DeprecationWarning, match="write_version_to_path called without scm_version"
+ ):
+ write_version_to_path(
+ target=target_file,
+ template=None, # Use default template
+ version="1.2.3",
+ # scm_version not provided - should warn
+ )
+
+ # Verify the file was created and contains the expected content
+ assert target_file.exists()
+ content = target_file.read_text(encoding="utf-8")
+
+ # Check that the version is correctly formatted
+ assert "__version__ = version = '1.2.3'" in content
+ assert "__version_tuple__ = version_tuple = (1, 2, 3)" in content
+
+ # Check that commit_id is set to None when scm_version is None
+ assert "__commit_id__ = commit_id = None" in content
diff --git a/testing/test_integration.py b/setuptools-scm/testing_scm/test_integration.py
similarity index 96%
rename from testing/test_integration.py
rename to setuptools-scm/testing_scm/test_integration.py
index 6800a314..6b0ceb83 100644
--- a/testing/test_integration.py
+++ b/setuptools-scm/testing_scm/test_integration.py
@@ -14,23 +14,23 @@
import pytest
from packaging.version import Version
+from vcs_versioning._requirement_cls import extract_package_name
from setuptools_scm._integration import setuptools as setuptools_integration
from setuptools_scm._integration.pyproject_reading import PyProjectData
from setuptools_scm._integration.setup_cfg import SetuptoolsBasicData
from setuptools_scm._integration.setup_cfg import read_setup_cfg
-from setuptools_scm._requirement_cls import extract_package_name
if TYPE_CHECKING:
import setuptools
+from vcs_versioning._overrides import PRETEND_KEY
+from vcs_versioning._overrides import PRETEND_KEY_NAMED
+from vcs_versioning._run_cmd import run
+from vcs_versioning.test_api import WorkDir
+
from setuptools_scm import Configuration
from setuptools_scm._integration.setuptools import _warn_on_old_setuptools
-from setuptools_scm._overrides import PRETEND_KEY
-from setuptools_scm._overrides import PRETEND_KEY_NAMED
-from setuptools_scm._run_cmd import run
-
-from .wd_wrapper import WorkDir
c = Configuration()
@@ -104,7 +104,7 @@ def test_pretend_metadata_with_version(
monkeypatch: pytest.MonkeyPatch, wd: WorkDir
) -> None:
"""Test pretend metadata overrides work with pretend version."""
- from setuptools_scm._overrides import PRETEND_METADATA_KEY
+ from vcs_versioning._overrides import PRETEND_METADATA_KEY
monkeypatch.setenv(PRETEND_KEY, "1.2.3.dev4+g1337beef")
monkeypatch.setenv(PRETEND_METADATA_KEY, '{node="g1337beef", distance=4}')
@@ -134,7 +134,7 @@ def test_pretend_metadata_with_version(
def test_pretend_metadata_named(monkeypatch: pytest.MonkeyPatch, wd: WorkDir) -> None:
"""Test pretend metadata with named package support."""
- from setuptools_scm._overrides import PRETEND_METADATA_KEY_NAMED
+ from vcs_versioning._overrides import PRETEND_METADATA_KEY_NAMED
monkeypatch.setenv(
PRETEND_KEY_NAMED.format(name="test".upper()), "1.2.3.dev5+gabcdef12"
@@ -152,7 +152,7 @@ def test_pretend_metadata_without_version_warns(
monkeypatch: pytest.MonkeyPatch, wd: WorkDir, caplog: pytest.LogCaptureFixture
) -> None:
"""Test that pretend metadata without any base version logs a warning."""
- from setuptools_scm._overrides import PRETEND_METADATA_KEY
+ from vcs_versioning._overrides import PRETEND_METADATA_KEY
# Only set metadata, no version - but there will be a git repo so there will be a base version
# Let's create an empty git repo without commits to truly have no base version
@@ -169,7 +169,7 @@ def test_pretend_metadata_with_scm_version(
wd: WorkDir, monkeypatch: pytest.MonkeyPatch
) -> None:
"""Test that pretend metadata works with actual SCM-detected version."""
- from setuptools_scm._overrides import PRETEND_METADATA_KEY
+ from vcs_versioning._overrides import PRETEND_METADATA_KEY
# Set up a git repo with a tag so we have a base version
wd("git init")
@@ -208,7 +208,7 @@ def test_pretend_metadata_type_conversion(
monkeypatch: pytest.MonkeyPatch, wd: WorkDir
) -> None:
"""Test that pretend metadata properly uses TOML native types."""
- from setuptools_scm._overrides import PRETEND_METADATA_KEY
+ from vcs_versioning._overrides import PRETEND_METADATA_KEY
monkeypatch.setenv(PRETEND_KEY, "2.0.0")
monkeypatch.setenv(
@@ -225,7 +225,7 @@ def test_pretend_metadata_invalid_fields_filtered(
monkeypatch: pytest.MonkeyPatch, wd: WorkDir, caplog: pytest.LogCaptureFixture
) -> None:
"""Test that invalid metadata fields are filtered out with a warning."""
- from setuptools_scm._overrides import PRETEND_METADATA_KEY
+ from vcs_versioning._overrides import PRETEND_METADATA_KEY
monkeypatch.setenv(PRETEND_KEY, "1.0.0")
monkeypatch.setenv(
@@ -237,7 +237,7 @@ def test_pretend_metadata_invalid_fields_filtered(
version = wd.get_version()
assert version == "1.0.0"
- assert "Invalid metadata fields in pretend metadata" in caplog.text
+ assert "Invalid fields in TOML data" in caplog.text
assert "invalid_field" in caplog.text
assert "another_bad_field" in caplog.text
@@ -246,7 +246,7 @@ def test_pretend_metadata_date_parsing(
monkeypatch: pytest.MonkeyPatch, wd: WorkDir
) -> None:
"""Test that TOML date values work in pretend metadata."""
- from setuptools_scm._overrides import PRETEND_METADATA_KEY
+ from vcs_versioning._overrides import PRETEND_METADATA_KEY
monkeypatch.setenv(PRETEND_KEY, "1.5.0")
monkeypatch.setenv(
@@ -261,7 +261,7 @@ def test_pretend_metadata_invalid_toml_error(
monkeypatch: pytest.MonkeyPatch, wd: WorkDir, caplog: pytest.LogCaptureFixture
) -> None:
"""Test that invalid TOML in pretend metadata logs an error."""
- from setuptools_scm._overrides import PRETEND_METADATA_KEY
+ from vcs_versioning._overrides import PRETEND_METADATA_KEY
monkeypatch.setenv(PRETEND_KEY, "1.0.0")
monkeypatch.setenv(PRETEND_METADATA_KEY, "{invalid toml syntax here}")
@@ -493,6 +493,7 @@ def test_setup_cfg_version_prevents_inference_version_keyword(
# Construct PyProjectData directly without requiring build backend inference
pyproject_data = PyProjectData.for_testing(
+ tool_name="setuptools_scm",
is_required=False, # setuptools-scm not required
section_present=False, # no [tool.setuptools_scm] section
project_present=False, # no [project] section
@@ -655,6 +656,7 @@ def test_integration_function_call_order(
# Create PyProjectData with equivalent configuration - no file I/O!
project_name = "test-call-order"
pyproject_data = PyProjectData.for_testing(
+ tool_name="setuptools_scm",
project_name=project_name,
has_dynamic_version=True,
project_present=True,
diff --git a/testing/test_main.py b/setuptools-scm/testing_scm/test_main.py
similarity index 97%
rename from testing/test_main.py
rename to setuptools-scm/testing_scm/test_main.py
index cd21b6d8..c63b0731 100644
--- a/testing/test_main.py
+++ b/setuptools-scm/testing_scm/test_main.py
@@ -7,7 +7,7 @@
import pytest
-from .wd_wrapper import WorkDir
+from vcs_versioning.test_api import WorkDir
def test_main() -> None:
diff --git a/setuptools-scm/testing_scm/test_pyproject_reading.py b/setuptools-scm/testing_scm/test_pyproject_reading.py
new file mode 100644
index 00000000..55e9938c
--- /dev/null
+++ b/setuptools-scm/testing_scm/test_pyproject_reading.py
@@ -0,0 +1,228 @@
+from __future__ import annotations
+
+import configparser
+
+from pathlib import Path
+from unittest.mock import Mock
+
+import pytest
+
+from setuptools_scm._integration.pyproject_reading import has_build_package_with_extra
+from setuptools_scm._integration.pyproject_reading import read_pyproject
+from setuptools_scm._integration.pyproject_reading import should_infer
+
+
+def parametrize_build_package_tests(ini_string: str) -> pytest.MarkDecorator:
+ """Parametrize has_build_package_with_extra tests from INI string.
+
+ Specific parser for testing build package requirements with extras.
+
+ Parameters:
+ - requires: multiline list of requirement strings
+ - package_name: string
+ - extra: string
+ - expected: boolean (using ConfigParser's getboolean)
+ """
+ parser = configparser.ConfigParser()
+ parser.read_string(ini_string)
+
+ test_cases = []
+ for section_name in parser.sections():
+ section = parser[section_name]
+
+ # Parse requires as list - split on newlines and strip
+ requires_str = section.get("requires", "")
+ requires = [line.strip() for line in requires_str.splitlines() if line.strip()]
+
+ # Parse strings directly
+ package_name = section.get("package_name")
+ extra = section.get("extra")
+
+ # Parse boolean using ConfigParser's native method
+ expected = section.getboolean("expected")
+
+ test_cases.append(
+ pytest.param(requires, package_name, extra, expected, id=section_name)
+ )
+
+ return pytest.mark.parametrize(
+ ("requires", "package_name", "extra", "expected"),
+ test_cases,
+ )
+
+
+class TestPyProjectReading:
+ """Test the pyproject reading functionality."""
+
+ def test_read_pyproject_missing_file_raises(self, tmp_path: Path) -> None:
+ """Test that read_pyproject raises FileNotFoundError when file is missing."""
+ with pytest.raises(FileNotFoundError):
+ read_pyproject(path=tmp_path / "nonexistent.toml")
+
+ def test_read_pyproject_existing_file(self, tmp_path: Path) -> None:
+ """Test that read_pyproject reads existing files correctly."""
+ # Create a simple pyproject.toml
+ pyproject_content = """
+[build-system]
+requires = ["setuptools>=80", "setuptools-scm>=8"]
+build-backend = "setuptools.build_meta"
+
+[project]
+name = "test-package"
+dynamic = ["version"]
+
+[tool.setuptools_scm]
+"""
+ pyproject_file = tmp_path / "pyproject.toml"
+ pyproject_file.write_text(pyproject_content, encoding="utf-8")
+
+ result = read_pyproject(path=pyproject_file)
+
+ assert result.path == pyproject_file
+ assert result.tool_name == "setuptools_scm"
+ assert result.is_required is True
+ assert result.section_present is True
+ assert result.project_present is True
+ assert result.project.get("name") == "test-package"
+
+
+@parametrize_build_package_tests(
+ """
+ [has_simple_extra]
+ requires =
+ setuptools-scm[simple]
+ package_name = setuptools-scm
+ extra = simple
+ expected = true
+
+ [has_no_simple_extra]
+ requires =
+ setuptools-scm
+ package_name = setuptools-scm
+ extra = simple
+ expected = false
+
+ [has_different_extra]
+ requires =
+ setuptools-scm[toml]
+ package_name = setuptools-scm
+ extra = simple
+ expected = false
+
+ [has_multiple_extras_including_simple]
+ requires =
+ setuptools-scm[simple,toml]
+ package_name = setuptools-scm
+ extra = simple
+ expected = true
+
+ [different_package_with_simple_extra]
+ requires =
+ other-package[simple]
+ package_name = setuptools-scm
+ extra = simple
+ expected = false
+
+ [version_specifier_with_extra]
+ requires =
+ setuptools-scm[simple]>=8.0
+ package_name = setuptools-scm
+ extra = simple
+ expected = true
+
+ [complex_requirement_with_extra]
+ requires =
+ setuptools-scm[simple]>=8.0,<9.0
+ package_name = setuptools-scm
+ extra = simple
+ expected = true
+
+ [empty_requires_list]
+ requires =
+ package_name = setuptools-scm
+ extra = simple
+ expected = false
+
+ [invalid_requirement_string]
+ requires =
+ invalid requirement string
+ package_name = setuptools-scm
+ extra = simple
+ expected = false
+ """
+)
+def test_has_build_package_with_extra(
+ requires: list[str], package_name: str, extra: str, expected: bool
+) -> None:
+ """Test the has_build_package_with_extra function with various inputs."""
+ assert has_build_package_with_extra(requires, package_name, extra) is expected
+
+
+def test_read_pyproject_with_given_definition(monkeypatch: pytest.MonkeyPatch) -> None:
+ """Test that read_pyproject reads existing files correctly."""
+ monkeypatch.setattr(
+ "vcs_versioning._toml.read_toml_content",
+ Mock(side_effect=FileNotFoundError("this test should not read")),
+ )
+
+ res = read_pyproject(
+ _given_definition={
+ "build-system": {"requires": ["setuptools-scm[simple]"]},
+ "project": {"name": "test-package", "dynamic": ["version"]},
+ }
+ )
+
+ assert should_infer(res)
+
+
+def test_read_pyproject_with_setuptools_dynamic_version_warns() -> None:
+ """Test that warning is issued when version inference is enabled."""
+ with pytest.warns(
+ UserWarning,
+ match=r"pyproject\.toml: at \[tool\.setuptools\.dynamic\]",
+ ):
+ pyproject_data = read_pyproject(
+ _given_definition={
+ "build-system": {"requires": ["setuptools-scm[simple]"]},
+ "project": {"name": "test-package", "dynamic": ["version"]},
+ "tool": {
+ "setuptools": {
+ "dynamic": {"version": {"attr": "test_package.__version__"}}
+ }
+ },
+ }
+ )
+ assert pyproject_data.project_version is None
+
+
+def test_read_pyproject_with_setuptools_dynamic_version_no_warn_when_file_finder_only() -> (
+ None
+):
+ """Test that no warning is issued when only file finder is used (no version inference)."""
+ # When setuptools-scm is used only for file finding (no [tool.setuptools_scm] section,
+ # no [simple] extra, version not in dynamic), it's valid to use tool.setuptools.dynamic.version
+ import warnings
+
+ with warnings.catch_warnings(record=True) as warning_list:
+ warnings.simplefilter("always")
+ pyproject_data = read_pyproject(
+ _given_definition={
+ "build-system": {"requires": ["setuptools-scm"]},
+ "project": {"name": "test-package", "version": "1.0.0"},
+ "tool": {
+ "setuptools": {
+ "dynamic": {"version": {"attr": "test_package.__version__"}}
+ }
+ },
+ }
+ )
+
+ # Filter to check for the dynamic version warning specifically
+ relevant_warnings = [
+ w for w in warning_list if "tool.setuptools.dynamic" in str(w.message)
+ ]
+ assert len(relevant_warnings) == 0, (
+ "Should not warn about tool.setuptools.dynamic when only using file finder"
+ )
+ assert pyproject_data.project_version == "1.0.0"
+ assert not should_infer(pyproject_data)
diff --git a/testing/test_regressions.py b/setuptools-scm/testing_scm/test_regressions.py
similarity index 52%
rename from testing/test_regressions.py
rename to setuptools-scm/testing_scm/test_regressions.py
index 317f8579..ae6aa8eb 100644
--- a/testing/test_regressions.py
+++ b/setuptools-scm/testing_scm/test_regressions.py
@@ -1,22 +1,24 @@
+"""Setuptools-scm specific regression tests.
+
+Core VCS regression tests have been moved to vcs-versioning/testing_vcs/test_regressions.py
+"""
+
from __future__ import annotations
import pprint
import subprocess
import sys
-from dataclasses import replace
from importlib.metadata import EntryPoint
from importlib.metadata import distribution
from pathlib import Path
-from typing import Sequence
import pytest
-from setuptools_scm import Configuration
-from setuptools_scm._run_cmd import run
-from setuptools_scm.git import parse
+from vcs_versioning._run_cmd import run
+from vcs_versioning.test_api import WorkDir
+
from setuptools_scm.integration import data_from_mime
-from setuptools_scm.version import meta
def test_data_from_mime_ignores_body() -> None:
@@ -91,68 +93,10 @@ def vs(v):
assert res.stdout == "1.0"
-@pytest.mark.skipif(sys.platform != "win32", reason="this bug is only valid on windows")
-def test_case_mismatch_on_windows_git(tmp_path: Path) -> None:
- """Case insensitive path checks on Windows"""
- camel_case_path = tmp_path / "CapitalizedDir"
- camel_case_path.mkdir()
- run("git init", camel_case_path)
- res = parse(str(camel_case_path).lower(), Configuration())
- assert res is not None
-
-
-@pytest.mark.skipif(sys.platform != "win32", reason="this bug is only valid on windows")
-def test_case_mismatch_nested_dir_windows_git(tmp_path: Path) -> None:
- """Test case where we have a nested directory with different casing"""
- from testing.wd_wrapper import WorkDir
-
- # Create git repo in my_repo
- repo_path = tmp_path / "my_repo"
- repo_path.mkdir()
- wd = WorkDir(repo_path).setup_git()
-
- # Create a nested directory with specific casing
- nested_dir = repo_path / "CasedDir"
- nested_dir.mkdir()
-
- # Create a pyproject.toml in the nested directory
- wd.write(
- "CasedDir/pyproject.toml",
- """
-[build-system]
-requires = ["setuptools>=64", "setuptools-scm"]
-build-backend = "setuptools.build_meta"
-
-[project]
-name = "test-project"
-dynamic = ["version"]
-
-[tool.setuptools_scm]
-""",
- )
-
- # Add and commit the file
- wd.add_and_commit("Initial commit")
-
- # Now try to parse from the nested directory with lowercase path
- # This simulates: cd my_repo/caseddir (lowercase) when actual dir is CasedDir
- lowercase_nested_path = str(nested_dir).replace("CasedDir", "caseddir")
-
- # This should trigger the assertion error in _git_toplevel
- try:
- res = parse(lowercase_nested_path, Configuration())
- # If we get here without assertion error, the bug is already fixed or not triggered
- print(f"Parse succeeded with result: {res}")
- except AssertionError as e:
- print(f"AssertionError caught as expected: {e}")
- # Re-raise so the test fails, showing we reproduced the bug
- raise
-
-
def test_case_mismatch_force_assertion_failure(tmp_path: Path) -> None:
"""Force the assertion failure by directly calling _git_toplevel with mismatched paths"""
- from setuptools_scm._file_finders.git import _git_toplevel
- from testing.wd_wrapper import WorkDir
+
+ from vcs_versioning._file_finders._git import _git_toplevel
# Create git repo structure
repo_path = tmp_path / "my_repo"
@@ -192,36 +136,3 @@ def test_entrypoints_load() -> None:
failed.append((ep, e))
if failed:
pytest.fail(pprint.pformat(failed))
-
-
-def test_write_to_absolute_path_passes_when_subdir_of_root(tmp_path: Path) -> None:
- c = Configuration(root=tmp_path, write_to=tmp_path / "VERSION.py")
- v = meta("1.0", config=c)
- from setuptools_scm._get_version_impl import write_version_files
-
- with pytest.warns(DeprecationWarning, match=".*write_to=.* is a absolute.*"):
- write_version_files(c, "1.0", v)
- write_version_files(replace(c, write_to="VERSION.py"), "1.0", v)
- subdir = tmp_path / "subdir"
- subdir.mkdir()
- with pytest.raises(
- # todo: python version specific error list
- ValueError,
- match=r".*VERSION.py' .* .*subdir.*",
- ):
- write_version_files(replace(c, root=subdir), "1.0", v)
-
-
-@pytest.mark.parametrize(
- ("input", "expected"),
- [
- ("1.0", (1, 0)),
- ("1.0a2", (1, 0, "a2")),
- ("1.0.b2dev1", (1, 0, "b2", "dev1")),
- ("1.0.dev1", (1, 0, "dev1")),
- ],
-)
-def test_version_as_tuple(input: str, expected: Sequence[int | str]) -> None:
- from setuptools_scm._version_cls import _version_as_tuple
-
- assert _version_as_tuple(input) == expected
diff --git a/testing/test_version_inference.py b/setuptools-scm/testing_scm/test_version_inference.py
similarity index 89%
rename from testing/test_version_inference.py
rename to setuptools-scm/testing_scm/test_version_inference.py
index 967ab768..f216f214 100644
--- a/testing/test_version_inference.py
+++ b/setuptools-scm/testing_scm/test_version_inference.py
@@ -6,25 +6,37 @@
import pytest
from setuptools_scm._integration.pyproject_reading import PyProjectData
+from setuptools_scm._integration.version_inference import VersionAlreadySetWarning
from setuptools_scm._integration.version_inference import VersionInferenceConfig
from setuptools_scm._integration.version_inference import VersionInferenceNoOp
from setuptools_scm._integration.version_inference import VersionInferenceResult
-from setuptools_scm._integration.version_inference import VersionInferenceWarning
from setuptools_scm._integration.version_inference import get_version_inference_config
# Common test data
PYPROJECT = SimpleNamespace(
DEFAULT=PyProjectData.for_testing(
- is_required=True, section_present=True, project_present=True
+ tool_name="setuptools_scm",
+ is_required=True,
+ section_present=True,
+ project_present=True,
),
WITHOUT_TOOL_SECTION=PyProjectData.for_testing(
- is_required=True, section_present=False, project_present=True
+ tool_name="setuptools_scm",
+ is_required=True,
+ section_present=False,
+ project_present=True,
),
ONLY_REQUIRED=PyProjectData.for_testing(
- is_required=True, section_present=False, project_present=False
+ tool_name="setuptools_scm",
+ is_required=True,
+ section_present=False,
+ project_present=False,
),
WITHOUT_PROJECT=PyProjectData.for_testing(
- is_required=True, section_present=True, project_present=False
+ tool_name="setuptools_scm",
+ is_required=True,
+ section_present=True,
+ project_present=False,
),
)
@@ -36,12 +48,8 @@
)
-WARNING_PACKAGE = VersionInferenceWarning(
- message="version of test_package already set",
-)
-WARNING_NO_PACKAGE = VersionInferenceWarning(
- message="version of None already set",
-)
+WARNING_PACKAGE = VersionAlreadySetWarning(dist_name="test_package")
+WARNING_NO_PACKAGE = VersionAlreadySetWarning(dist_name=None)
NOOP = VersionInferenceNoOp()
@@ -53,7 +61,7 @@ def expect_config(
pyproject_data: PyProjectData = PYPROJECT.DEFAULT,
overrides: dict[str, Any] | None = None,
expected: type[VersionInferenceConfig]
- | VersionInferenceWarning
+ | VersionAlreadySetWarning
| VersionInferenceNoOp,
) -> None:
"""Helper to test get_version_inference_config and assert expected result type."""
@@ -73,7 +81,7 @@ def expect_config(
overrides=overrides,
)
else:
- assert isinstance(expected, (VersionInferenceNoOp, VersionInferenceWarning))
+ assert isinstance(expected, (VersionInferenceNoOp, VersionAlreadySetWarning))
expectation = expected
assert result == expectation
@@ -183,6 +191,7 @@ def test_tool_section_present(self) -> None:
def test_simple_extra_with_dynamic_version_infers(self) -> None:
"""We infer when setuptools-scm[simple] is in build-system.requires and version is dynamic."""
pyproject_data = PyProjectData.for_testing(
+ tool_name="setuptools_scm",
is_required=True,
section_present=False,
project_present=True,
@@ -198,6 +207,7 @@ def test_simple_extra_with_dynamic_version_infers(self) -> None:
def test_simple_extra_without_dynamic_version_no_infer(self) -> None:
"""We don't infer when setuptools-scm[simple] is present but version is not dynamic."""
pyproject_data = PyProjectData.for_testing(
+ tool_name="setuptools_scm",
is_required=True,
section_present=False,
project_present=True,
@@ -213,6 +223,7 @@ def test_simple_extra_without_dynamic_version_no_infer(self) -> None:
def test_no_simple_extra_with_dynamic_version_no_infer(self) -> None:
"""We don't infer when setuptools-scm (without simple extra) is present even with dynamic version."""
pyproject_data = PyProjectData.for_testing(
+ tool_name="setuptools_scm",
is_required=True,
section_present=False,
project_present=True,
@@ -228,6 +239,7 @@ def test_no_simple_extra_with_dynamic_version_no_infer(self) -> None:
def test_simple_extra_no_project_section_no_infer(self) -> None:
"""We don't infer when setuptools-scm[simple] is present but no project section."""
pyproject_data = PyProjectData.for_testing(
+ tool_name="setuptools_scm",
is_required=True,
section_present=False,
project_present=False,
@@ -242,6 +254,7 @@ def test_simple_extra_no_project_section_no_infer(self) -> None:
def test_simple_extra_with_version_warns(self) -> None:
"""We warn when setuptools-scm[simple] is present with dynamic version but version is already set."""
pyproject_data = PyProjectData.for_testing(
+ tool_name="setuptools_scm",
is_required=True,
section_present=False,
project_present=True,
diff --git a/tox.ini b/setuptools-scm/tox.ini
similarity index 100%
rename from tox.ini
rename to setuptools-scm/tox.ini
diff --git a/src/setuptools_scm/__init__.py b/src/setuptools_scm/__init__.py
deleted file mode 100644
index e265e859..00000000
--- a/src/setuptools_scm/__init__.py
+++ /dev/null
@@ -1,30 +0,0 @@
-"""
-:copyright: 2010-2023 by Ronny Pfannschmidt
-:license: MIT
-"""
-
-from __future__ import annotations
-
-from ._config import DEFAULT_LOCAL_SCHEME
-from ._config import DEFAULT_VERSION_SCHEME
-from ._config import Configuration
-from ._get_version_impl import _get_version
-from ._get_version_impl import get_version
-from ._integration.dump_version import dump_version # soft deprecated
-from ._version_cls import NonNormalizedVersion
-from ._version_cls import Version
-from .version import ScmVersion
-
-# Public API
-__all__ = [
- "DEFAULT_LOCAL_SCHEME",
- "DEFAULT_VERSION_SCHEME",
- "Configuration",
- "NonNormalizedVersion",
- "ScmVersion",
- "Version",
- "_get_version",
- "dump_version",
- # soft deprecated imports, left for backward compatibility
- "get_version",
-]
diff --git a/src/setuptools_scm/_cli.py b/src/setuptools_scm/_cli.py
deleted file mode 100644
index 6ed4a2e5..00000000
--- a/src/setuptools_scm/_cli.py
+++ /dev/null
@@ -1,295 +0,0 @@
-from __future__ import annotations
-
-import argparse
-import json
-import os
-import sys
-
-from pathlib import Path
-from typing import Any
-
-from setuptools_scm import Configuration
-from setuptools_scm._file_finders import find_files
-from setuptools_scm._get_version_impl import _get_version
-from setuptools_scm._integration.pyproject_reading import PyProjectData
-from setuptools_scm.discover import walk_potential_roots
-
-
-def main(
- args: list[str] | None = None, *, _given_pyproject_data: PyProjectData | None = None
-) -> int:
- opts = _get_cli_opts(args)
- inferred_root: str = opts.root or "."
-
- pyproject = opts.config or _find_pyproject(inferred_root)
-
- try:
- config = Configuration.from_file(
- pyproject,
- root=(os.path.abspath(opts.root) if opts.root is not None else None),
- pyproject_data=_given_pyproject_data,
- )
- except (LookupError, FileNotFoundError) as ex:
- # no pyproject.toml OR no [tool.setuptools_scm]
- print(
- f"Warning: could not use {os.path.relpath(pyproject)},"
- " using default configuration.\n"
- f" Reason: {ex}.",
- file=sys.stderr,
- )
- config = Configuration(root=inferred_root)
- version: str | None
- if opts.no_version:
- version = "0.0.0+no-version-was-requested.fake-version"
- else:
- version = _get_version(
- config, force_write_version_files=opts.force_write_version_files
- )
- if version is None:
- raise SystemExit("ERROR: no version found for", opts)
- if opts.strip_dev:
- version = version.partition(".dev")[0]
-
- return command(opts, version, config)
-
-
-def _get_cli_opts(args: list[str] | None) -> argparse.Namespace:
- prog = "python -m setuptools_scm"
- desc = "Print project version according to SCM metadata"
- parser = argparse.ArgumentParser(prog, description=desc)
- # By default, help for `--help` starts with lower case, so we keep the pattern:
- parser.add_argument(
- "-r",
- "--root",
- default=None,
- help='directory managed by the SCM, default: inferred from config file, or "."',
- )
- parser.add_argument(
- "-c",
- "--config",
- default=None,
- metavar="PATH",
- help="path to 'pyproject.toml' with setuptools-scm config, "
- "default: looked up in the current or parent directories",
- )
- parser.add_argument(
- "--strip-dev",
- action="store_true",
- help="remove the dev/local parts of the version before printing the version",
- )
- parser.add_argument(
- "-N",
- "--no-version",
- action="store_true",
- help="do not include package version in the output",
- )
- output_formats = ["json", "plain", "key-value"]
- parser.add_argument(
- "-f",
- "--format",
- type=str.casefold,
- default="plain",
- help="specify output format",
- choices=output_formats,
- )
- parser.add_argument(
- "-q",
- "--query",
- type=str.casefold,
- nargs="*",
- help="display setuptools-scm settings according to query, "
- "e.g. dist_name, do not supply an argument in order to "
- "print a list of valid queries.",
- )
- parser.add_argument(
- "--force-write-version-files",
- action="store_true",
- help="trigger to write the content of the version files\n"
- "its recommended to use normal/editable installation instead)",
- )
- sub = parser.add_subparsers(title="extra commands", dest="command", metavar="")
- # We avoid `metavar` to prevent printing repetitive information
- desc = "List information about the package, e.g. included files"
- sub.add_parser("ls", help=desc[0].lower() + desc[1:], description=desc)
-
- # Add create-archival-file subcommand
- archival_desc = "Create .git_archival.txt file for git archive support"
- archival_parser = sub.add_parser(
- "create-archival-file",
- help=archival_desc[0].lower() + archival_desc[1:],
- description=archival_desc,
- )
- archival_group = archival_parser.add_mutually_exclusive_group(required=True)
- archival_group.add_argument(
- "--stable",
- action="store_true",
- help="create stable archival file (recommended, no branch names)",
- )
- archival_group.add_argument(
- "--full",
- action="store_true",
- help="create full archival file with branch information (can cause instability)",
- )
- archival_parser.add_argument(
- "--force", action="store_true", help="overwrite existing .git_archival.txt file"
- )
- return parser.parse_args(args)
-
-
-# flake8: noqa: C901
-def command(opts: argparse.Namespace, version: str, config: Configuration) -> int:
- data: dict[str, Any] = {}
-
- if opts.command == "ls":
- opts.query = ["files"]
-
- if opts.command == "create-archival-file":
- return _create_archival_file(opts, config)
-
- if opts.query == []:
- opts.no_version = True
- sys.stderr.write("Available queries:\n\n")
- opts.query = ["queries"]
- data["queries"] = ["files", *config.__dataclass_fields__]
-
- if opts.query is None:
- opts.query = []
-
- if not opts.no_version:
- data["version"] = version
-
- if "files" in opts.query:
- data["files"] = find_files(config.root)
-
- for q in opts.query:
- if q in ["files", "queries", "version"]:
- continue
-
- try:
- if q.startswith("_"):
- raise AttributeError()
- data[q] = getattr(config, q)
- except AttributeError:
- sys.stderr.write(f"Error: unknown query: '{q}'\n")
- return 1
-
- if opts.format == "json":
- print(json.dumps(data, indent=2))
-
- if opts.format == "plain":
- _print_plain(data)
-
- if opts.format == "key-value":
- _print_key_value(data)
-
- return 0
-
-
-def _print_plain(data: dict[str, Any]) -> None:
- version = data.pop("version", None)
- if version:
- print(version)
- files = data.pop("files", [])
- for file_ in files:
- print(file_)
- queries = data.pop("queries", [])
- for query in queries:
- print(query)
- if data:
- print("\n".join(data.values()))
-
-
-def _print_key_value(data: dict[str, Any]) -> None:
- for key, value in data.items():
- if isinstance(value, str):
- print(f"{key} = {value}")
- else:
- str_value = "\n ".join(value)
- print(f"{key} = {str_value}")
-
-
-def _find_pyproject(parent: str) -> str:
- for directory in walk_potential_roots(os.path.abspath(parent)):
- pyproject = os.path.join(directory, "pyproject.toml")
- if os.path.isfile(pyproject):
- return pyproject
-
- return os.path.abspath(
- "pyproject.toml"
- ) # use default name to trigger the default errors
-
-
-def _create_archival_file(opts: argparse.Namespace, config: Configuration) -> int:
- """Create .git_archival.txt file with appropriate content."""
- archival_path = Path(config.root, ".git_archival.txt")
-
- # Check if file exists and force flag
- if archival_path.exists() and not opts.force:
- print(
- f"Error: {archival_path} already exists. Use --force to overwrite.",
- file=sys.stderr,
- )
- return 1
-
- if opts.stable:
- content = _get_stable_archival_content()
- print("Creating stable .git_archival.txt (recommended for releases)")
- elif opts.full:
- content = _get_full_archival_content()
- print("Creating full .git_archival.txt with branch information")
- print("WARNING: This can cause archive checksums to be unstable!")
-
- try:
- archival_path.write_text(content, encoding="utf-8")
- print(f"Created: {archival_path}")
-
- gitattributes_path = Path(config.root, ".gitattributes")
- needs_gitattributes = True
-
- if gitattributes_path.exists():
- # TODO: more nuanced check later
- gitattributes_content = gitattributes_path.read_text("utf-8")
- if (
- ".git_archival.txt" in gitattributes_content
- and "export-subst" in gitattributes_content
- ):
- needs_gitattributes = False
-
- if needs_gitattributes:
- print("\nNext steps:")
- print("1. Add this line to .gitattributes:")
- print(" .git_archival.txt export-subst")
- print("2. Commit both files:")
- print(" git add .git_archival.txt .gitattributes")
- print(" git commit -m 'add git archive support'")
- else:
- print("\nNext step:")
- print("Commit the archival file:")
- print(" git add .git_archival.txt")
- print(" git commit -m 'update git archival file'")
-
- return 0
- except OSError as e:
- print(f"Error: Could not create {archival_path}: {e}", file=sys.stderr)
- return 1
-
-
-def _get_stable_archival_content() -> str:
- """Generate stable archival file content (no branch names)."""
- return """\
-node: $Format:%H$
-node-date: $Format:%cI$
-describe-name: $Format:%(describe:tags=true,match=*[0-9]*)$
-"""
-
-
-def _get_full_archival_content() -> str:
- """Generate full archival file content with branch information."""
- return """\
-# WARNING: Including ref-names can make archive checksums unstable
-# after commits are added post-release. Use only if describe-name is insufficient.
-node: $Format:%H$
-node-date: $Format:%cI$
-describe-name: $Format:%(describe:tags=true,match=*[0-9]*)$
-ref-names: $Format:%D$
-"""
diff --git a/src/setuptools_scm/_file_finders/pathtools.py b/src/setuptools_scm/_file_finders/pathtools.py
deleted file mode 100644
index 6de85089..00000000
--- a/src/setuptools_scm/_file_finders/pathtools.py
+++ /dev/null
@@ -1,9 +0,0 @@
-from __future__ import annotations
-
-import os
-
-from setuptools_scm import _types as _t
-
-
-def norm_real(path: _t.PathT) -> str:
- return os.path.normcase(os.path.realpath(path))
diff --git a/src/setuptools_scm/_integration/toml.py b/src/setuptools_scm/_integration/toml.py
deleted file mode 100644
index 2253287c..00000000
--- a/src/setuptools_scm/_integration/toml.py
+++ /dev/null
@@ -1,69 +0,0 @@
-from __future__ import annotations
-
-import sys
-
-from pathlib import Path
-from typing import TYPE_CHECKING
-from typing import Any
-from typing import Callable
-from typing import Dict
-from typing import TypedDict
-from typing import cast
-
-if sys.version_info >= (3, 11):
- from tomllib import loads as load_toml
-else:
- from tomli import loads as load_toml
-
-if TYPE_CHECKING:
- if sys.version_info >= (3, 10):
- from typing import TypeAlias
- else:
- from typing_extensions import TypeAlias
-
-from .. import _log
-
-log = _log.log.getChild("toml")
-
-TOML_RESULT: TypeAlias = Dict[str, Any]
-TOML_LOADER: TypeAlias = Callable[[str], TOML_RESULT]
-
-
-class InvalidTomlError(ValueError):
- """Raised when TOML data cannot be parsed."""
-
-
-def read_toml_content(path: Path, default: TOML_RESULT | None = None) -> TOML_RESULT:
- try:
- data = path.read_text(encoding="utf-8")
- except FileNotFoundError:
- if default is None:
- raise
- else:
- log.debug("%s missing, presuming default %r", path, default)
- return default
- else:
- try:
- return load_toml(data)
- except Exception as e: # tomllib/tomli raise different decode errors
- raise InvalidTomlError(f"Invalid TOML in {path}") from e
-
-
-class _CheatTomlData(TypedDict):
- cheat: dict[str, Any]
-
-
-def load_toml_or_inline_map(data: str | None) -> dict[str, Any]:
- """
- load toml data - with a special hack if only a inline map is given
- """
- if not data:
- return {}
- try:
- if data[0] == "{":
- data = "cheat=" + data
- loaded: _CheatTomlData = cast(_CheatTomlData, load_toml(data))
- return loaded["cheat"]
- return load_toml(data)
- except Exception as e: # tomllib/tomli raise different decode errors
- raise InvalidTomlError("Invalid TOML content") from e
diff --git a/src/setuptools_scm/_log.py b/src/setuptools_scm/_log.py
deleted file mode 100644
index ea17f375..00000000
--- a/src/setuptools_scm/_log.py
+++ /dev/null
@@ -1,87 +0,0 @@
-"""
-logging helpers, supports vendoring
-"""
-
-from __future__ import annotations
-
-import contextlib
-import logging
-import os
-import sys
-
-from typing import IO
-from typing import Iterator
-from typing import Mapping
-
-log = logging.getLogger(__name__.rsplit(".", 1)[0])
-log.propagate = False
-
-
-class AlwaysStdErrHandler(logging.StreamHandler): # type: ignore[type-arg]
- def __init__(self) -> None:
- super().__init__(sys.stderr)
-
- @property
- def stream(self) -> IO[str]:
- return sys.stderr
-
- @stream.setter
- def stream(self, value: IO[str]) -> None:
- assert value is sys.stderr
-
-
-def make_default_handler() -> logging.Handler:
- try:
- from rich.console import Console
-
- console = Console(stderr=True)
- from rich.logging import RichHandler
-
- return RichHandler(console=console)
- except ImportError:
- last_resort = logging.lastResort
- assert last_resort is not None
- return last_resort
-
-
-_default_handler = make_default_handler()
-
-log.addHandler(_default_handler)
-
-
-def _default_log_level(_env: Mapping[str, str] = os.environ) -> int:
- val: str | None = _env.get("SETUPTOOLS_SCM_DEBUG")
- return logging.WARNING if val is None else logging.DEBUG
-
-
-log.setLevel(_default_log_level())
-
-
-@contextlib.contextmanager
-def defer_to_pytest() -> Iterator[None]:
- log.propagate = True
- old_level = log.level
- log.setLevel(logging.NOTSET)
- log.removeHandler(_default_handler)
- try:
- yield
- finally:
- log.addHandler(_default_handler)
- log.propagate = False
- log.setLevel(old_level)
-
-
-@contextlib.contextmanager
-def enable_debug(handler: logging.Handler = _default_handler) -> Iterator[None]:
- log.addHandler(handler)
- old_level = log.level
- log.setLevel(logging.DEBUG)
- old_handler_level = handler.level
- handler.setLevel(logging.DEBUG)
- try:
- yield
- finally:
- log.setLevel(old_level)
- handler.setLevel(old_handler_level)
- if handler is not _default_handler:
- log.removeHandler(handler)
diff --git a/src/setuptools_scm/_overrides.py b/src/setuptools_scm/_overrides.py
deleted file mode 100644
index 4e06b7a7..00000000
--- a/src/setuptools_scm/_overrides.py
+++ /dev/null
@@ -1,298 +0,0 @@
-from __future__ import annotations
-
-import dataclasses
-import os
-
-from difflib import get_close_matches
-from typing import Any
-from typing import Mapping
-
-from packaging.utils import canonicalize_name
-
-from . import _config
-from . import _log
-from . import version
-from ._integration.toml import load_toml_or_inline_map
-
-log = _log.log.getChild("overrides")
-
-PRETEND_KEY = "SETUPTOOLS_SCM_PRETEND_VERSION"
-PRETEND_KEY_NAMED = PRETEND_KEY + "_FOR_{name}"
-PRETEND_METADATA_KEY = "SETUPTOOLS_SCM_PRETEND_METADATA"
-PRETEND_METADATA_KEY_NAMED = PRETEND_METADATA_KEY + "_FOR_{name}"
-
-
-def _search_env_vars_with_prefix(
- prefix: str, dist_name: str, env: Mapping[str, str]
-) -> list[tuple[str, str]]:
- """Search environment variables with a given prefix for potential dist name matches.
-
- Args:
- prefix: The environment variable prefix (e.g., "SETUPTOOLS_SCM_PRETEND_VERSION_FOR_")
- dist_name: The original dist name to match against
- env: Environment dictionary to search in
-
- Returns:
- List of (env_var_name, env_var_value) tuples for potential matches
- """
- # Get the canonical name for comparison
- canonical_dist_name = canonicalize_name(dist_name)
-
- matches = []
- for env_var, value in env.items():
- if env_var.startswith(prefix):
- suffix = env_var[len(prefix) :]
- # Normalize the suffix and compare to canonical dist name
- try:
- normalized_suffix = canonicalize_name(suffix.lower().replace("_", "-"))
- if normalized_suffix == canonical_dist_name:
- matches.append((env_var, value))
- except Exception:
- # If normalization fails for any reason, skip this env var
- continue
-
- return matches
-
-
-def _find_close_env_var_matches(
- prefix: str, expected_suffix: str, env: Mapping[str, str], threshold: float = 0.6
-) -> list[str]:
- """Find environment variables with similar suffixes that might be typos.
-
- Args:
- prefix: The environment variable prefix
- expected_suffix: The expected suffix (canonicalized dist name in env var format)
- env: Environment dictionary to search in
- threshold: Similarity threshold for matches (0.0 to 1.0)
-
- Returns:
- List of environment variable names that are close matches
- """
- candidates = []
- for env_var in env:
- if env_var.startswith(prefix):
- suffix = env_var[len(prefix) :]
- candidates.append(suffix)
-
- # Use difflib to find close matches
- close_matches = get_close_matches(
- expected_suffix, candidates, n=3, cutoff=threshold
- )
-
- return [f"{prefix}{match}" for match in close_matches if match != expected_suffix]
-
-
-def read_named_env(
- *,
- tool: str = "SETUPTOOLS_SCM",
- name: str,
- dist_name: str | None,
- env: Mapping[str, str] = os.environ,
-) -> str | None:
- """Read a named environment variable, with fallback search for dist-specific variants.
-
- This function first tries the standard normalized environment variable name.
- If that's not found and a dist_name is provided, it searches for alternative
- normalizations and warns about potential issues.
-
- Args:
- tool: The tool prefix (default: "SETUPTOOLS_SCM")
- name: The environment variable name component
- dist_name: The distribution name for dist-specific variables
- env: Environment dictionary to search in (defaults to os.environ)
-
- Returns:
- The environment variable value if found, None otherwise
- """
-
- # First try the generic version
- generic_val = env.get(f"{tool}_{name}")
-
- if dist_name is not None:
- # Normalize the dist name using packaging.utils.canonicalize_name
- canonical_dist_name = canonicalize_name(dist_name)
- env_var_dist_name = canonical_dist_name.replace("-", "_").upper()
- expected_env_var = f"{tool}_{name}_FOR_{env_var_dist_name}"
-
- # Try the standard normalized name first
- val = env.get(expected_env_var)
- if val is not None:
- return val
-
- # If not found, search for alternative normalizations
- prefix = f"{tool}_{name}_FOR_"
- alternative_matches = _search_env_vars_with_prefix(prefix, dist_name, env)
-
- if alternative_matches:
- # Found alternative matches - use the first one but warn
- env_var, value = alternative_matches[0]
- log.warning(
- "Found environment variable '%s' for dist name '%s', "
- "but expected '%s'. Consider using the standard normalized name.",
- env_var,
- dist_name,
- expected_env_var,
- )
- if len(alternative_matches) > 1:
- other_vars = [var for var, _ in alternative_matches[1:]]
- log.warning(
- "Multiple alternative environment variables found: %s. Using '%s'.",
- other_vars,
- env_var,
- )
- return value
-
- # No exact or alternative matches found - look for potential typos
- close_matches = _find_close_env_var_matches(prefix, env_var_dist_name, env)
- if close_matches:
- log.warning(
- "Environment variable '%s' not found for dist name '%s' "
- "(canonicalized as '%s'). Did you mean one of these? %s",
- expected_env_var,
- dist_name,
- canonical_dist_name,
- close_matches,
- )
-
- return generic_val
-
-
-def _read_pretended_metadata_for(
- config: _config.Configuration,
-) -> dict[str, Any] | None:
- """read overridden metadata from the environment
-
- tries ``SETUPTOOLS_SCM_PRETEND_METADATA``
- and ``SETUPTOOLS_SCM_PRETEND_METADATA_FOR_$UPPERCASE_DIST_NAME``
-
- Returns a dictionary with metadata field overrides like:
- {"node": "g1337beef", "distance": 4}
- """
- log.debug("dist name: %s", config.dist_name)
-
- pretended = read_named_env(name="PRETEND_METADATA", dist_name=config.dist_name)
-
- if pretended:
- try:
- metadata_overrides = load_toml_or_inline_map(pretended)
- # Validate that only known ScmVersion fields are provided
- valid_fields = {
- "tag",
- "distance",
- "node",
- "dirty",
- "preformatted",
- "branch",
- "node_date",
- "time",
- }
- invalid_fields = set(metadata_overrides.keys()) - valid_fields
- if invalid_fields:
- log.warning(
- "Invalid metadata fields in pretend metadata: %s. "
- "Valid fields are: %s",
- invalid_fields,
- valid_fields,
- )
- # Remove invalid fields but continue processing
- for field in invalid_fields:
- metadata_overrides.pop(field)
-
- return metadata_overrides or None
- except Exception as e:
- log.error("Failed to parse pretend metadata: %s", e)
- return None
- else:
- return None
-
-
-def _apply_metadata_overrides(
- scm_version: version.ScmVersion | None,
- config: _config.Configuration,
-) -> version.ScmVersion | None:
- """Apply metadata overrides to a ScmVersion object.
-
- This function reads pretend metadata from environment variables and applies
- the overrides to the given ScmVersion. TOML type coercion is used so values
- should be provided in their correct types (int, bool, datetime, etc.).
-
- Args:
- scm_version: The ScmVersion to apply overrides to, or None
- config: Configuration object
-
- Returns:
- Modified ScmVersion with overrides applied, or None
- """
- metadata_overrides = _read_pretended_metadata_for(config)
-
- if not metadata_overrides:
- return scm_version
-
- if scm_version is None:
- log.warning(
- "PRETEND_METADATA specified but no base version found. "
- "Metadata overrides cannot be applied without a base version."
- )
- return None
-
- log.info("Applying metadata overrides: %s", metadata_overrides)
-
- # Define type checks and field mappings
- from datetime import date
- from datetime import datetime
-
- field_specs: dict[str, tuple[type | tuple[type, type], str]] = {
- "distance": (int, "int"),
- "dirty": (bool, "bool"),
- "preformatted": (bool, "bool"),
- "node_date": (date, "date"),
- "time": (datetime, "datetime"),
- "node": ((str, type(None)), "str or None"),
- "branch": ((str, type(None)), "str or None"),
- # tag is special - can be multiple types, handled separately
- }
-
- # Apply each override individually using dataclasses.replace for type safety
- result = scm_version
-
- for field, value in metadata_overrides.items():
- if field in field_specs:
- expected_type, type_name = field_specs[field]
- assert isinstance(value, expected_type), (
- f"{field} must be {type_name}, got {type(value).__name__}: {value!r}"
- )
- result = dataclasses.replace(result, **{field: value})
- elif field == "tag":
- # tag can be Version, NonNormalizedVersion, or str - we'll let the assignment handle validation
- result = dataclasses.replace(result, tag=value)
- else:
- # This shouldn't happen due to validation in _read_pretended_metadata_for
- log.warning("Unknown field '%s' in metadata overrides", field)
-
- # Ensure config is preserved (should not be overridden)
- assert result.config is config, "Config must be preserved during metadata overrides"
-
- return result
-
-
-def _read_pretended_version_for(
- config: _config.Configuration,
-) -> version.ScmVersion | None:
- """read a a overridden version from the environment
-
- tries ``SETUPTOOLS_SCM_PRETEND_VERSION``
- and ``SETUPTOOLS_SCM_PRETEND_VERSION_FOR_$UPPERCASE_DIST_NAME``
- """
- log.debug("dist name: %s", config.dist_name)
-
- pretended = read_named_env(name="PRETEND_VERSION", dist_name=config.dist_name)
-
- if pretended:
- return version.meta(tag=pretended, preformatted=True, config=config)
- else:
- return None
-
-
-def read_toml_overrides(dist_name: str | None) -> dict[str, Any]:
- data = read_named_env(name="OVERRIDES", dist_name=dist_name)
- return load_toml_or_inline_map(data)
diff --git a/src/setuptools_scm/_types.py b/src/setuptools_scm/_types.py
deleted file mode 100644
index 4f8874fb..00000000
--- a/src/setuptools_scm/_types.py
+++ /dev/null
@@ -1,61 +0,0 @@
-from __future__ import annotations
-
-import os
-
-from typing import TYPE_CHECKING
-from typing import Callable
-from typing import List
-from typing import Protocol
-from typing import Sequence
-from typing import Tuple
-from typing import Union
-
-from setuptools import Distribution
-
-if TYPE_CHECKING:
- import sys
-
- if sys.version_info >= (3, 10):
- from typing import TypeAlias
- else:
- from typing_extensions import TypeAlias
-
- from . import version
- from ._integration.pyproject_reading import PyProjectData
- from ._integration.toml import InvalidTomlError
-
-PathT: TypeAlias = Union["os.PathLike[str]", str]
-
-CMD_TYPE: TypeAlias = Union[Sequence[PathT], str]
-
-VERSION_SCHEME: TypeAlias = Union[str, Callable[["version.ScmVersion"], str]]
-VERSION_SCHEMES: TypeAlias = Union[List[str], Tuple[str, ...], VERSION_SCHEME]
-SCMVERSION: TypeAlias = "version.ScmVersion"
-
-# Git pre-parse function types
-GIT_PRE_PARSE: TypeAlias = Union[str, None]
-
-# Testing injection types for configuration reading
-GivenPyProjectResult: TypeAlias = Union[
- "PyProjectData", "InvalidTomlError", FileNotFoundError, None
-]
-
-
-class VersionInferenceApplicable(Protocol):
- """A result object from version inference decision that can be applied to a dist."""
-
- def apply(self, dist: Distribution) -> None: # pragma: no cover - structural type
- ...
-
-
-class GetVersionInferenceConfig(Protocol):
- """Callable protocol for the decision function used by integration points."""
-
- def __call__(
- self,
- dist_name: str | None,
- current_version: str | None,
- pyproject_data: PyProjectData,
- overrides: dict[str, object] | None = None,
- ) -> VersionInferenceApplicable: # pragma: no cover - structural type
- ...
diff --git a/src/setuptools_scm/version.py b/src/setuptools_scm/version.py
deleted file mode 100644
index b35ba919..00000000
--- a/src/setuptools_scm/version.py
+++ /dev/null
@@ -1,686 +0,0 @@
-from __future__ import annotations
-
-import dataclasses
-import logging
-import os
-import re
-import warnings
-
-from datetime import date
-from datetime import datetime
-from datetime import timezone
-from typing import TYPE_CHECKING
-from typing import Any
-from typing import Callable
-from typing import Match
-
-from . import _entrypoints
-from . import _modify_version
-from ._node_utils import _format_node_for_output
-
-if TYPE_CHECKING:
- import sys
-
- if sys.version_info >= (3, 10):
- from typing import Concatenate
- from typing import ParamSpec
- else:
- from typing_extensions import Concatenate
- from typing_extensions import ParamSpec
-
- if sys.version_info >= (3, 11):
- from typing import Unpack
- else:
- from typing_extensions import Unpack
-
- _P = ParamSpec("_P")
-
-from typing import TypedDict
-
-from . import _config
-from . import _version_cls as _v
-from ._version_cls import Version as PkgVersion
-from ._version_cls import _VersionT
-
-log = logging.getLogger(__name__)
-
-
-SEMVER_MINOR = 2
-SEMVER_PATCH = 3
-SEMVER_LEN = 3
-
-
-class _TagDict(TypedDict):
- version: str
- prefix: str
- suffix: str
-
-
-class VersionExpectations(TypedDict, total=False):
- """Expected properties for ScmVersion matching."""
-
- tag: str | _VersionT
- distance: int
- dirty: bool
- node_prefix: str # Prefix of the node/commit hash
- branch: str | None
- exact: bool
- preformatted: bool
- node_date: date | None
- time: datetime | None
-
-
-@dataclasses.dataclass
-class mismatches:
- """Represents mismatches between expected and actual ScmVersion properties."""
-
- expected: dict[str, Any]
- actual: dict[str, Any]
-
- def __bool__(self) -> bool:
- """mismatches is falsy to allow `if not version.matches(...)`."""
- return False
-
- def __str__(self) -> str:
- """Format mismatches for error reporting."""
- lines = []
- for key, exp_val in self.expected.items():
- if key == "node_prefix":
- # Special handling for node prefix matching
- actual_node = self.actual.get("node")
- if not actual_node or not actual_node.startswith(exp_val):
- lines.append(
- f" node: expected prefix '{exp_val}', got '{actual_node}'"
- )
- else:
- act_val = self.actual.get(key)
- if str(exp_val) != str(act_val):
- lines.append(f" {key}: expected {exp_val!r}, got {act_val!r}")
- return "\n".join(lines)
-
- def __repr__(self) -> str:
- return f"mismatches(expected={self.expected!r}, actual={self.actual!r})"
-
-
-def _parse_version_tag(
- tag: str | object, config: _config.Configuration
-) -> _TagDict | None:
- match = config.tag_regex.match(str(tag))
-
- if match:
- key: str | int = 1 if len(match.groups()) == 1 else "version"
- full = match.group(0)
- log.debug("%r %r %s", tag, config.tag_regex, match)
- log.debug(
- "key %s data %s, %s, %r", key, match.groupdict(), match.groups(), full
- )
-
- if version := match.group(key):
- result = _TagDict(
- version=version,
- prefix=full[: match.start(key)],
- suffix=full[match.end(key) :],
- )
-
- log.debug("tag %r parsed to %r", tag, result)
- return result
-
- raise ValueError(
- f'The tag_regex "{config.tag_regex.pattern}" matched tag "{tag}", '
- "however the matched group has no value."
- )
- else:
- log.debug("tag %r did not parse", tag)
-
- return None
-
-
-def callable_or_entrypoint(group: str, callable_or_name: str | Any) -> Any:
- log.debug("ep %r %r", group, callable_or_name)
-
- if callable(callable_or_name):
- return callable_or_name
-
- from ._entrypoints import _get_ep
-
- return _get_ep(group, callable_or_name)
-
-
-def tag_to_version(
- tag: _VersionT | str, config: _config.Configuration
-) -> _VersionT | None:
- """
- take a tag that might be prefixed with a keyword and return only the version part
- """
- log.debug("tag %s", tag)
-
- tag_dict = _parse_version_tag(tag, config)
- if tag_dict is None or not tag_dict.get("version", None):
- warnings.warn(f"tag {tag!r} no version found")
- return None
-
- version_str = tag_dict["version"]
- log.debug("version pre parse %s", version_str)
-
- # Try to create version from base version first
- try:
- version: _VersionT = config.version_cls(version_str)
- log.debug("version=%r", version)
- except Exception:
- warnings.warn(
- f"tag {tag!r} will be stripped of its suffix {tag_dict.get('suffix', '')!r}"
- )
- # Fall back to trying without any suffix
- version = config.version_cls(version_str)
- log.debug("version=%r", version)
- return version
-
- # If base version is valid, check if we can preserve the suffix
- if suffix := tag_dict.get("suffix", ""):
- log.debug("tag %r includes local build data %r, preserving it", tag, suffix)
- # Try creating version with suffix - if it fails, we'll use the base version
- try:
- version_with_suffix = config.version_cls(version_str + suffix)
- log.debug("version with suffix=%r", version_with_suffix)
- return version_with_suffix
- except Exception:
- warnings.warn(f"tag {tag!r} will be stripped of its suffix {suffix!r}")
- # Return the base version without suffix
- return version
-
- return version
-
-
-def _source_epoch_or_utc_now() -> datetime:
- if "SOURCE_DATE_EPOCH" in os.environ:
- date_epoch = int(os.environ["SOURCE_DATE_EPOCH"])
- return datetime.fromtimestamp(date_epoch, timezone.utc)
- else:
- return datetime.now(timezone.utc)
-
-
-@dataclasses.dataclass
-class ScmVersion:
- """represents a parsed version from scm"""
-
- tag: _v.Version | _v.NonNormalizedVersion
- """the related tag or preformatted version"""
- config: _config.Configuration
- """the configuration used to parse the version"""
- distance: int = 0
- """the number of commits since the tag"""
- node: str | None = None
- """the shortened node id"""
- dirty: bool = False
- """whether the working copy had uncommitted changes"""
- preformatted: bool = False
- """whether the version string was preformatted"""
- branch: str | None = None
- """the branch name if any"""
- node_date: date | None = None
- """the date of the commit if available"""
- time: datetime = dataclasses.field(default_factory=_source_epoch_or_utc_now)
- """the current time or source epoch time
- only set for unit-testing version schemes
- for real usage it must be `now(utc)` or `SOURCE_EPOCH`
- """
-
- @property
- def exact(self) -> bool:
- """returns true checked out exactly on a tag and no local changes apply"""
- return self.distance == 0 and not self.dirty
-
- @property
- def short_node(self) -> str | None:
- """Return the node formatted for output."""
- return _format_node_for_output(self.node)
-
- def __repr__(self) -> str:
- return (
- f""
- )
-
- def format_with(self, fmt: str, **kw: object) -> str:
- """format a given format string with attributes of this object"""
- return fmt.format(
- time=self.time,
- tag=self.tag,
- distance=self.distance,
- node=_format_node_for_output(self.node),
- dirty=self.dirty,
- branch=self.branch,
- node_date=self.node_date,
- **kw,
- )
-
- def format_choice(self, clean_format: str, dirty_format: str, **kw: object) -> str:
- """given `clean_format` and `dirty_format`
-
- choose one based on `self.dirty` and format it using `self.format_with`"""
-
- return self.format_with(dirty_format if self.dirty else clean_format, **kw)
-
- def format_next_version(
- self,
- guess_next: Callable[Concatenate[ScmVersion, _P], str],
- fmt: str = "{guessed}.dev{distance}",
- *k: _P.args,
- **kw: _P.kwargs,
- ) -> str:
- guessed = guess_next(self, *k, **kw)
- return self.format_with(fmt, guessed=guessed)
-
- def matches(self, **expectations: Unpack[VersionExpectations]) -> bool | mismatches:
- """Check if this ScmVersion matches the given expectations.
-
- Returns True if all specified properties match, or a mismatches
- object (which is falsy) containing details of what didn't match.
-
- Args:
- **expectations: Properties to check, using VersionExpectations TypedDict
- """
- # Map expectation keys to ScmVersion attributes
- attr_map: dict[str, Callable[[], Any]] = {
- "tag": lambda: str(self.tag),
- "node_prefix": lambda: self.node,
- "distance": lambda: self.distance,
- "dirty": lambda: self.dirty,
- "branch": lambda: self.branch,
- "exact": lambda: self.exact,
- "preformatted": lambda: self.preformatted,
- "node_date": lambda: self.node_date,
- "time": lambda: self.time,
- }
-
- # Build actual values dict
- actual: dict[str, Any] = {
- key: attr_map[key]() for key in expectations if key in attr_map
- }
-
- # Process expectations
- expected = {
- "tag" if k == "tag" else k: str(v) if k == "tag" else v
- for k, v in expectations.items()
- }
-
- # Check for mismatches
- def has_mismatch() -> bool:
- for key, exp_val in expected.items():
- if key == "node_prefix":
- act_val = actual.get("node_prefix")
- if not act_val or not act_val.startswith(exp_val):
- return True
- else:
- if str(exp_val) != str(actual.get(key)):
- return True
- return False
-
- if has_mismatch():
- # Rename node_prefix back to node for actual values in mismatch reporting
- if "node_prefix" in actual:
- actual["node"] = actual.pop("node_prefix")
- return mismatches(expected=expected, actual=actual)
- return True
-
-
-def _parse_tag(
- tag: _VersionT | str, preformatted: bool, config: _config.Configuration
-) -> _VersionT:
- if preformatted:
- # For preformatted versions, tag should already be validated as a version object
- # String validation is handled in meta function before calling this
- if isinstance(tag, str):
- # This should not happen with enhanced meta, but kept for safety
- return _v.NonNormalizedVersion(tag)
- else:
- # Already a version object (including test mocks), return as-is
- return tag
- elif not isinstance(tag, config.version_cls):
- version = tag_to_version(tag, config)
- assert version is not None
- return version
- else:
- return tag
-
-
-def meta(
- tag: str | _VersionT,
- *,
- distance: int = 0,
- dirty: bool = False,
- node: str | None = None,
- preformatted: bool = False,
- branch: str | None = None,
- config: _config.Configuration,
- node_date: date | None = None,
- time: datetime | None = None,
-) -> ScmVersion:
- parsed_version: _VersionT
- # Enhanced string validation for preformatted versions
- if preformatted and isinstance(tag, str):
- # Validate PEP 440 compliance using NonNormalizedVersion
- # Let validation errors bubble up to the caller
- parsed_version = _v.NonNormalizedVersion(tag)
- else:
- # Use existing _parse_tag logic for non-preformatted or already validated inputs
- parsed_version = _parse_tag(tag, preformatted, config)
-
- log.info("version %s -> %s", tag, parsed_version)
- assert parsed_version is not None, f"Can't parse version {tag}"
- scm_version = ScmVersion(
- parsed_version,
- distance=distance,
- node=node,
- dirty=dirty,
- preformatted=preformatted,
- branch=branch,
- config=config,
- node_date=node_date,
- )
- if time is not None:
- scm_version = dataclasses.replace(scm_version, time=time)
- return scm_version
-
-
-def guess_next_version(tag_version: ScmVersion) -> str:
- version = _modify_version.strip_local(str(tag_version.tag))
- return _modify_version._bump_dev(version) or _modify_version._bump_regex(version)
-
-
-def guess_next_dev_version(version: ScmVersion) -> str:
- if version.exact:
- return version.format_with("{tag}")
- else:
- return version.format_next_version(guess_next_version)
-
-
-def guess_next_simple_semver(
- version: ScmVersion, retain: int, increment: bool = True
-) -> str:
- if isinstance(version.tag, _v.Version):
- parts = list(version.tag.release[:retain])
- else:
- try:
- parts = [int(i) for i in str(version.tag).split(".")[:retain]]
- except ValueError:
- raise ValueError(f"{version} can't be parsed as numeric version") from None
- while len(parts) < retain:
- parts.append(0)
- if increment:
- parts[-1] += 1
- while len(parts) < SEMVER_LEN:
- parts.append(0)
- return ".".join(str(i) for i in parts)
-
-
-def simplified_semver_version(version: ScmVersion) -> str:
- if version.exact:
- return guess_next_simple_semver(version, retain=SEMVER_LEN, increment=False)
- elif version.branch is not None and "feature" in version.branch:
- return version.format_next_version(
- guess_next_simple_semver, retain=SEMVER_MINOR
- )
- else:
- return version.format_next_version(
- guess_next_simple_semver, retain=SEMVER_PATCH
- )
-
-
-def release_branch_semver_version(version: ScmVersion) -> str:
- if version.exact:
- return version.format_with("{tag}")
- if version.branch is not None:
- # Does the branch name (stripped of namespace) parse as a version?
- branch_ver_data = _parse_version_tag(
- version.branch.split("/")[-1], version.config
- )
- if branch_ver_data is not None:
- branch_ver = branch_ver_data["version"]
- if branch_ver[0] == "v":
- # Allow branches that start with 'v', similar to Version.
- branch_ver = branch_ver[1:]
- # Does the branch version up to the minor part match the tag? If not it
- # might be like, an issue number or something and not a version number, so
- # we only want to use it if it matches.
- tag_ver_up_to_minor = str(version.tag).split(".")[:SEMVER_MINOR]
- branch_ver_up_to_minor = branch_ver.split(".")[:SEMVER_MINOR]
- if branch_ver_up_to_minor == tag_ver_up_to_minor:
- # We're in a release/maintenance branch, next is a patch/rc/beta bump:
- return version.format_next_version(guess_next_version)
- # We're in a development branch, next is a minor bump:
- return version.format_next_version(guess_next_simple_semver, retain=SEMVER_MINOR)
-
-
-def release_branch_semver(version: ScmVersion) -> str:
- warnings.warn(
- "release_branch_semver is deprecated and will be removed in the future. "
- "Use release_branch_semver_version instead",
- category=DeprecationWarning,
- stacklevel=2,
- )
- return release_branch_semver_version(version)
-
-
-def only_version(version: ScmVersion) -> str:
- return version.format_with("{tag}")
-
-
-def no_guess_dev_version(version: ScmVersion) -> str:
- if version.exact:
- return version.format_with("{tag}")
- else:
- return version.format_next_version(_modify_version._dont_guess_next_version)
-
-
-_DATE_REGEX = re.compile(
- r"""
- ^(?P
- (?P[vV]?)
- (?P\d{2}|\d{4})(?:\.\d{1,2}){2})
- (?:\.(?P\d*))?$
- """,
- re.VERBOSE,
-)
-
-
-def date_ver_match(ver: str) -> Match[str] | None:
- return _DATE_REGEX.match(ver)
-
-
-def guess_next_date_ver(
- version: ScmVersion,
- node_date: date | None = None,
- date_fmt: str | None = None,
- version_cls: type | None = None,
-) -> str:
- """
- same-day -> patch +1
- other-day -> today
-
- distance is always added as .devX
- """
- match = date_ver_match(str(version.tag))
- if match is None:
- warnings.warn(
- f"{version} does not correspond to a valid versioning date, "
- "assuming legacy version"
- )
- if date_fmt is None:
- date_fmt = "%y.%m.%d"
- else:
- # deduct date format if not provided
- if date_fmt is None:
- date_fmt = "%Y.%m.%d" if len(match.group("year")) == 4 else "%y.%m.%d"
- if prefix := match.group("prefix"):
- if not date_fmt.startswith(prefix):
- date_fmt = prefix + date_fmt
-
- today = version.time.date()
- head_date = node_date or today
- # compute patch
- if match is None:
- # For legacy non-date tags, always use patch=0 (treat as "other day")
- # Use yesterday to ensure tag_date != head_date
- from datetime import timedelta
-
- tag_date = head_date - timedelta(days=1)
- else:
- tag_date = (
- datetime.strptime(match.group("date"), date_fmt)
- .replace(tzinfo=timezone.utc)
- .date()
- )
- if tag_date == head_date:
- assert match is not None
- # Same day as existing date tag - increment patch
- patch = int(match.group("patch") or "0") + 1
- else:
- # Different day or legacy non-date tag - use patch 0
- if tag_date > head_date and match is not None:
- # warn on future times (only for actual date tags, not legacy)
- warnings.warn(
- f"your previous tag ({tag_date}) is ahead your node date ({head_date})"
- )
- patch = 0
- next_version = "{node_date:{date_fmt}}.{patch}".format(
- node_date=head_date, date_fmt=date_fmt, patch=patch
- )
- # rely on the Version object to ensure consistency (e.g. remove leading 0s)
- if version_cls is None:
- version_cls = PkgVersion
- next_version = str(version_cls(next_version))
- return next_version
-
-
-def calver_by_date(version: ScmVersion) -> str:
- if version.exact and not version.dirty:
- return version.format_with("{tag}")
- # TODO: move the release-X check to a new scheme
- if version.branch is not None and version.branch.startswith("release-"):
- branch_ver = _parse_version_tag(version.branch.split("-")[-1], version.config)
- if branch_ver is not None:
- ver = branch_ver["version"]
- match = date_ver_match(ver)
- if match:
- return ver
- return version.format_next_version(
- guess_next_date_ver,
- node_date=version.node_date,
- version_cls=version.config.version_cls,
- )
-
-
-def get_local_node_and_date(version: ScmVersion) -> str:
- return _modify_version._format_local_with_time(version, time_format="%Y%m%d")
-
-
-def get_local_node_and_timestamp(version: ScmVersion) -> str:
- return _modify_version._format_local_with_time(version, time_format="%Y%m%d%H%M%S")
-
-
-def get_local_dirty_tag(version: ScmVersion) -> str:
- return version.format_choice("", "+dirty")
-
-
-def get_no_local_node(version: ScmVersion) -> str:
- return ""
-
-
-def postrelease_version(version: ScmVersion) -> str:
- if version.exact:
- return version.format_with("{tag}")
- else:
- return version.format_with("{tag}.post{distance}")
-
-
-def _combine_version_with_local_parts(
- main_version: str, *local_parts: str | None
-) -> str:
- """
- Combine a main version with multiple local parts into a valid PEP 440 version string.
- Handles deduplication of local parts to avoid adding the same local data twice.
-
- Args:
- main_version: The main version string (e.g., "1.2.0", "1.2.dev3")
- *local_parts: Variable number of local version parts, can be None or empty
-
- Returns:
- A valid PEP 440 version string
-
- Examples:
- _combine_version_with_local_parts("1.2.0", "build.123", "d20090213") -> "1.2.0+build.123.d20090213"
- _combine_version_with_local_parts("1.2.0", "build.123", None) -> "1.2.0+build.123"
- _combine_version_with_local_parts("1.2.0+build.123", "d20090213") -> "1.2.0+build.123.d20090213"
- _combine_version_with_local_parts("1.2.0+build.123", "build.123") -> "1.2.0+build.123" # no duplication
- _combine_version_with_local_parts("1.2.0", None, None) -> "1.2.0"
- """
- # Split main version into base and existing local parts
- if "+" in main_version:
- main_part, existing_local = main_version.split("+", 1)
- all_local_parts = existing_local.split(".")
- else:
- main_part = main_version
- all_local_parts = []
-
- # Process each new local part
- for part in local_parts:
- if not part or not part.strip():
- continue
-
- # Strip any leading + and split into segments
- clean_part = part.strip("+")
- if not clean_part:
- continue
-
- # Split multi-part local identifiers (e.g., "build.123" -> ["build", "123"])
- part_segments = clean_part.split(".")
-
- # Add each segment if not already present
- for segment in part_segments:
- if segment and segment not in all_local_parts:
- all_local_parts.append(segment)
-
- # Return combined result
- if all_local_parts:
- return main_part + "+" + ".".join(all_local_parts)
- else:
- return main_part
-
-
-def format_version(version: ScmVersion) -> str:
- log.debug("scm version %s", version)
- log.debug("config %s", version.config)
- if version.preformatted:
- return str(version.tag)
-
- # Extract original tag's local data for later combination
- original_local = ""
- if hasattr(version.tag, "local") and version.tag.local is not None:
- original_local = str(version.tag.local)
-
- # Create a patched ScmVersion with only the base version (no local data) for version schemes
- from dataclasses import replace
-
- # Extract the base version (public part) from the tag using config's version_cls
- base_version_str = str(version.tag.public)
- base_tag = version.config.version_cls(base_version_str)
- version_for_scheme = replace(version, tag=base_tag)
-
- main_version = _entrypoints._call_version_scheme(
- version_for_scheme,
- "setuptools_scm.version_scheme",
- version.config.version_scheme,
- )
- log.debug("version %s", main_version)
- assert main_version is not None
-
- local_version = _entrypoints._call_version_scheme(
- version, "setuptools_scm.local_scheme", version.config.local_scheme, "+unknown"
- )
- log.debug("local_version %s", local_version)
-
- # Combine main version with original local data and new local scheme data
- return _combine_version_with_local_parts(
- str(main_version), original_local, local_version
- )
diff --git a/src/vcs_versioning_workspace/__init__.py b/src/vcs_versioning_workspace/__init__.py
new file mode 100644
index 00000000..d422aa93
--- /dev/null
+++ b/src/vcs_versioning_workspace/__init__.py
@@ -0,0 +1 @@
+"""Workspace automation tools for setuptools-scm monorepo."""
diff --git a/src/vcs_versioning_workspace/create_release_proposal.py b/src/vcs_versioning_workspace/create_release_proposal.py
new file mode 100644
index 00000000..9c90148e
--- /dev/null
+++ b/src/vcs_versioning_workspace/create_release_proposal.py
@@ -0,0 +1,323 @@
+#!/usr/bin/env python3
+"""Unified release proposal script for setuptools-scm monorepo."""
+
+import argparse
+import os
+import subprocess
+import sys
+from pathlib import Path
+
+from github import Github
+from github.Repository import Repository
+from vcs_versioning._config import Configuration
+from vcs_versioning._get_version_impl import ( # type: ignore[attr-defined]
+ _format_version,
+ parse_version,
+)
+
+
+def find_fragments(project_dir: Path) -> list[Path]:
+ """Find changelog fragments in a project directory."""
+ changelog_dir = project_dir / "changelog.d"
+
+ if not changelog_dir.exists():
+ return []
+
+ fragments = []
+ for entry in changelog_dir.iterdir():
+ if not entry.is_file():
+ continue
+
+ # Skip template, README, and .gitkeep files
+ if entry.name in ("template.md", "README.md", ".gitkeep"):
+ continue
+
+ # Fragment naming: {number}.{type}.md
+ parts = entry.name.split(".")
+ if len(parts) >= 2 and entry.suffix == ".md":
+ fragments.append(entry)
+
+ return fragments
+
+
+def get_next_version(project_dir: Path, repo_root: Path) -> str | None:
+ """Get the next version for a project using vcs-versioning API."""
+ try:
+ # Load configuration from project's pyproject.toml
+ # All project-specific settings (tag_regex, fallback_version, etc.) are in the config files
+ # Override local_scheme to get clean version strings
+ pyproject = project_dir / "pyproject.toml"
+ config = Configuration.from_file(pyproject, local_scheme="no-local-version")
+
+ # Get the ScmVersion object
+ scm_version = parse_version(config)
+ if scm_version is None:
+ print(f"ERROR: Could not parse version for {project_dir}", file=sys.stderr)
+ return None
+
+ # Format the version string
+ version_string = _format_version(scm_version)
+
+ # Extract just the public version (X.Y.Z)
+ return version_string.split("+")[0] # Remove local part if present
+
+ except Exception as e:
+ print(f"Error determining version: {e}", file=sys.stderr)
+ return None
+
+
+def run_towncrier(project_dir: Path, version: str, *, draft: bool = False) -> bool:
+ """Run towncrier build for a project."""
+ try:
+ cmd = ["uv", "run", "towncrier", "build", "--version", version]
+ if draft:
+ cmd.append("--draft")
+ else:
+ cmd.append("--yes")
+
+ result = subprocess.run(
+ cmd,
+ cwd=project_dir,
+ capture_output=True,
+ text=True,
+ check=False,
+ )
+
+ if result.returncode != 0:
+ print(f"Towncrier failed: {result.stderr}", file=sys.stderr)
+ return False
+
+ return True
+
+ except Exception as e:
+ print(f"Error running towncrier: {e}", file=sys.stderr)
+ return False
+
+
+def check_existing_pr(repo: Repository, source_branch: str) -> tuple[str, int | None]:
+ """
+ Check for existing release PR.
+
+ Returns:
+ Tuple of (release_branch, pr_number)
+ """
+ release_branch = f"release/{source_branch}"
+ repo_owner = repo.owner.login
+
+ try:
+ pulls = repo.get_pulls(
+ state="open", base="main", head=f"{repo_owner}:{release_branch}"
+ )
+
+ for pr in pulls:
+ print(f"Found existing release PR #{pr.number}")
+ return release_branch, pr.number
+
+ print("No existing release PR found, will create new")
+ return release_branch, None
+
+ except Exception as e:
+ print(f"Error checking for PR: {e}", file=sys.stderr)
+ return release_branch, None
+
+
+def main() -> None:
+ parser = argparse.ArgumentParser(description="Create release proposal")
+ parser.add_argument(
+ "--event",
+ help="GitHub event type (push or pull_request)",
+ )
+ parser.add_argument(
+ "--branch",
+ help="Source branch name (defaults to current branch)",
+ )
+ args = parser.parse_args()
+
+ # Get environment variables
+ token = os.environ.get("GITHUB_TOKEN")
+ repo_name = os.environ.get("GITHUB_REPOSITORY")
+
+ # Determine source branch
+ if args.branch:
+ source_branch = args.branch
+ else:
+ # Get current branch from git
+ try:
+ result = subprocess.run(
+ ["git", "branch", "--show-current"],
+ capture_output=True,
+ text=True,
+ check=True,
+ )
+ source_branch = result.stdout.strip()
+ print(f"Using current branch: {source_branch}")
+ except subprocess.CalledProcessError:
+ print("ERROR: Could not determine current branch", file=sys.stderr)
+ sys.exit(1)
+
+ is_pr = args.event == "pull_request" if args.event else False
+
+ # GitHub integration is optional
+ github_mode = bool(token and repo_name)
+
+ if github_mode:
+ # Type narrowing: when github_mode is True, both token and repo_name are not None
+ assert token is not None
+ assert repo_name is not None
+ print(f"GitHub mode: enabled (repo: {repo_name})")
+ # Initialize GitHub API
+ gh = Github(token)
+ repo = gh.get_repo(repo_name)
+
+ # Check for existing PR (skip for pull_request events)
+ if not is_pr:
+ release_branch, existing_pr_number = check_existing_pr(repo, source_branch)
+ else:
+ release_branch = f"release/{source_branch}"
+ existing_pr_number = None
+ print(
+ f"[PR VALIDATION MODE] Validating release for branch: {source_branch}"
+ )
+ else:
+ print("GitHub mode: disabled (missing GITHUB_TOKEN or GITHUB_REPOSITORY)")
+ release_branch = f"release/{source_branch}"
+ existing_pr_number = None
+
+ repo_root = Path.cwd()
+ projects = {
+ "setuptools-scm": repo_root / "setuptools-scm",
+ "vcs-versioning": repo_root / "vcs-versioning",
+ }
+
+ # Detect which projects have fragments
+ to_release = {}
+ for project_name, project_path in projects.items():
+ fragments = find_fragments(project_path)
+ to_release[project_name] = len(fragments) > 0
+
+ if to_release[project_name]:
+ print(f"Found {len(fragments)} fragment(s) for {project_name}")
+ else:
+ print(f"No fragments found for {project_name}")
+
+ # Exit if no projects have fragments
+ if not any(to_release.values()):
+ print("No changelog fragments found in any project, skipping release")
+
+ # Write GitHub Step Summary (if in GitHub mode)
+ if github_mode:
+ github_summary = os.environ.get("GITHUB_STEP_SUMMARY")
+ if github_summary:
+ with open(github_summary, "a") as f:
+ f.write("## Release Proposal\n\n")
+ f.write("ℹ️ No changelog fragments to process\n")
+
+ sys.exit(0)
+
+ # Prepare releases
+ releases = []
+ labels = []
+
+ for project_name in ["setuptools-scm", "vcs-versioning"]:
+ if not to_release[project_name]:
+ continue
+
+ print(f"\nPreparing {project_name} release...")
+ project_dir = projects[project_name]
+
+ # Get next version
+ version = get_next_version(project_dir, repo_root)
+ if not version:
+ print(
+ f"ERROR: Failed to determine version for {project_name}",
+ file=sys.stderr,
+ )
+ sys.exit(1)
+
+ print(f"{project_name} next version: {version}")
+
+ # Run towncrier (draft mode for local runs)
+ if not run_towncrier(project_dir, version, draft=not github_mode):
+ print(f"ERROR: Towncrier build failed for {project_name}", file=sys.stderr)
+ sys.exit(1)
+
+ releases.append(f"{project_name} v{version}")
+ labels.append(f"release:{project_name}")
+
+ if not releases:
+ print("ERROR: No releases were prepared", file=sys.stderr)
+ sys.exit(1)
+
+ releases_str = ", ".join(releases)
+ print(f"\nSuccessfully prepared releases: {releases_str}")
+
+ # Write GitHub Actions outputs (if in GitHub mode)
+ if github_mode:
+ github_output = os.environ.get("GITHUB_OUTPUT")
+ if github_output:
+ with open(github_output, "a") as f:
+ f.write(f"release_branch={release_branch}\n")
+ f.write(f"releases={releases_str}\n")
+ f.write(f"labels={','.join(labels)}\n")
+
+ # Prepare PR content for workflow to use
+ pr_title = f"Release: {releases_str}"
+ pr_body = f"""## Release Proposal
+
+This PR prepares the following releases:
+{releases_str}
+
+**Source branch:** {source_branch}
+
+### Changes
+- Updated CHANGELOG.md with towncrier fragments
+- Removed processed fragments from changelog.d/
+
+### Review Checklist
+- [ ] Changelog entries are accurate
+- [ ] Version numbers are correct
+- [ ] All tests pass
+
+**Merging this PR will automatically create tags and trigger PyPI uploads.**"""
+
+ # Write outputs for workflow
+ if github_output:
+ with open(github_output, "a") as f:
+ # Write PR metadata (multiline strings need special encoding)
+ f.write(f"pr_title={pr_title}\n")
+ # For multiline, use GitHub Actions multiline syntax
+ f.write(f"pr_body< None:
- assert _log._default_log_level({"SETUPTOOLS_SCM_DEBUG": ""}) == logging.DEBUG
- assert _log._default_log_level({"SETUPTOOLS_SCM_DEBUG": "INFO"}) == logging.DEBUG
- assert _log._default_log_level({"SETUPTOOLS_SCM_DEBUG": "3"}) == logging.DEBUG
-
-
-def test_log_levels_when_unset() -> None:
- assert _log._default_log_level({}) == logging.WARNING
diff --git a/testing/test_pyproject_reading.py b/testing/test_pyproject_reading.py
deleted file mode 100644
index dc26e955..00000000
--- a/testing/test_pyproject_reading.py
+++ /dev/null
@@ -1,147 +0,0 @@
-from __future__ import annotations
-
-from pathlib import Path
-from unittest.mock import Mock
-
-import pytest
-
-from setuptools_scm._integration.pyproject_reading import has_build_package_with_extra
-from setuptools_scm._integration.pyproject_reading import read_pyproject
-
-
-class TestPyProjectReading:
- """Test the pyproject reading functionality."""
-
- def test_read_pyproject_missing_file_raises(self, tmp_path: Path) -> None:
- """Test that read_pyproject raises FileNotFoundError when file is missing."""
- with pytest.raises(FileNotFoundError):
- read_pyproject(path=tmp_path / "nonexistent.toml")
-
- def test_read_pyproject_existing_file(self, tmp_path: Path) -> None:
- """Test that read_pyproject reads existing files correctly."""
- # Create a simple pyproject.toml
- pyproject_content = """
-[build-system]
-requires = ["setuptools>=80", "setuptools-scm>=8"]
-build-backend = "setuptools.build_meta"
-
-[project]
-name = "test-package"
-dynamic = ["version"]
-
-[tool.setuptools_scm]
-"""
- pyproject_file = tmp_path / "pyproject.toml"
- pyproject_file.write_text(pyproject_content, encoding="utf-8")
-
- result = read_pyproject(path=pyproject_file)
-
- assert result.path == pyproject_file
- assert result.tool_name == "setuptools_scm"
- assert result.is_required is True
- assert result.section_present is True
- assert result.project_present is True
- assert result.project.get("name") == "test-package"
-
-
-class TestBuildPackageWithExtra:
- """Test the has_build_package_with_extra function."""
-
- def test_has_simple_extra(self) -> None:
- """Test that simple extra is detected correctly."""
- requires = ["setuptools-scm[simple]"]
- assert (
- has_build_package_with_extra(requires, "setuptools-scm", "simple") is True
- )
-
- def test_has_no_simple_extra(self) -> None:
- """Test that missing simple extra is detected correctly."""
- requires = ["setuptools-scm"]
- assert (
- has_build_package_with_extra(requires, "setuptools-scm", "simple") is False
- )
-
- def test_has_different_extra(self) -> None:
- """Test that different extra is not detected as simple."""
- requires = ["setuptools-scm[toml]"]
- assert (
- has_build_package_with_extra(requires, "setuptools-scm", "simple") is False
- )
-
- def test_has_multiple_extras_including_simple(self) -> None:
- """Test that simple extra is detected when multiple extras are present."""
- requires = ["setuptools-scm[simple,toml]"]
- assert (
- has_build_package_with_extra(requires, "setuptools-scm", "simple") is True
- )
-
- def test_different_package_with_simple_extra(self) -> None:
- """Test that simple extra on different package is not detected."""
- requires = ["other-package[simple]"]
- assert (
- has_build_package_with_extra(requires, "setuptools-scm", "simple") is False
- )
-
- def test_version_specifier_with_extra(self) -> None:
- """Test that version specifiers work correctly with extras."""
- requires = ["setuptools-scm[simple]>=8.0"]
- assert (
- has_build_package_with_extra(requires, "setuptools-scm", "simple") is True
- )
-
- def test_complex_requirement_with_extra(self) -> None:
- """Test that complex requirements with extras work correctly."""
- requires = ["setuptools-scm[simple]>=8.0,<9.0"]
- assert (
- has_build_package_with_extra(requires, "setuptools-scm", "simple") is True
- )
-
- def test_empty_requires_list(self) -> None:
- """Test that empty requires list returns False."""
- requires: list[str] = []
- assert (
- has_build_package_with_extra(requires, "setuptools-scm", "simple") is False
- )
-
- def test_invalid_requirement_string(self) -> None:
- """Test that invalid requirement strings are handled gracefully."""
- requires = ["invalid requirement string"]
- assert (
- has_build_package_with_extra(requires, "setuptools-scm", "simple") is False
- )
-
-
-def test_read_pyproject_with_given_definition(monkeypatch: pytest.MonkeyPatch) -> None:
- """Test that read_pyproject reads existing files correctly."""
- monkeypatch.setattr(
- "setuptools_scm._integration.pyproject_reading.read_toml_content",
- Mock(side_effect=FileNotFoundError("this test should not read")),
- )
-
- res = read_pyproject(
- _given_definition={
- "build-system": {"requires": ["setuptools-scm[simple]"]},
- "project": {"name": "test-package", "dynamic": ["version"]},
- }
- )
-
- assert res.should_infer()
-
-
-def test_read_pyproject_with_setuptools_dynamic_version_warns() -> None:
- with pytest.warns(
- UserWarning,
- match=r"pyproject\.toml: at \[tool\.setuptools\.dynamic\]",
- ):
- pyproject_data = read_pyproject(
- _given_definition={
- "build-system": {"requires": ["setuptools-scm[simple]"]},
- "project": {"name": "test-package", "dynamic": ["version"]},
- "tool": {
- "setuptools": {
- "dynamic": {"version": {"attr": "test_package.__version__"}}
- }
- },
- }
- )
- assert pyproject_data.project_version is None
diff --git a/uv.lock b/uv.lock
index 0ead2d5d..2a73b9d3 100644
--- a/uv.lock
+++ b/uv.lock
@@ -1,12 +1,18 @@
version = 1
revision = 3
-requires-python = ">=3.8"
+requires-python = ">=3.10"
resolution-markers = [
- "python_full_version >= '3.11'",
- "python_full_version == '3.10.*'",
- "python_full_version == '3.9.*'",
- "python_full_version >= '3.8.1' and python_full_version < '3.9'",
- "python_full_version < '3.8.1'",
+ "python_full_version >= '3.14' and platform_python_implementation != 'PyPy'",
+ "python_full_version >= '3.11' and python_full_version < '3.14' and platform_python_implementation != 'PyPy'",
+ "python_full_version >= '3.11' and platform_python_implementation == 'PyPy'",
+ "python_full_version < '3.11'",
+]
+
+[manifest]
+members = [
+ "setuptools-scm",
+ "vcs-versioning",
+ "vcs-versioning-workspace",
]
[[package]]
@@ -27,57 +33,19 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/7e/51/99d9dfcb588e15b4d9630f98f84d4e766d03b47da52839395057dbbe2df4/argh-0.30.5-py3-none-any.whl", hash = "sha256:3844e955d160f0689a3cdca06a59dfcfbf1fcea70029d67d473f73503341e0d8", size = 44635, upload-time = "2023-12-25T22:05:29.35Z" },
]
-[[package]]
-name = "astunparse"
-version = "1.6.3"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "six", marker = "python_full_version < '3.9'" },
- { name = "wheel", marker = "python_full_version < '3.9'" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/f3/af/4182184d3c338792894f34a62672919db7ca008c89abee9b564dd34d8029/astunparse-1.6.3.tar.gz", hash = "sha256:5ad93a8456f0d084c3456d059fd9a92cce667963232cbf763eac3bc5b7940872", size = 18290, upload-time = "2019-12-22T18:12:13.129Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/2b/03/13dde6512ad7b4557eb792fbcf0c653af6076b81e5941d36ec61f7ce6028/astunparse-1.6.3-py2.py3-none-any.whl", hash = "sha256:c2652417f2c8b5bb325c885ae329bdf3f86424075c4fd1a128674bc6fba4b8e8", size = 12732, upload-time = "2019-12-22T18:12:11.297Z" },
-]
-
[[package]]
name = "babel"
version = "2.17.0"
source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "pytz", marker = "python_full_version < '3.9'" },
-]
sdist = { url = "https://files.pythonhosted.org/packages/7d/6b/d52e42361e1aa00709585ecc30b3f9684b3ab62530771402248b1b1d6240/babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d", size = 9951852, upload-time = "2025-02-01T15:17:41.026Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b7/b8/3fe70c75fe32afc4bb507f75563d39bc5642255d1d94f1f23604725780bf/babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2", size = 10182537, upload-time = "2025-02-01T15:17:37.39Z" },
]
-[[package]]
-name = "backrefs"
-version = "5.7.post1"
-source = { registry = "https://pypi.org/simple" }
-resolution-markers = [
- "python_full_version >= '3.8.1' and python_full_version < '3.9'",
- "python_full_version < '3.8.1'",
-]
-sdist = { url = "https://files.pythonhosted.org/packages/df/30/903f35159c87ff1d92aa3fcf8cb52de97632a21e0ae43ed940f5d033e01a/backrefs-5.7.post1.tar.gz", hash = "sha256:8b0f83b770332ee2f1c8244f4e03c77d127a0fa529328e6a0e77fa25bee99678", size = 6582270, upload-time = "2024-06-16T18:38:20.166Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/24/bb/47fc255d1060dcfd55b460236380edd8ebfc5b2a42a0799ca90c9fc983e3/backrefs-5.7.post1-py310-none-any.whl", hash = "sha256:c5e3fd8fd185607a7cb1fefe878cfb09c34c0be3c18328f12c574245f1c0287e", size = 380429, upload-time = "2024-06-16T18:38:10.131Z" },
- { url = "https://files.pythonhosted.org/packages/89/72/39ef491caef3abae945f5a5fd72830d3b596bfac0630508629283585e213/backrefs-5.7.post1-py311-none-any.whl", hash = "sha256:712ea7e494c5bf3291156e28954dd96d04dc44681d0e5c030adf2623d5606d51", size = 392234, upload-time = "2024-06-16T18:38:12.283Z" },
- { url = "https://files.pythonhosted.org/packages/6a/00/33403f581b732ca70fdebab558e8bbb426a29c34e0c3ed674a479b74beea/backrefs-5.7.post1-py312-none-any.whl", hash = "sha256:a6142201c8293e75bce7577ac29e1a9438c12e730d73a59efdd1b75528d1a6c5", size = 398110, upload-time = "2024-06-16T18:38:14.257Z" },
- { url = "https://files.pythonhosted.org/packages/5d/ea/df0ac74a26838f6588aa012d5d801831448b87d0a7d0aefbbfabbe894870/backrefs-5.7.post1-py38-none-any.whl", hash = "sha256:ec61b1ee0a4bfa24267f6b67d0f8c5ffdc8e0d7dc2f18a2685fd1d8d9187054a", size = 369477, upload-time = "2024-06-16T18:38:16.196Z" },
- { url = "https://files.pythonhosted.org/packages/6f/e8/e43f535c0a17a695e5768670fc855a0e5d52dc0d4135b3915bfa355f65ac/backrefs-5.7.post1-py39-none-any.whl", hash = "sha256:05c04af2bf752bb9a6c9dcebb2aff2fab372d3d9d311f2a138540e307756bd3a", size = 380429, upload-time = "2024-06-16T18:38:18.079Z" },
-]
-
[[package]]
name = "backrefs"
version = "5.9"
source = { registry = "https://pypi.org/simple" }
-resolution-markers = [
- "python_full_version >= '3.11'",
- "python_full_version == '3.10.*'",
- "python_full_version == '3.9.*'",
-]
sdist = { url = "https://files.pythonhosted.org/packages/eb/a7/312f673df6a79003279e1f55619abbe7daebbb87c17c976ddc0345c04c7b/backrefs-5.9.tar.gz", hash = "sha256:808548cb708d66b82ee231f962cb36faaf4f2baab032f2fbb783e9c2fdddaa59", size = 5765857, upload-time = "2025-06-22T19:34:13.97Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/19/4d/798dc1f30468134906575156c089c492cf79b5a5fd373f07fe26c4d046bf/backrefs-5.9-py310-none-any.whl", hash = "sha256:db8e8ba0e9de81fcd635f440deab5ae5f2591b54ac1ebe0550a2ca063488cd9f", size = 380267, upload-time = "2025-06-22T19:34:05.252Z" },
@@ -88,28 +56,10 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/41/ff/392bff89415399a979be4a65357a41d92729ae8580a66073d8ec8d810f98/backrefs-5.9-py39-none-any.whl", hash = "sha256:f48ee18f6252b8f5777a22a00a09a85de0ca931658f1dd96d4406a34f3748c60", size = 380265, upload-time = "2025-06-22T19:34:12.405Z" },
]
-[[package]]
-name = "bracex"
-version = "2.5.post1"
-source = { registry = "https://pypi.org/simple" }
-resolution-markers = [
- "python_full_version >= '3.8.1' and python_full_version < '3.9'",
- "python_full_version < '3.8.1'",
-]
-sdist = { url = "https://files.pythonhosted.org/packages/d6/6c/57418c4404cd22fe6275b8301ca2b46a8cdaa8157938017a9ae0b3edf363/bracex-2.5.post1.tar.gz", hash = "sha256:12c50952415bfa773d2d9ccb8e79651b8cdb1f31a42f6091b804f6ba2b4a66b6", size = 26641, upload-time = "2024-09-28T21:41:22.017Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/4b/02/8db98cdc1a58e0abd6716d5e63244658e6e63513c65f469f34b6f1053fd0/bracex-2.5.post1-py3-none-any.whl", hash = "sha256:13e5732fec27828d6af308628285ad358047cec36801598368cb28bc631dbaf6", size = 11558, upload-time = "2024-09-28T21:41:21.016Z" },
-]
-
[[package]]
name = "bracex"
version = "2.6"
source = { registry = "https://pypi.org/simple" }
-resolution-markers = [
- "python_full_version >= '3.11'",
- "python_full_version == '3.10.*'",
- "python_full_version == '3.9.*'",
-]
sdist = { url = "https://files.pythonhosted.org/packages/63/9a/fec38644694abfaaeca2798b58e276a8e61de49e2e37494ace423395febc/bracex-2.6.tar.gz", hash = "sha256:98f1347cd77e22ee8d967a30ad4e310b233f7754dbf31ff3fceb76145ba47dc7", size = 26642, upload-time = "2025-06-22T19:12:31.254Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/9d/2a/9186535ce58db529927f6cf5990a849aa9e052eea3e2cfefe20b9e1802da/bracex-2.6-py3-none-any.whl", hash = "sha256:0b0049264e7340b3ec782b5cb99beb325f36c3782a32e36e876452fd49a09952", size = 11508, upload-time = "2025-06-22T19:12:29.781Z" },
@@ -135,8 +85,7 @@ version = "1.2.2.post1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "os_name == 'nt'" },
- { name = "importlib-metadata", version = "8.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" },
- { name = "importlib-metadata", version = "8.7.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9' and python_full_version < '3.10.2'" },
+ { name = "importlib-metadata", marker = "python_full_version < '3.10.2'" },
{ name = "packaging" },
{ name = "pyproject-hooks" },
{ name = "tomli", marker = "python_full_version < '3.11'" },
@@ -155,6 +104,71 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/4f/52/34c6cf5bb9285074dc3531c437b3919e825d976fde097a7a73f79e726d03/certifi-2025.7.14-py3-none-any.whl", hash = "sha256:6b31f564a415d79ee77df69d757bb49a5bb53bd9f756cbbe24394ffd6fc1f4b2", size = 162722, upload-time = "2025-07-14T03:29:26.863Z" },
]
+[[package]]
+name = "cffi"
+version = "2.0.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pycparser", marker = "(python_full_version < '3.11' and implementation_name != 'PyPy') or (implementation_name != 'PyPy' and platform_python_implementation != 'PyPy')" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/93/d7/516d984057745a6cd96575eea814fe1edd6646ee6efd552fb7b0921dec83/cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44", size = 184283, upload-time = "2025-09-08T23:22:08.01Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/84/ad6a0b408daa859246f57c03efd28e5dd1b33c21737c2db84cae8c237aa5/cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49", size = 180504, upload-time = "2025-09-08T23:22:10.637Z" },
+ { url = "https://files.pythonhosted.org/packages/50/bd/b1a6362b80628111e6653c961f987faa55262b4002fcec42308cad1db680/cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c", size = 208811, upload-time = "2025-09-08T23:22:12.267Z" },
+ { url = "https://files.pythonhosted.org/packages/4f/27/6933a8b2562d7bd1fb595074cf99cc81fc3789f6a6c05cdabb46284a3188/cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb", size = 216402, upload-time = "2025-09-08T23:22:13.455Z" },
+ { url = "https://files.pythonhosted.org/packages/05/eb/b86f2a2645b62adcfff53b0dd97e8dfafb5c8aa864bd0d9a2c2049a0d551/cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0", size = 203217, upload-time = "2025-09-08T23:22:14.596Z" },
+ { url = "https://files.pythonhosted.org/packages/9f/e0/6cbe77a53acf5acc7c08cc186c9928864bd7c005f9efd0d126884858a5fe/cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4", size = 203079, upload-time = "2025-09-08T23:22:15.769Z" },
+ { url = "https://files.pythonhosted.org/packages/98/29/9b366e70e243eb3d14a5cb488dfd3a0b6b2f1fb001a203f653b93ccfac88/cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453", size = 216475, upload-time = "2025-09-08T23:22:17.427Z" },
+ { url = "https://files.pythonhosted.org/packages/21/7a/13b24e70d2f90a322f2900c5d8e1f14fa7e2a6b3332b7309ba7b2ba51a5a/cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495", size = 218829, upload-time = "2025-09-08T23:22:19.069Z" },
+ { url = "https://files.pythonhosted.org/packages/60/99/c9dc110974c59cc981b1f5b66e1d8af8af764e00f0293266824d9c4254bc/cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5", size = 211211, upload-time = "2025-09-08T23:22:20.588Z" },
+ { url = "https://files.pythonhosted.org/packages/49/72/ff2d12dbf21aca1b32a40ed792ee6b40f6dc3a9cf1644bd7ef6e95e0ac5e/cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb", size = 218036, upload-time = "2025-09-08T23:22:22.143Z" },
+ { url = "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344, upload-time = "2025-09-08T23:22:26.456Z" },
+ { url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560, upload-time = "2025-09-08T23:22:28.197Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476, upload-time = "2025-09-08T23:22:31.063Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374, upload-time = "2025-09-08T23:22:32.507Z" },
+ { url = "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597, upload-time = "2025-09-08T23:22:34.132Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574, upload-time = "2025-09-08T23:22:35.443Z" },
+ { url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971, upload-time = "2025-09-08T23:22:36.805Z" },
+ { url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972, upload-time = "2025-09-08T23:22:38.436Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078, upload-time = "2025-09-08T23:22:39.776Z" },
+ { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" },
+ { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" },
+ { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" },
+ { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" },
+ { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" },
+ { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" },
+ { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" },
+ { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" },
+ { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" },
+ { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" },
+ { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" },
+ { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" },
+ { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" },
+ { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" },
+ { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" },
+ { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" },
+]
+
[[package]]
name = "charset-normalizer"
version = "3.4.2"
@@ -213,62 +227,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/78/be/8392efc43487ac051eee6c36d5fbd63032d78f7728cb37aebcc98191f1ff/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148", size = 149166, upload-time = "2025-05-02T08:33:15.458Z" },
{ url = "https://files.pythonhosted.org/packages/44/96/392abd49b094d30b91d9fbda6a69519e95802250b777841cf3bda8fe136c/charset_normalizer-3.4.2-cp313-cp313-win32.whl", hash = "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7", size = 98064, upload-time = "2025-05-02T08:33:17.06Z" },
{ url = "https://files.pythonhosted.org/packages/e9/b0/0200da600134e001d91851ddc797809e2fe0ea72de90e09bec5a2fbdaccb/charset_normalizer-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980", size = 105641, upload-time = "2025-05-02T08:33:18.753Z" },
- { url = "https://files.pythonhosted.org/packages/4c/fd/f700cfd4ad876def96d2c769d8a32d808b12d1010b6003dc6639157f99ee/charset_normalizer-3.4.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:76af085e67e56c8816c3ccf256ebd136def2ed9654525348cfa744b6802b69eb", size = 198257, upload-time = "2025-05-02T08:33:45.511Z" },
- { url = "https://files.pythonhosted.org/packages/3a/95/6eec4cbbbd119e6a402e3bfd16246785cc52ce64cf21af2ecdf7b3a08e91/charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e45ba65510e2647721e35323d6ef54c7974959f6081b58d4ef5d87c60c84919a", size = 143453, upload-time = "2025-05-02T08:33:47.463Z" },
- { url = "https://files.pythonhosted.org/packages/b6/b3/d4f913660383b3d93dbe6f687a312ea9f7e89879ae883c4e8942048174d4/charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:046595208aae0120559a67693ecc65dd75d46f7bf687f159127046628178dc45", size = 153130, upload-time = "2025-05-02T08:33:50.568Z" },
- { url = "https://files.pythonhosted.org/packages/e5/69/7540141529eabc55bf19cc05cd9b61c2078bebfcdbd3e799af99b777fc28/charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75d10d37a47afee94919c4fab4c22b9bc2a8bf7d4f46f87363bcf0573f3ff4f5", size = 145688, upload-time = "2025-05-02T08:33:52.828Z" },
- { url = "https://files.pythonhosted.org/packages/2e/bb/d76d3d6e340fb0967c43c564101e28a78c9a363ea62f736a68af59ee3683/charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6333b3aa5a12c26b2a4d4e7335a28f1475e0e5e17d69d55141ee3cab736f66d1", size = 147418, upload-time = "2025-05-02T08:33:54.718Z" },
- { url = "https://files.pythonhosted.org/packages/3e/ef/b7c1f39c0dc3808160c8b72e0209c2479393966313bfebc833533cfff9cc/charset_normalizer-3.4.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e8323a9b031aa0393768b87f04b4164a40037fb2a3c11ac06a03ffecd3618027", size = 150066, upload-time = "2025-05-02T08:33:56.597Z" },
- { url = "https://files.pythonhosted.org/packages/20/26/4e47cc23d2a4a5eb6ed7d6f0f8cda87d753e2f8abc936d5cf5ad2aae8518/charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:24498ba8ed6c2e0b56d4acbf83f2d989720a93b41d712ebd4f4979660db4417b", size = 144499, upload-time = "2025-05-02T08:33:58.637Z" },
- { url = "https://files.pythonhosted.org/packages/d7/9c/efdf59dd46593cecad0548d36a702683a0bdc056793398a9cd1e1546ad21/charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:844da2b5728b5ce0e32d863af26f32b5ce61bc4273a9c720a9f3aa9df73b1455", size = 152954, upload-time = "2025-05-02T08:34:00.552Z" },
- { url = "https://files.pythonhosted.org/packages/59/b3/4e8b73f7299d9aaabd7cd26db4a765f741b8e57df97b034bb8de15609002/charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:65c981bdbd3f57670af8b59777cbfae75364b483fa8a9f420f08094531d54a01", size = 155876, upload-time = "2025-05-02T08:34:02.527Z" },
- { url = "https://files.pythonhosted.org/packages/53/cb/6fa0ccf941a069adce3edb8a1e430bc80e4929f4d43b5140fdf8628bdf7d/charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:3c21d4fca343c805a52c0c78edc01e3477f6dd1ad7c47653241cf2a206d4fc58", size = 153186, upload-time = "2025-05-02T08:34:04.481Z" },
- { url = "https://files.pythonhosted.org/packages/ac/c6/80b93fabc626b75b1665ffe405e28c3cef0aae9237c5c05f15955af4edd8/charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:dc7039885fa1baf9be153a0626e337aa7ec8bf96b0128605fb0d77788ddc1681", size = 148007, upload-time = "2025-05-02T08:34:06.888Z" },
- { url = "https://files.pythonhosted.org/packages/41/eb/c7367ac326a2628e4f05b5c737c86fe4a8eb3ecc597a4243fc65720b3eeb/charset_normalizer-3.4.2-cp38-cp38-win32.whl", hash = "sha256:8272b73e1c5603666618805fe821edba66892e2870058c94c53147602eab29c7", size = 97923, upload-time = "2025-05-02T08:34:08.792Z" },
- { url = "https://files.pythonhosted.org/packages/7c/02/1c82646582ccf2c757fa6af69b1a3ea88744b8d2b4ab93b7686b2533e023/charset_normalizer-3.4.2-cp38-cp38-win_amd64.whl", hash = "sha256:70f7172939fdf8790425ba31915bfbe8335030f05b9913d7ae00a87d4395620a", size = 105020, upload-time = "2025-05-02T08:34:10.6Z" },
- { url = "https://files.pythonhosted.org/packages/28/f8/dfb01ff6cc9af38552c69c9027501ff5a5117c4cc18dcd27cb5259fa1888/charset_normalizer-3.4.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:005fa3432484527f9732ebd315da8da8001593e2cf46a3d817669f062c3d9ed4", size = 201671, upload-time = "2025-05-02T08:34:12.696Z" },
- { url = "https://files.pythonhosted.org/packages/32/fb/74e26ee556a9dbfe3bd264289b67be1e6d616329403036f6507bb9f3f29c/charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e92fca20c46e9f5e1bb485887d074918b13543b1c2a1185e69bb8d17ab6236a7", size = 144744, upload-time = "2025-05-02T08:34:14.665Z" },
- { url = "https://files.pythonhosted.org/packages/ad/06/8499ee5aa7addc6f6d72e068691826ff093329fe59891e83b092ae4c851c/charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:50bf98d5e563b83cc29471fa114366e6806bc06bc7a25fd59641e41445327836", size = 154993, upload-time = "2025-05-02T08:34:17.134Z" },
- { url = "https://files.pythonhosted.org/packages/f1/a2/5e4c187680728219254ef107a6949c60ee0e9a916a5dadb148c7ae82459c/charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:721c76e84fe669be19c5791da68232ca2e05ba5185575086e384352e2c309597", size = 147382, upload-time = "2025-05-02T08:34:19.081Z" },
- { url = "https://files.pythonhosted.org/packages/4c/fe/56aca740dda674f0cc1ba1418c4d84534be51f639b5f98f538b332dc9a95/charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82d8fd25b7f4675d0c47cf95b594d4e7b158aca33b76aa63d07186e13c0e0ab7", size = 149536, upload-time = "2025-05-02T08:34:21.073Z" },
- { url = "https://files.pythonhosted.org/packages/53/13/db2e7779f892386b589173dd689c1b1e304621c5792046edd8a978cbf9e0/charset_normalizer-3.4.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b3daeac64d5b371dea99714f08ffc2c208522ec6b06fbc7866a450dd446f5c0f", size = 151349, upload-time = "2025-05-02T08:34:23.193Z" },
- { url = "https://files.pythonhosted.org/packages/69/35/e52ab9a276186f729bce7a0638585d2982f50402046e4b0faa5d2c3ef2da/charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:dccab8d5fa1ef9bfba0590ecf4d46df048d18ffe3eec01eeb73a42e0d9e7a8ba", size = 146365, upload-time = "2025-05-02T08:34:25.187Z" },
- { url = "https://files.pythonhosted.org/packages/a6/d8/af7333f732fc2e7635867d56cb7c349c28c7094910c72267586947561b4b/charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:aaf27faa992bfee0264dc1f03f4c75e9fcdda66a519db6b957a3f826e285cf12", size = 154499, upload-time = "2025-05-02T08:34:27.359Z" },
- { url = "https://files.pythonhosted.org/packages/7a/3d/a5b2e48acef264d71e036ff30bcc49e51bde80219bb628ba3e00cf59baac/charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:eb30abc20df9ab0814b5a2524f23d75dcf83cde762c161917a2b4b7b55b1e518", size = 157735, upload-time = "2025-05-02T08:34:29.798Z" },
- { url = "https://files.pythonhosted.org/packages/85/d8/23e2c112532a29f3eef374375a8684a4f3b8e784f62b01da931186f43494/charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:c72fbbe68c6f32f251bdc08b8611c7b3060612236e960ef848e0a517ddbe76c5", size = 154786, upload-time = "2025-05-02T08:34:31.858Z" },
- { url = "https://files.pythonhosted.org/packages/c7/57/93e0169f08ecc20fe82d12254a200dfaceddc1c12a4077bf454ecc597e33/charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:982bb1e8b4ffda883b3d0a521e23abcd6fd17418f6d2c4118d257a10199c0ce3", size = 150203, upload-time = "2025-05-02T08:34:33.88Z" },
- { url = "https://files.pythonhosted.org/packages/2c/9d/9bf2b005138e7e060d7ebdec7503d0ef3240141587651f4b445bdf7286c2/charset_normalizer-3.4.2-cp39-cp39-win32.whl", hash = "sha256:43e0933a0eff183ee85833f341ec567c0980dae57c464d8a508e1b2ceb336471", size = 98436, upload-time = "2025-05-02T08:34:35.907Z" },
- { url = "https://files.pythonhosted.org/packages/6d/24/5849d46cf4311bbf21b424c443b09b459f5b436b1558c04e45dbb7cc478b/charset_normalizer-3.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:d11b54acf878eef558599658b0ffca78138c8c3655cf4f3a4a673c437e67732e", size = 105772, upload-time = "2025-05-02T08:34:37.935Z" },
{ url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626, upload-time = "2025-05-02T08:34:40.053Z" },
]
-[[package]]
-name = "click"
-version = "8.1.8"
-source = { registry = "https://pypi.org/simple" }
-resolution-markers = [
- "python_full_version == '3.9.*'",
- "python_full_version >= '3.8.1' and python_full_version < '3.9'",
- "python_full_version < '3.8.1'",
-]
-dependencies = [
- { name = "colorama", marker = "python_full_version < '3.10' and sys_platform == 'win32'" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593, upload-time = "2024-12-21T18:38:44.339Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188, upload-time = "2024-12-21T18:38:41.666Z" },
-]
-
[[package]]
name = "click"
version = "8.2.1"
source = { registry = "https://pypi.org/simple" }
-resolution-markers = [
- "python_full_version >= '3.11'",
- "python_full_version == '3.10.*'",
-]
dependencies = [
- { name = "colorama", marker = "python_full_version >= '3.10' and sys_platform == 'win32'" },
+ { name = "colorama", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342, upload-time = "2025-05-20T23:19:49.832Z" }
wheels = [
@@ -296,7 +263,7 @@ dependencies = [
{ name = "jinja2-ansible-filters", marker = "python_full_version >= '3.11'" },
{ name = "packaging", marker = "python_full_version >= '3.11'" },
{ name = "pathspec", marker = "python_full_version >= '3.11'" },
- { name = "platformdirs", version = "4.3.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
+ { name = "platformdirs", marker = "python_full_version >= '3.11'" },
{ name = "plumbum", marker = "python_full_version >= '3.11'" },
{ name = "pydantic", marker = "python_full_version >= '3.11'" },
{ name = "pygments", marker = "python_full_version >= '3.11'" },
@@ -308,6 +275,164 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/4f/ed/839c91ff365f24756c90189e07f9de226d2e37cbc03c635f5d16d45d79cb/copier-9.8.0-py3-none-any.whl", hash = "sha256:ca0bee47f198b66cec926c4f1a3aa77f11ee0102624369c10e42ca9058c0a891", size = 55744, upload-time = "2025-07-07T18:47:01.905Z" },
]
+[[package]]
+name = "coverage"
+version = "7.10.7"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/51/26/d22c300112504f5f9a9fd2297ce33c35f3d353e4aeb987c8419453b2a7c2/coverage-7.10.7.tar.gz", hash = "sha256:f4ab143ab113be368a3e9b795f9cd7906c5ef407d6173fe9675a902e1fffc239", size = 827704, upload-time = "2025-09-21T20:03:56.815Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e5/6c/3a3f7a46888e69d18abe3ccc6fe4cb16cccb1e6a2f99698931dafca489e6/coverage-7.10.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:fc04cc7a3db33664e0c2d10eb8990ff6b3536f6842c9590ae8da4c614b9ed05a", size = 217987, upload-time = "2025-09-21T20:00:57.218Z" },
+ { url = "https://files.pythonhosted.org/packages/03/94/952d30f180b1a916c11a56f5c22d3535e943aa22430e9e3322447e520e1c/coverage-7.10.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e201e015644e207139f7e2351980feb7040e6f4b2c2978892f3e3789d1c125e5", size = 218388, upload-time = "2025-09-21T20:01:00.081Z" },
+ { url = "https://files.pythonhosted.org/packages/50/2b/9e0cf8ded1e114bcd8b2fd42792b57f1c4e9e4ea1824cde2af93a67305be/coverage-7.10.7-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:240af60539987ced2c399809bd34f7c78e8abe0736af91c3d7d0e795df633d17", size = 245148, upload-time = "2025-09-21T20:01:01.768Z" },
+ { url = "https://files.pythonhosted.org/packages/19/20/d0384ac06a6f908783d9b6aa6135e41b093971499ec488e47279f5b846e6/coverage-7.10.7-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8421e088bc051361b01c4b3a50fd39a4b9133079a2229978d9d30511fd05231b", size = 246958, upload-time = "2025-09-21T20:01:03.355Z" },
+ { url = "https://files.pythonhosted.org/packages/60/83/5c283cff3d41285f8eab897651585db908a909c572bdc014bcfaf8a8b6ae/coverage-7.10.7-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6be8ed3039ae7f7ac5ce058c308484787c86e8437e72b30bf5e88b8ea10f3c87", size = 248819, upload-time = "2025-09-21T20:01:04.968Z" },
+ { url = "https://files.pythonhosted.org/packages/60/22/02eb98fdc5ff79f423e990d877693e5310ae1eab6cb20ae0b0b9ac45b23b/coverage-7.10.7-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e28299d9f2e889e6d51b1f043f58d5f997c373cc12e6403b90df95b8b047c13e", size = 245754, upload-time = "2025-09-21T20:01:06.321Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/bc/25c83bcf3ad141b32cd7dc45485ef3c01a776ca3aa8ef0a93e77e8b5bc43/coverage-7.10.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c4e16bd7761c5e454f4efd36f345286d6f7c5fa111623c355691e2755cae3b9e", size = 246860, upload-time = "2025-09-21T20:01:07.605Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/b7/95574702888b58c0928a6e982038c596f9c34d52c5e5107f1eef729399b5/coverage-7.10.7-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b1c81d0e5e160651879755c9c675b974276f135558cf4ba79fee7b8413a515df", size = 244877, upload-time = "2025-09-21T20:01:08.829Z" },
+ { url = "https://files.pythonhosted.org/packages/47/b6/40095c185f235e085df0e0b158f6bd68cc6e1d80ba6c7721dc81d97ec318/coverage-7.10.7-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:606cc265adc9aaedcc84f1f064f0e8736bc45814f15a357e30fca7ecc01504e0", size = 245108, upload-time = "2025-09-21T20:01:10.527Z" },
+ { url = "https://files.pythonhosted.org/packages/c8/50/4aea0556da7a4b93ec9168420d170b55e2eb50ae21b25062513d020c6861/coverage-7.10.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:10b24412692df990dbc34f8fb1b6b13d236ace9dfdd68df5b28c2e39cafbba13", size = 245752, upload-time = "2025-09-21T20:01:11.857Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/28/ea1a84a60828177ae3b100cb6723838523369a44ec5742313ed7db3da160/coverage-7.10.7-cp310-cp310-win32.whl", hash = "sha256:b51dcd060f18c19290d9b8a9dd1e0181538df2ce0717f562fff6cf74d9fc0b5b", size = 220497, upload-time = "2025-09-21T20:01:13.459Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/1a/a81d46bbeb3c3fd97b9602ebaa411e076219a150489bcc2c025f151bd52d/coverage-7.10.7-cp310-cp310-win_amd64.whl", hash = "sha256:3a622ac801b17198020f09af3eaf45666b344a0d69fc2a6ffe2ea83aeef1d807", size = 221392, upload-time = "2025-09-21T20:01:14.722Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/5d/c1a17867b0456f2e9ce2d8d4708a4c3a089947d0bec9c66cdf60c9e7739f/coverage-7.10.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a609f9c93113be646f44c2a0256d6ea375ad047005d7f57a5c15f614dc1b2f59", size = 218102, upload-time = "2025-09-21T20:01:16.089Z" },
+ { url = "https://files.pythonhosted.org/packages/54/f0/514dcf4b4e3698b9a9077f084429681bf3aad2b4a72578f89d7f643eb506/coverage-7.10.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:65646bb0359386e07639c367a22cf9b5bf6304e8630b565d0626e2bdf329227a", size = 218505, upload-time = "2025-09-21T20:01:17.788Z" },
+ { url = "https://files.pythonhosted.org/packages/20/f6/9626b81d17e2a4b25c63ac1b425ff307ecdeef03d67c9a147673ae40dc36/coverage-7.10.7-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5f33166f0dfcce728191f520bd2692914ec70fac2713f6bf3ce59c3deacb4699", size = 248898, upload-time = "2025-09-21T20:01:19.488Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/ef/bd8e719c2f7417ba03239052e099b76ea1130ac0cbb183ee1fcaa58aaff3/coverage-7.10.7-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:35f5e3f9e455bb17831876048355dca0f758b6df22f49258cb5a91da23ef437d", size = 250831, upload-time = "2025-09-21T20:01:20.817Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/b6/bf054de41ec948b151ae2b79a55c107f5760979538f5fb80c195f2517718/coverage-7.10.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4da86b6d62a496e908ac2898243920c7992499c1712ff7c2b6d837cc69d9467e", size = 252937, upload-time = "2025-09-21T20:01:22.171Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/e5/3860756aa6f9318227443c6ce4ed7bf9e70bb7f1447a0353f45ac5c7974b/coverage-7.10.7-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6b8b09c1fad947c84bbbc95eca841350fad9cbfa5a2d7ca88ac9f8d836c92e23", size = 249021, upload-time = "2025-09-21T20:01:23.907Z" },
+ { url = "https://files.pythonhosted.org/packages/26/0f/bd08bd042854f7fd07b45808927ebcce99a7ed0f2f412d11629883517ac2/coverage-7.10.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4376538f36b533b46f8971d3a3e63464f2c7905c9800db97361c43a2b14792ab", size = 250626, upload-time = "2025-09-21T20:01:25.721Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/a7/4777b14de4abcc2e80c6b1d430f5d51eb18ed1d75fca56cbce5f2db9b36e/coverage-7.10.7-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:121da30abb574f6ce6ae09840dae322bef734480ceafe410117627aa54f76d82", size = 248682, upload-time = "2025-09-21T20:01:27.105Z" },
+ { url = "https://files.pythonhosted.org/packages/34/72/17d082b00b53cd45679bad682fac058b87f011fd8b9fe31d77f5f8d3a4e4/coverage-7.10.7-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:88127d40df529336a9836870436fc2751c339fbaed3a836d42c93f3e4bd1d0a2", size = 248402, upload-time = "2025-09-21T20:01:28.629Z" },
+ { url = "https://files.pythonhosted.org/packages/81/7a/92367572eb5bdd6a84bfa278cc7e97db192f9f45b28c94a9ca1a921c3577/coverage-7.10.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ba58bbcd1b72f136080c0bccc2400d66cc6115f3f906c499013d065ac33a4b61", size = 249320, upload-time = "2025-09-21T20:01:30.004Z" },
+ { url = "https://files.pythonhosted.org/packages/2f/88/a23cc185f6a805dfc4fdf14a94016835eeb85e22ac3a0e66d5e89acd6462/coverage-7.10.7-cp311-cp311-win32.whl", hash = "sha256:972b9e3a4094b053a4e46832b4bc829fc8a8d347160eb39d03f1690316a99c14", size = 220536, upload-time = "2025-09-21T20:01:32.184Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/ef/0b510a399dfca17cec7bc2f05ad8bd78cf55f15c8bc9a73ab20c5c913c2e/coverage-7.10.7-cp311-cp311-win_amd64.whl", hash = "sha256:a7b55a944a7f43892e28ad4bc0561dfd5f0d73e605d1aa5c3c976b52aea121d2", size = 221425, upload-time = "2025-09-21T20:01:33.557Z" },
+ { url = "https://files.pythonhosted.org/packages/51/7f/023657f301a276e4ba1850f82749bc136f5a7e8768060c2e5d9744a22951/coverage-7.10.7-cp311-cp311-win_arm64.whl", hash = "sha256:736f227fb490f03c6488f9b6d45855f8e0fd749c007f9303ad30efab0e73c05a", size = 220103, upload-time = "2025-09-21T20:01:34.929Z" },
+ { url = "https://files.pythonhosted.org/packages/13/e4/eb12450f71b542a53972d19117ea5a5cea1cab3ac9e31b0b5d498df1bd5a/coverage-7.10.7-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7bb3b9ddb87ef7725056572368040c32775036472d5a033679d1fa6c8dc08417", size = 218290, upload-time = "2025-09-21T20:01:36.455Z" },
+ { url = "https://files.pythonhosted.org/packages/37/66/593f9be12fc19fb36711f19a5371af79a718537204d16ea1d36f16bd78d2/coverage-7.10.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:18afb24843cbc175687225cab1138c95d262337f5473512010e46831aa0c2973", size = 218515, upload-time = "2025-09-21T20:01:37.982Z" },
+ { url = "https://files.pythonhosted.org/packages/66/80/4c49f7ae09cafdacc73fbc30949ffe77359635c168f4e9ff33c9ebb07838/coverage-7.10.7-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:399a0b6347bcd3822be369392932884b8216d0944049ae22925631a9b3d4ba4c", size = 250020, upload-time = "2025-09-21T20:01:39.617Z" },
+ { url = "https://files.pythonhosted.org/packages/a6/90/a64aaacab3b37a17aaedd83e8000142561a29eb262cede42d94a67f7556b/coverage-7.10.7-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:314f2c326ded3f4b09be11bc282eb2fc861184bc95748ae67b360ac962770be7", size = 252769, upload-time = "2025-09-21T20:01:41.341Z" },
+ { url = "https://files.pythonhosted.org/packages/98/2e/2dda59afd6103b342e096f246ebc5f87a3363b5412609946c120f4e7750d/coverage-7.10.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c41e71c9cfb854789dee6fc51e46743a6d138b1803fab6cb860af43265b42ea6", size = 253901, upload-time = "2025-09-21T20:01:43.042Z" },
+ { url = "https://files.pythonhosted.org/packages/53/dc/8d8119c9051d50f3119bb4a75f29f1e4a6ab9415cd1fa8bf22fcc3fb3b5f/coverage-7.10.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc01f57ca26269c2c706e838f6422e2a8788e41b3e3c65e2f41148212e57cd59", size = 250413, upload-time = "2025-09-21T20:01:44.469Z" },
+ { url = "https://files.pythonhosted.org/packages/98/b3/edaff9c5d79ee4d4b6d3fe046f2b1d799850425695b789d491a64225d493/coverage-7.10.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a6442c59a8ac8b85812ce33bc4d05bde3fb22321fa8294e2a5b487c3505f611b", size = 251820, upload-time = "2025-09-21T20:01:45.915Z" },
+ { url = "https://files.pythonhosted.org/packages/11/25/9a0728564bb05863f7e513e5a594fe5ffef091b325437f5430e8cfb0d530/coverage-7.10.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:78a384e49f46b80fb4c901d52d92abe098e78768ed829c673fbb53c498bef73a", size = 249941, upload-time = "2025-09-21T20:01:47.296Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/fd/ca2650443bfbef5b0e74373aac4df67b08180d2f184b482c41499668e258/coverage-7.10.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:5e1e9802121405ede4b0133aa4340ad8186a1d2526de5b7c3eca519db7bb89fb", size = 249519, upload-time = "2025-09-21T20:01:48.73Z" },
+ { url = "https://files.pythonhosted.org/packages/24/79/f692f125fb4299b6f963b0745124998ebb8e73ecdfce4ceceb06a8c6bec5/coverage-7.10.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d41213ea25a86f69efd1575073d34ea11aabe075604ddf3d148ecfec9e1e96a1", size = 251375, upload-time = "2025-09-21T20:01:50.529Z" },
+ { url = "https://files.pythonhosted.org/packages/5e/75/61b9bbd6c7d24d896bfeec57acba78e0f8deac68e6baf2d4804f7aae1f88/coverage-7.10.7-cp312-cp312-win32.whl", hash = "sha256:77eb4c747061a6af8d0f7bdb31f1e108d172762ef579166ec84542f711d90256", size = 220699, upload-time = "2025-09-21T20:01:51.941Z" },
+ { url = "https://files.pythonhosted.org/packages/ca/f3/3bf7905288b45b075918d372498f1cf845b5b579b723c8fd17168018d5f5/coverage-7.10.7-cp312-cp312-win_amd64.whl", hash = "sha256:f51328ffe987aecf6d09f3cd9d979face89a617eacdaea43e7b3080777f647ba", size = 221512, upload-time = "2025-09-21T20:01:53.481Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/44/3e32dbe933979d05cf2dac5e697c8599cfe038aaf51223ab901e208d5a62/coverage-7.10.7-cp312-cp312-win_arm64.whl", hash = "sha256:bda5e34f8a75721c96085903c6f2197dc398c20ffd98df33f866a9c8fd95f4bf", size = 220147, upload-time = "2025-09-21T20:01:55.2Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/94/b765c1abcb613d103b64fcf10395f54d69b0ef8be6a0dd9c524384892cc7/coverage-7.10.7-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:981a651f543f2854abd3b5fcb3263aac581b18209be49863ba575de6edf4c14d", size = 218320, upload-time = "2025-09-21T20:01:56.629Z" },
+ { url = "https://files.pythonhosted.org/packages/72/4f/732fff31c119bb73b35236dd333030f32c4bfe909f445b423e6c7594f9a2/coverage-7.10.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:73ab1601f84dc804f7812dc297e93cd99381162da39c47040a827d4e8dafe63b", size = 218575, upload-time = "2025-09-21T20:01:58.203Z" },
+ { url = "https://files.pythonhosted.org/packages/87/02/ae7e0af4b674be47566707777db1aa375474f02a1d64b9323e5813a6cdd5/coverage-7.10.7-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a8b6f03672aa6734e700bbcd65ff050fd19cddfec4b031cc8cf1c6967de5a68e", size = 249568, upload-time = "2025-09-21T20:01:59.748Z" },
+ { url = "https://files.pythonhosted.org/packages/a2/77/8c6d22bf61921a59bce5471c2f1f7ac30cd4ac50aadde72b8c48d5727902/coverage-7.10.7-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:10b6ba00ab1132a0ce4428ff68cf50a25efd6840a42cdf4239c9b99aad83be8b", size = 252174, upload-time = "2025-09-21T20:02:01.192Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/20/b6ea4f69bbb52dac0aebd62157ba6a9dddbfe664f5af8122dac296c3ee15/coverage-7.10.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c79124f70465a150e89340de5963f936ee97097d2ef76c869708c4248c63ca49", size = 253447, upload-time = "2025-09-21T20:02:02.701Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/28/4831523ba483a7f90f7b259d2018fef02cb4d5b90bc7c1505d6e5a84883c/coverage-7.10.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:69212fbccdbd5b0e39eac4067e20a4a5256609e209547d86f740d68ad4f04911", size = 249779, upload-time = "2025-09-21T20:02:04.185Z" },
+ { url = "https://files.pythonhosted.org/packages/a7/9f/4331142bc98c10ca6436d2d620c3e165f31e6c58d43479985afce6f3191c/coverage-7.10.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7ea7c6c9d0d286d04ed3541747e6597cbe4971f22648b68248f7ddcd329207f0", size = 251604, upload-time = "2025-09-21T20:02:06.034Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/60/bda83b96602036b77ecf34e6393a3836365481b69f7ed7079ab85048202b/coverage-7.10.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b9be91986841a75042b3e3243d0b3cb0b2434252b977baaf0cd56e960fe1e46f", size = 249497, upload-time = "2025-09-21T20:02:07.619Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/af/152633ff35b2af63977edd835d8e6430f0caef27d171edf2fc76c270ef31/coverage-7.10.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:b281d5eca50189325cfe1f365fafade89b14b4a78d9b40b05ddd1fc7d2a10a9c", size = 249350, upload-time = "2025-09-21T20:02:10.34Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/71/d92105d122bd21cebba877228990e1646d862e34a98bb3374d3fece5a794/coverage-7.10.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:99e4aa63097ab1118e75a848a28e40d68b08a5e19ce587891ab7fd04475e780f", size = 251111, upload-time = "2025-09-21T20:02:12.122Z" },
+ { url = "https://files.pythonhosted.org/packages/a2/9e/9fdb08f4bf476c912f0c3ca292e019aab6712c93c9344a1653986c3fd305/coverage-7.10.7-cp313-cp313-win32.whl", hash = "sha256:dc7c389dce432500273eaf48f410b37886be9208b2dd5710aaf7c57fd442c698", size = 220746, upload-time = "2025-09-21T20:02:13.919Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/b1/a75fd25df44eab52d1931e89980d1ada46824c7a3210be0d3c88a44aaa99/coverage-7.10.7-cp313-cp313-win_amd64.whl", hash = "sha256:cac0fdca17b036af3881a9d2729a850b76553f3f716ccb0360ad4dbc06b3b843", size = 221541, upload-time = "2025-09-21T20:02:15.57Z" },
+ { url = "https://files.pythonhosted.org/packages/14/3a/d720d7c989562a6e9a14b2c9f5f2876bdb38e9367126d118495b89c99c37/coverage-7.10.7-cp313-cp313-win_arm64.whl", hash = "sha256:4b6f236edf6e2f9ae8fcd1332da4e791c1b6ba0dc16a2dc94590ceccb482e546", size = 220170, upload-time = "2025-09-21T20:02:17.395Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/22/e04514bf2a735d8b0add31d2b4ab636fc02370730787c576bb995390d2d5/coverage-7.10.7-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a0ec07fd264d0745ee396b666d47cef20875f4ff2375d7c4f58235886cc1ef0c", size = 219029, upload-time = "2025-09-21T20:02:18.936Z" },
+ { url = "https://files.pythonhosted.org/packages/11/0b/91128e099035ece15da3445d9015e4b4153a6059403452d324cbb0a575fa/coverage-7.10.7-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:dd5e856ebb7bfb7672b0086846db5afb4567a7b9714b8a0ebafd211ec7ce6a15", size = 219259, upload-time = "2025-09-21T20:02:20.44Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/51/66420081e72801536a091a0c8f8c1f88a5c4bf7b9b1bdc6222c7afe6dc9b/coverage-7.10.7-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f57b2a3c8353d3e04acf75b3fed57ba41f5c0646bbf1d10c7c282291c97936b4", size = 260592, upload-time = "2025-09-21T20:02:22.313Z" },
+ { url = "https://files.pythonhosted.org/packages/5d/22/9b8d458c2881b22df3db5bb3e7369e63d527d986decb6c11a591ba2364f7/coverage-7.10.7-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1ef2319dd15a0b009667301a3f84452a4dc6fddfd06b0c5c53ea472d3989fbf0", size = 262768, upload-time = "2025-09-21T20:02:24.287Z" },
+ { url = "https://files.pythonhosted.org/packages/f7/08/16bee2c433e60913c610ea200b276e8eeef084b0d200bdcff69920bd5828/coverage-7.10.7-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:83082a57783239717ceb0ad584de3c69cf581b2a95ed6bf81ea66034f00401c0", size = 264995, upload-time = "2025-09-21T20:02:26.133Z" },
+ { url = "https://files.pythonhosted.org/packages/20/9d/e53eb9771d154859b084b90201e5221bca7674ba449a17c101a5031d4054/coverage-7.10.7-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:50aa94fb1fb9a397eaa19c0d5ec15a5edd03a47bf1a3a6111a16b36e190cff65", size = 259546, upload-time = "2025-09-21T20:02:27.716Z" },
+ { url = "https://files.pythonhosted.org/packages/ad/b0/69bc7050f8d4e56a89fb550a1577d5d0d1db2278106f6f626464067b3817/coverage-7.10.7-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2120043f147bebb41c85b97ac45dd173595ff14f2a584f2963891cbcc3091541", size = 262544, upload-time = "2025-09-21T20:02:29.216Z" },
+ { url = "https://files.pythonhosted.org/packages/ef/4b/2514b060dbd1bc0aaf23b852c14bb5818f244c664cb16517feff6bb3a5ab/coverage-7.10.7-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:2fafd773231dd0378fdba66d339f84904a8e57a262f583530f4f156ab83863e6", size = 260308, upload-time = "2025-09-21T20:02:31.226Z" },
+ { url = "https://files.pythonhosted.org/packages/54/78/7ba2175007c246d75e496f64c06e94122bdb914790a1285d627a918bd271/coverage-7.10.7-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:0b944ee8459f515f28b851728ad224fa2d068f1513ef6b7ff1efafeb2185f999", size = 258920, upload-time = "2025-09-21T20:02:32.823Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/b3/fac9f7abbc841409b9a410309d73bfa6cfb2e51c3fada738cb607ce174f8/coverage-7.10.7-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4b583b97ab2e3efe1b3e75248a9b333bd3f8b0b1b8e5b45578e05e5850dfb2c2", size = 261434, upload-time = "2025-09-21T20:02:34.86Z" },
+ { url = "https://files.pythonhosted.org/packages/ee/51/a03bec00d37faaa891b3ff7387192cef20f01604e5283a5fabc95346befa/coverage-7.10.7-cp313-cp313t-win32.whl", hash = "sha256:2a78cd46550081a7909b3329e2266204d584866e8d97b898cd7fb5ac8d888b1a", size = 221403, upload-time = "2025-09-21T20:02:37.034Z" },
+ { url = "https://files.pythonhosted.org/packages/53/22/3cf25d614e64bf6d8e59c7c669b20d6d940bb337bdee5900b9ca41c820bb/coverage-7.10.7-cp313-cp313t-win_amd64.whl", hash = "sha256:33a5e6396ab684cb43dc7befa386258acb2d7fae7f67330ebb85ba4ea27938eb", size = 222469, upload-time = "2025-09-21T20:02:39.011Z" },
+ { url = "https://files.pythonhosted.org/packages/49/a1/00164f6d30d8a01c3c9c48418a7a5be394de5349b421b9ee019f380df2a0/coverage-7.10.7-cp313-cp313t-win_arm64.whl", hash = "sha256:86b0e7308289ddde73d863b7683f596d8d21c7d8664ce1dee061d0bcf3fbb4bb", size = 220731, upload-time = "2025-09-21T20:02:40.939Z" },
+ { url = "https://files.pythonhosted.org/packages/23/9c/5844ab4ca6a4dd97a1850e030a15ec7d292b5c5cb93082979225126e35dd/coverage-7.10.7-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b06f260b16ead11643a5a9f955bd4b5fd76c1a4c6796aeade8520095b75de520", size = 218302, upload-time = "2025-09-21T20:02:42.527Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/89/673f6514b0961d1f0e20ddc242e9342f6da21eaba3489901b565c0689f34/coverage-7.10.7-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:212f8f2e0612778f09c55dd4872cb1f64a1f2b074393d139278ce902064d5b32", size = 218578, upload-time = "2025-09-21T20:02:44.468Z" },
+ { url = "https://files.pythonhosted.org/packages/05/e8/261cae479e85232828fb17ad536765c88dd818c8470aca690b0ac6feeaa3/coverage-7.10.7-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3445258bcded7d4aa630ab8296dea4d3f15a255588dd535f980c193ab6b95f3f", size = 249629, upload-time = "2025-09-21T20:02:46.503Z" },
+ { url = "https://files.pythonhosted.org/packages/82/62/14ed6546d0207e6eda876434e3e8475a3e9adbe32110ce896c9e0c06bb9a/coverage-7.10.7-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bb45474711ba385c46a0bfe696c695a929ae69ac636cda8f532be9e8c93d720a", size = 252162, upload-time = "2025-09-21T20:02:48.689Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/49/07f00db9ac6478e4358165a08fb41b469a1b053212e8a00cb02f0d27a05f/coverage-7.10.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:813922f35bd800dca9994c5971883cbc0d291128a5de6b167c7aa697fcf59360", size = 253517, upload-time = "2025-09-21T20:02:50.31Z" },
+ { url = "https://files.pythonhosted.org/packages/a2/59/c5201c62dbf165dfbc91460f6dbbaa85a8b82cfa6131ac45d6c1bfb52deb/coverage-7.10.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:93c1b03552081b2a4423091d6fb3787265b8f86af404cff98d1b5342713bdd69", size = 249632, upload-time = "2025-09-21T20:02:51.971Z" },
+ { url = "https://files.pythonhosted.org/packages/07/ae/5920097195291a51fb00b3a70b9bbd2edbfe3c84876a1762bd1ef1565ebc/coverage-7.10.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:cc87dd1b6eaf0b848eebb1c86469b9f72a1891cb42ac7adcfbce75eadb13dd14", size = 251520, upload-time = "2025-09-21T20:02:53.858Z" },
+ { url = "https://files.pythonhosted.org/packages/b9/3c/a815dde77a2981f5743a60b63df31cb322c944843e57dbd579326625a413/coverage-7.10.7-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:39508ffda4f343c35f3236fe8d1a6634a51f4581226a1262769d7f970e73bffe", size = 249455, upload-time = "2025-09-21T20:02:55.807Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/99/f5cdd8421ea656abefb6c0ce92556709db2265c41e8f9fc6c8ae0f7824c9/coverage-7.10.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:925a1edf3d810537c5a3abe78ec5530160c5f9a26b1f4270b40e62cc79304a1e", size = 249287, upload-time = "2025-09-21T20:02:57.784Z" },
+ { url = "https://files.pythonhosted.org/packages/c3/7a/e9a2da6a1fc5d007dd51fca083a663ab930a8c4d149c087732a5dbaa0029/coverage-7.10.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2c8b9a0636f94c43cd3576811e05b89aa9bc2d0a85137affc544ae5cb0e4bfbd", size = 250946, upload-time = "2025-09-21T20:02:59.431Z" },
+ { url = "https://files.pythonhosted.org/packages/ef/5b/0b5799aa30380a949005a353715095d6d1da81927d6dbed5def2200a4e25/coverage-7.10.7-cp314-cp314-win32.whl", hash = "sha256:b7b8288eb7cdd268b0304632da8cb0bb93fadcfec2fe5712f7b9cc8f4d487be2", size = 221009, upload-time = "2025-09-21T20:03:01.324Z" },
+ { url = "https://files.pythonhosted.org/packages/da/b0/e802fbb6eb746de006490abc9bb554b708918b6774b722bb3a0e6aa1b7de/coverage-7.10.7-cp314-cp314-win_amd64.whl", hash = "sha256:1ca6db7c8807fb9e755d0379ccc39017ce0a84dcd26d14b5a03b78563776f681", size = 221804, upload-time = "2025-09-21T20:03:03.4Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/e8/71d0c8e374e31f39e3389bb0bd19e527d46f00ea8571ec7ec8fd261d8b44/coverage-7.10.7-cp314-cp314-win_arm64.whl", hash = "sha256:097c1591f5af4496226d5783d036bf6fd6cd0cbc132e071b33861de756efb880", size = 220384, upload-time = "2025-09-21T20:03:05.111Z" },
+ { url = "https://files.pythonhosted.org/packages/62/09/9a5608d319fa3eba7a2019addeacb8c746fb50872b57a724c9f79f146969/coverage-7.10.7-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:a62c6ef0d50e6de320c270ff91d9dd0a05e7250cac2a800b7784bae474506e63", size = 219047, upload-time = "2025-09-21T20:03:06.795Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/6f/f58d46f33db9f2e3647b2d0764704548c184e6f5e014bef528b7f979ef84/coverage-7.10.7-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9fa6e4dd51fe15d8738708a973470f67a855ca50002294852e9571cdbd9433f2", size = 219266, upload-time = "2025-09-21T20:03:08.495Z" },
+ { url = "https://files.pythonhosted.org/packages/74/5c/183ffc817ba68e0b443b8c934c8795553eb0c14573813415bd59941ee165/coverage-7.10.7-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8fb190658865565c549b6b4706856d6a7b09302c797eb2cf8e7fe9dabb043f0d", size = 260767, upload-time = "2025-09-21T20:03:10.172Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/48/71a8abe9c1ad7e97548835e3cc1adbf361e743e9d60310c5f75c9e7bf847/coverage-7.10.7-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:affef7c76a9ef259187ef31599a9260330e0335a3011732c4b9effa01e1cd6e0", size = 262931, upload-time = "2025-09-21T20:03:11.861Z" },
+ { url = "https://files.pythonhosted.org/packages/84/fd/193a8fb132acfc0a901f72020e54be5e48021e1575bb327d8ee1097a28fd/coverage-7.10.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e16e07d85ca0cf8bafe5f5d23a0b850064e8e945d5677492b06bbe6f09cc699", size = 265186, upload-time = "2025-09-21T20:03:13.539Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/8f/74ecc30607dd95ad50e3034221113ccb1c6d4e8085cc761134782995daae/coverage-7.10.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:03ffc58aacdf65d2a82bbeb1ffe4d01ead4017a21bfd0454983b88ca73af94b9", size = 259470, upload-time = "2025-09-21T20:03:15.584Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/55/79ff53a769f20d71b07023ea115c9167c0bb56f281320520cf64c5298a96/coverage-7.10.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1b4fd784344d4e52647fd7857b2af5b3fbe6c239b0b5fa63e94eb67320770e0f", size = 262626, upload-time = "2025-09-21T20:03:17.673Z" },
+ { url = "https://files.pythonhosted.org/packages/88/e2/dac66c140009b61ac3fc13af673a574b00c16efdf04f9b5c740703e953c0/coverage-7.10.7-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:0ebbaddb2c19b71912c6f2518e791aa8b9f054985a0769bdb3a53ebbc765c6a1", size = 260386, upload-time = "2025-09-21T20:03:19.36Z" },
+ { url = "https://files.pythonhosted.org/packages/a2/f1/f48f645e3f33bb9ca8a496bc4a9671b52f2f353146233ebd7c1df6160440/coverage-7.10.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:a2d9a3b260cc1d1dbdb1c582e63ddcf5363426a1a68faa0f5da28d8ee3c722a0", size = 258852, upload-time = "2025-09-21T20:03:21.007Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/3b/8442618972c51a7affeead957995cfa8323c0c9bcf8fa5a027421f720ff4/coverage-7.10.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a3cc8638b2480865eaa3926d192e64ce6c51e3d29c849e09d5b4ad95efae5399", size = 261534, upload-time = "2025-09-21T20:03:23.12Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/dc/101f3fa3a45146db0cb03f5b4376e24c0aac818309da23e2de0c75295a91/coverage-7.10.7-cp314-cp314t-win32.whl", hash = "sha256:67f8c5cbcd3deb7a60b3345dffc89a961a484ed0af1f6f73de91705cc6e31235", size = 221784, upload-time = "2025-09-21T20:03:24.769Z" },
+ { url = "https://files.pythonhosted.org/packages/4c/a1/74c51803fc70a8a40d7346660379e144be772bab4ac7bb6e6b905152345c/coverage-7.10.7-cp314-cp314t-win_amd64.whl", hash = "sha256:e1ed71194ef6dea7ed2d5cb5f7243d4bcd334bfb63e59878519be558078f848d", size = 222905, upload-time = "2025-09-21T20:03:26.93Z" },
+ { url = "https://files.pythonhosted.org/packages/12/65/f116a6d2127df30bcafbceef0302d8a64ba87488bf6f73a6d8eebf060873/coverage-7.10.7-cp314-cp314t-win_arm64.whl", hash = "sha256:7fe650342addd8524ca63d77b2362b02345e5f1a093266787d210c70a50b471a", size = 220922, upload-time = "2025-09-21T20:03:28.672Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/16/114df1c291c22cac3b0c127a73e0af5c12ed7bbb6558d310429a0ae24023/coverage-7.10.7-py3-none-any.whl", hash = "sha256:f7941f6f2fe6dd6807a1208737b8a0cbcf1cc6d7b07d24998ad2d63590868260", size = 209952, upload-time = "2025-09-21T20:03:53.918Z" },
+]
+
+[package.optional-dependencies]
+toml = [
+ { name = "tomli", marker = "python_full_version <= '3.11'" },
+]
+
+[[package]]
+name = "cryptography"
+version = "46.0.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
+ { name = "typing-extensions", marker = "python_full_version < '3.11'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/4a/9b/e301418629f7bfdf72db9e80ad6ed9d1b83c487c471803eaa6464c511a01/cryptography-46.0.2.tar.gz", hash = "sha256:21b6fc8c71a3f9a604f028a329e5560009cc4a3a828bfea5fcba8eb7647d88fe", size = 749293, upload-time = "2025-10-01T00:29:11.856Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e0/98/7a8df8c19a335c8028414738490fc3955c0cecbfdd37fcc1b9c3d04bd561/cryptography-46.0.2-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:f3e32ab7dd1b1ef67b9232c4cf5e2ee4cd517d4316ea910acaaa9c5712a1c663", size = 7261255, upload-time = "2025-10-01T00:27:22.947Z" },
+ { url = "https://files.pythonhosted.org/packages/c6/38/b2adb2aa1baa6706adc3eb746691edd6f90a656a9a65c3509e274d15a2b8/cryptography-46.0.2-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1fd1a69086926b623ef8126b4c33d5399ce9e2f3fac07c9c734c2a4ec38b6d02", size = 4297596, upload-time = "2025-10-01T00:27:25.258Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/27/0f190ada240003119488ae66c897b5e97149292988f556aef4a6a2a57595/cryptography-46.0.2-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bb7fb9cd44c2582aa5990cf61a4183e6f54eea3172e54963787ba47287edd135", size = 4450899, upload-time = "2025-10-01T00:27:27.458Z" },
+ { url = "https://files.pythonhosted.org/packages/85/d5/e4744105ab02fdf6bb58ba9a816e23b7a633255987310b4187d6745533db/cryptography-46.0.2-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:9066cfd7f146f291869a9898b01df1c9b0e314bfa182cef432043f13fc462c92", size = 4300382, upload-time = "2025-10-01T00:27:29.091Z" },
+ { url = "https://files.pythonhosted.org/packages/33/fb/bf9571065c18c04818cb07de90c43fc042c7977c68e5de6876049559c72f/cryptography-46.0.2-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:97e83bf4f2f2c084d8dd792d13841d0a9b241643151686010866bbd076b19659", size = 4017347, upload-time = "2025-10-01T00:27:30.767Z" },
+ { url = "https://files.pythonhosted.org/packages/35/72/fc51856b9b16155ca071080e1a3ad0c3a8e86616daf7eb018d9565b99baa/cryptography-46.0.2-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:4a766d2a5d8127364fd936572c6e6757682fc5dfcbdba1632d4554943199f2fa", size = 4983500, upload-time = "2025-10-01T00:27:32.741Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/53/0f51e926799025e31746d454ab2e36f8c3f0d41592bc65cb9840368d3275/cryptography-46.0.2-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:fab8f805e9675e61ed8538f192aad70500fa6afb33a8803932999b1049363a08", size = 4482591, upload-time = "2025-10-01T00:27:34.869Z" },
+ { url = "https://files.pythonhosted.org/packages/86/96/4302af40b23ab8aa360862251fb8fc450b2a06ff24bc5e261c2007f27014/cryptography-46.0.2-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:1e3b6428a3d56043bff0bb85b41c535734204e599c1c0977e1d0f261b02f3ad5", size = 4300019, upload-time = "2025-10-01T00:27:37.029Z" },
+ { url = "https://files.pythonhosted.org/packages/9b/59/0be12c7fcc4c5e34fe2b665a75bc20958473047a30d095a7657c218fa9e8/cryptography-46.0.2-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:1a88634851d9b8de8bb53726f4300ab191d3b2f42595e2581a54b26aba71b7cc", size = 4950006, upload-time = "2025-10-01T00:27:40.272Z" },
+ { url = "https://files.pythonhosted.org/packages/55/1d/42fda47b0111834b49e31590ae14fd020594d5e4dadd639bce89ad790fba/cryptography-46.0.2-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:be939b99d4e091eec9a2bcf41aaf8f351f312cd19ff74b5c83480f08a8a43e0b", size = 4482088, upload-time = "2025-10-01T00:27:42.668Z" },
+ { url = "https://files.pythonhosted.org/packages/17/50/60f583f69aa1602c2bdc7022dae86a0d2b837276182f8c1ec825feb9b874/cryptography-46.0.2-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9f13b040649bc18e7eb37936009b24fd31ca095a5c647be8bb6aaf1761142bd1", size = 4425599, upload-time = "2025-10-01T00:27:44.616Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/57/d8d4134cd27e6e94cf44adb3f3489f935bde85f3a5508e1b5b43095b917d/cryptography-46.0.2-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9bdc25e4e01b261a8fda4e98618f1c9515febcecebc9566ddf4a70c63967043b", size = 4697458, upload-time = "2025-10-01T00:27:46.209Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/cc/47fc6223a341f26d103cb6da2216805e08a37d3b52bee7f3b2aee8066f95/cryptography-46.0.2-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:bda55e8dbe8533937956c996beaa20266a8eca3570402e52ae52ed60de1faca8", size = 7198626, upload-time = "2025-10-01T00:27:54.8Z" },
+ { url = "https://files.pythonhosted.org/packages/93/22/d66a8591207c28bbe4ac7afa25c4656dc19dc0db29a219f9809205639ede/cryptography-46.0.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e7155c0b004e936d381b15425273aee1cebc94f879c0ce82b0d7fecbf755d53a", size = 4287584, upload-time = "2025-10-01T00:27:57.018Z" },
+ { url = "https://files.pythonhosted.org/packages/8c/3e/fac3ab6302b928e0398c269eddab5978e6c1c50b2b77bb5365ffa8633b37/cryptography-46.0.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a61c154cc5488272a6c4b86e8d5beff4639cdb173d75325ce464d723cda0052b", size = 4433796, upload-time = "2025-10-01T00:27:58.631Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/d8/24392e5d3c58e2d83f98fe5a2322ae343360ec5b5b93fe18bc52e47298f5/cryptography-46.0.2-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:9ec3f2e2173f36a9679d3b06d3d01121ab9b57c979de1e6a244b98d51fea1b20", size = 4292126, upload-time = "2025-10-01T00:28:00.643Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/38/3d9f9359b84c16c49a5a336ee8be8d322072a09fac17e737f3bb11f1ce64/cryptography-46.0.2-cp314-cp314t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2fafb6aa24e702bbf74de4cb23bfa2c3beb7ab7683a299062b69724c92e0fa73", size = 3993056, upload-time = "2025-10-01T00:28:02.8Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/a3/4c44fce0d49a4703cc94bfbe705adebf7ab36efe978053742957bc7ec324/cryptography-46.0.2-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:0c7ffe8c9b1fcbb07a26d7c9fa5e857c2fe80d72d7b9e0353dcf1d2180ae60ee", size = 4967604, upload-time = "2025-10-01T00:28:04.783Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/c2/49d73218747c8cac16bb8318a5513fde3129e06a018af3bc4dc722aa4a98/cryptography-46.0.2-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:5840f05518caa86b09d23f8b9405a7b6d5400085aa14a72a98fdf5cf1568c0d2", size = 4465367, upload-time = "2025-10-01T00:28:06.864Z" },
+ { url = "https://files.pythonhosted.org/packages/1b/64/9afa7d2ee742f55ca6285a54386ed2778556a4ed8871571cb1c1bfd8db9e/cryptography-46.0.2-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:27c53b4f6a682a1b645fbf1cd5058c72cf2f5aeba7d74314c36838c7cbc06e0f", size = 4291678, upload-time = "2025-10-01T00:28:08.982Z" },
+ { url = "https://files.pythonhosted.org/packages/50/48/1696d5ea9623a7b72ace87608f6899ca3c331709ac7ebf80740abb8ac673/cryptography-46.0.2-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:512c0250065e0a6b286b2db4bbcc2e67d810acd53eb81733e71314340366279e", size = 4931366, upload-time = "2025-10-01T00:28:10.74Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/3c/9dfc778401a334db3b24435ee0733dd005aefb74afe036e2d154547cb917/cryptography-46.0.2-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:07c0eb6657c0e9cca5891f4e35081dbf985c8131825e21d99b4f440a8f496f36", size = 4464738, upload-time = "2025-10-01T00:28:12.491Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/b1/abcde62072b8f3fd414e191a6238ce55a0050e9738090dc6cded24c12036/cryptography-46.0.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:48b983089378f50cba258f7f7aa28198c3f6e13e607eaf10472c26320332ca9a", size = 4419305, upload-time = "2025-10-01T00:28:14.145Z" },
+ { url = "https://files.pythonhosted.org/packages/c7/1f/3d2228492f9391395ca34c677e8f2571fb5370fe13dc48c1014f8c509864/cryptography-46.0.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e6f6775eaaa08c0eec73e301f7592f4367ccde5e4e4df8e58320f2ebf161ea2c", size = 4681201, upload-time = "2025-10-01T00:28:15.951Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/bb/fa95abcf147a1b0bb94d95f53fbb09da77b24c776c5d87d36f3d94521d2c/cryptography-46.0.2-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:a08e7401a94c002e79dc3bc5231b6558cd4b2280ee525c4673f650a37e2c7685", size = 7248090, upload-time = "2025-10-01T00:28:22.846Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/66/f42071ce0e3ffbfa80a88feadb209c779fda92a23fbc1e14f74ebf72ef6b/cryptography-46.0.2-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d30bc11d35743bf4ddf76674a0a369ec8a21f87aaa09b0661b04c5f6c46e8d7b", size = 4293123, upload-time = "2025-10-01T00:28:25.072Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/5d/1fdbd2e5c1ba822828d250e5a966622ef00185e476d1cd2726b6dd135e53/cryptography-46.0.2-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bca3f0ce67e5a2a2cf524e86f44697c4323a86e0fd7ba857de1c30d52c11ede1", size = 4439524, upload-time = "2025-10-01T00:28:26.808Z" },
+ { url = "https://files.pythonhosted.org/packages/c8/c1/5e4989a7d102d4306053770d60f978c7b6b1ea2ff8c06e0265e305b23516/cryptography-46.0.2-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ff798ad7a957a5021dcbab78dfff681f0cf15744d0e6af62bd6746984d9c9e9c", size = 4297264, upload-time = "2025-10-01T00:28:29.327Z" },
+ { url = "https://files.pythonhosted.org/packages/28/78/b56f847d220cb1d6d6aef5a390e116ad603ce13a0945a3386a33abc80385/cryptography-46.0.2-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:cb5e8daac840e8879407acbe689a174f5ebaf344a062f8918e526824eb5d97af", size = 4011872, upload-time = "2025-10-01T00:28:31.479Z" },
+ { url = "https://files.pythonhosted.org/packages/e1/80/2971f214b066b888944f7b57761bf709ee3f2cf805619a18b18cab9b263c/cryptography-46.0.2-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:3f37aa12b2d91e157827d90ce78f6180f0c02319468a0aea86ab5a9566da644b", size = 4978458, upload-time = "2025-10-01T00:28:33.267Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/84/0cb0a2beaa4f1cbe63ebec4e97cd7e0e9f835d0ba5ee143ed2523a1e0016/cryptography-46.0.2-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:5e38f203160a48b93010b07493c15f2babb4e0f2319bbd001885adb3f3696d21", size = 4472195, upload-time = "2025-10-01T00:28:36.039Z" },
+ { url = "https://files.pythonhosted.org/packages/30/8b/2b542ddbf78835c7cd67b6fa79e95560023481213a060b92352a61a10efe/cryptography-46.0.2-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:d19f5f48883752b5ab34cff9e2f7e4a7f216296f33714e77d1beb03d108632b6", size = 4296791, upload-time = "2025-10-01T00:28:37.732Z" },
+ { url = "https://files.pythonhosted.org/packages/78/12/9065b40201b4f4876e93b9b94d91feb18de9150d60bd842a16a21565007f/cryptography-46.0.2-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:04911b149eae142ccd8c9a68892a70c21613864afb47aba92d8c7ed9cc001023", size = 4939629, upload-time = "2025-10-01T00:28:39.654Z" },
+ { url = "https://files.pythonhosted.org/packages/f6/9e/6507dc048c1b1530d372c483dfd34e7709fc542765015425f0442b08547f/cryptography-46.0.2-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:8b16c1ede6a937c291d41176934268e4ccac2c6521c69d3f5961c5a1e11e039e", size = 4471988, upload-time = "2025-10-01T00:28:41.822Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/86/d025584a5f7d5c5ec8d3633dbcdce83a0cd579f1141ceada7817a4c26934/cryptography-46.0.2-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:747b6f4a4a23d5a215aadd1d0b12233b4119c4313df83ab4137631d43672cc90", size = 4422989, upload-time = "2025-10-01T00:28:43.608Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/39/536370418b38a15a61bbe413006b79dfc3d2b4b0eafceb5581983f973c15/cryptography-46.0.2-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6b275e398ab3a7905e168c036aad54b5969d63d3d9099a0a66cc147a3cc983be", size = 4685578, upload-time = "2025-10-01T00:28:45.361Z" },
+ { url = "https://files.pythonhosted.org/packages/25/b2/067a7db693488f19777ecf73f925bcb6a3efa2eae42355bafaafa37a6588/cryptography-46.0.2-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:f25a41f5b34b371a06dad3f01799706631331adc7d6c05253f5bca22068c7a34", size = 3701860, upload-time = "2025-10-01T00:28:53.003Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/8c/1aabe338149a7d0f52c3e30f2880b20027ca2a485316756ed6f000462db3/cryptography-46.0.2-pp311-pypy311_pp73-macosx_10_9_x86_64.whl", hash = "sha256:1d3b3edd145953832e09607986f2bd86f85d1dc9c48ced41808b18009d9f30e5", size = 3714495, upload-time = "2025-10-01T00:28:57.222Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/0a/0d10eb970fe3e57da9e9ddcfd9464c76f42baf7b3d0db4a782d6746f788f/cryptography-46.0.2-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:fe245cf4a73c20592f0f48da39748b3513db114465be78f0a36da847221bd1b4", size = 4243379, upload-time = "2025-10-01T00:28:58.989Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/60/e274b4d41a9eb82538b39950a74ef06e9e4d723cb998044635d9deb1b435/cryptography-46.0.2-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:2b9cad9cf71d0c45566624ff76654e9bae5f8a25970c250a26ccfc73f8553e2d", size = 4409533, upload-time = "2025-10-01T00:29:00.785Z" },
+ { url = "https://files.pythonhosted.org/packages/19/9a/fb8548f762b4749aebd13b57b8f865de80258083fe814957f9b0619cfc56/cryptography-46.0.2-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:9bd26f2f75a925fdf5e0a446c0de2714f17819bf560b44b7480e4dd632ad6c46", size = 4243120, upload-time = "2025-10-01T00:29:02.515Z" },
+ { url = "https://files.pythonhosted.org/packages/71/60/883f24147fd4a0c5cab74ac7e36a1ff3094a54ba5c3a6253d2ff4b19255b/cryptography-46.0.2-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:7282d8f092b5be7172d6472f29b0631f39f18512a3642aefe52c3c0e0ccfad5a", size = 4408940, upload-time = "2025-10-01T00:29:04.42Z" },
+]
+
[[package]]
name = "dunamai"
version = "1.25.0"
@@ -347,8 +472,7 @@ name = "exceptiongroup"
version = "1.3.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
- { name = "typing-extensions", version = "4.13.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" },
- { name = "typing-extensions", version = "4.14.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9' and python_full_version < '3.11'" },
+ { name = "typing-extensions", marker = "python_full_version < '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749, upload-time = "2025-05-10T17:42:51.123Z" }
wheels = [
@@ -373,53 +497,14 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/4d/36/2a115987e2d8c300a974597416d9de88f2444426de9571f4b59b2cca3acc/filelock-3.18.0-py3-none-any.whl", hash = "sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de", size = 16215, upload-time = "2025-03-14T07:11:39.145Z" },
]
-[[package]]
-name = "flake8"
-version = "5.0.4"
-source = { registry = "https://pypi.org/simple" }
-resolution-markers = [
- "python_full_version < '3.8.1'",
-]
-dependencies = [
- { name = "mccabe", marker = "python_full_version < '3.8.1'" },
- { name = "pycodestyle", version = "2.9.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8.1'" },
- { name = "pyflakes", version = "2.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8.1'" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/ad/00/9808c62b2d529cefc69ce4e4a1ea42c0f855effa55817b7327ec5b75e60a/flake8-5.0.4.tar.gz", hash = "sha256:6fbe320aad8d6b95cec8b8e47bc933004678dc63095be98528b7bdd2a9f510db", size = 145862, upload-time = "2022-08-03T23:21:27.108Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/cf/a0/b881b63a17a59d9d07f5c0cc91a29182c8e8a9aa2bde5b3b2b16519c02f4/flake8-5.0.4-py2.py3-none-any.whl", hash = "sha256:7a1cf6b73744f5806ab95e526f6f0d8c01c66d7bbe349562d22dfca20610b248", size = 61897, upload-time = "2022-08-03T23:21:25.027Z" },
-]
-
-[[package]]
-name = "flake8"
-version = "7.1.2"
-source = { registry = "https://pypi.org/simple" }
-resolution-markers = [
- "python_full_version >= '3.8.1' and python_full_version < '3.9'",
-]
-dependencies = [
- { name = "mccabe", marker = "python_full_version >= '3.8.1' and python_full_version < '3.9'" },
- { name = "pycodestyle", version = "2.12.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.8.1' and python_full_version < '3.9'" },
- { name = "pyflakes", version = "3.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.8.1' and python_full_version < '3.9'" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/58/16/3f2a0bb700ad65ac9663262905a025917c020a3f92f014d2ba8964b4602c/flake8-7.1.2.tar.gz", hash = "sha256:c586ffd0b41540951ae41af572e6790dbd49fc12b3aa2541685d253d9bd504bd", size = 48119, upload-time = "2025-02-16T18:45:44.296Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/35/f8/08d37b2cd89da306e3520bd27f8a85692122b42b56c0c2c3784ff09c022f/flake8-7.1.2-py2.py3-none-any.whl", hash = "sha256:1cbc62e65536f65e6d754dfe6f1bada7f5cf392d6f5db3c2b85892466c3e7c1a", size = 57745, upload-time = "2025-02-16T18:45:42.351Z" },
-]
-
[[package]]
name = "flake8"
version = "7.3.0"
source = { registry = "https://pypi.org/simple" }
-resolution-markers = [
- "python_full_version >= '3.11'",
- "python_full_version == '3.10.*'",
- "python_full_version == '3.9.*'",
-]
dependencies = [
- { name = "mccabe", marker = "python_full_version >= '3.9'" },
- { name = "pycodestyle", version = "2.14.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" },
- { name = "pyflakes", version = "3.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" },
+ { name = "mccabe" },
+ { name = "pycodestyle" },
+ { name = "pyflakes" },
]
sdist = { url = "https://files.pythonhosted.org/packages/9b/af/fbfe3c4b5a657d79e5c47a2827a362f9e1b763336a52f926126aa6dc7123/flake8-7.3.0.tar.gz", hash = "sha256:fe044858146b9fc69b551a4b490d69cf960fcb78ad1edcb84e7fbb1b4a8e3872", size = 48326, upload-time = "2025-06-20T19:31:35.838Z" }
wheels = [
@@ -449,36 +534,26 @@ wheels = [
[[package]]
name = "griffe"
-version = "1.4.0"
+version = "1.8.0"
source = { registry = "https://pypi.org/simple" }
-resolution-markers = [
- "python_full_version >= '3.8.1' and python_full_version < '3.9'",
- "python_full_version < '3.8.1'",
-]
dependencies = [
- { name = "astunparse", marker = "python_full_version < '3.9'" },
- { name = "colorama", marker = "python_full_version < '3.9'" },
+ { name = "colorama" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/05/e9/b2c86ad9d69053e497a24ceb25d661094fb321ab4ed39a8b71793dcbae82/griffe-1.4.0.tar.gz", hash = "sha256:8fccc585896d13f1221035d32c50dec65830c87d23f9adb9b1e6f3d63574f7f5", size = 381028, upload-time = "2024-10-11T12:53:54.414Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/dd/72/10c5799440ce6f3001b7913988b50a99d7b156da71fe19be06178d5a2dd5/griffe-1.8.0.tar.gz", hash = "sha256:0b4658443858465c13b2de07ff5e15a1032bc889cfafad738a476b8b97bb28d7", size = 401098, upload-time = "2025-07-22T23:45:54.629Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/22/7c/e9e66869c2e4c9b378474e49c993128ec0131ef4721038b6d06e50538caf/griffe-1.4.0-py3-none-any.whl", hash = "sha256:e589de8b8c137e99a46ec45f9598fc0ac5b6868ce824b24db09c02d117b89bc5", size = 127015, upload-time = "2024-10-11T12:53:52.383Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/c4/a839fcc28bebfa72925d9121c4d39398f77f95bcba0cf26c972a0cfb1de7/griffe-1.8.0-py3-none-any.whl", hash = "sha256:110faa744b2c5c84dd432f4fa9aa3b14805dd9519777dd55e8db214320593b02", size = 132487, upload-time = "2025-07-22T23:45:52.778Z" },
]
[[package]]
-name = "griffe"
-version = "1.8.0"
+name = "griffe-public-wildcard-imports"
+version = "0.2.1"
source = { registry = "https://pypi.org/simple" }
-resolution-markers = [
- "python_full_version >= '3.11'",
- "python_full_version == '3.10.*'",
- "python_full_version == '3.9.*'",
-]
dependencies = [
- { name = "colorama", marker = "python_full_version >= '3.9'" },
+ { name = "griffe" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/dd/72/10c5799440ce6f3001b7913988b50a99d7b156da71fe19be06178d5a2dd5/griffe-1.8.0.tar.gz", hash = "sha256:0b4658443858465c13b2de07ff5e15a1032bc889cfafad738a476b8b97bb28d7", size = 401098, upload-time = "2025-07-22T23:45:54.629Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/c3/3c/53330b357cd7e52463159e33880bdd7aad7bb2cbef16be63a0e20cca84b1/griffe_public_wildcard_imports-0.2.1.tar.gz", hash = "sha256:2f8279e5e0520a19d15d1215f1a236d8bdc8722c59e14565dd216cd0d979e2e6", size = 31212, upload-time = "2024-08-18T13:53:32.071Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/bf/c4/a839fcc28bebfa72925d9121c4d39398f77f95bcba0cf26c972a0cfb1de7/griffe-1.8.0-py3-none-any.whl", hash = "sha256:110faa744b2c5c84dd432f4fa9aa3b14805dd9519777dd55e8db214320593b02", size = 132487, upload-time = "2025-07-22T23:45:52.778Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/b0/670b26b8237ff9b47fa1ba4427cee5b9ebfd5f4125941049f9c3ddb82cd1/griffe_public_wildcard_imports-0.2.1-py3-none-any.whl", hash = "sha256:5def6502e5af61bba1dffa0e7129c1e147b3c1e609f841cffae470b91db93672", size = 4815, upload-time = "2024-08-18T13:53:30.219Z" },
]
[[package]]
@@ -490,32 +565,12 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" },
]
-[[package]]
-name = "importlib-metadata"
-version = "8.5.0"
-source = { registry = "https://pypi.org/simple" }
-resolution-markers = [
- "python_full_version >= '3.8.1' and python_full_version < '3.9'",
- "python_full_version < '3.8.1'",
-]
-dependencies = [
- { name = "zipp", version = "3.20.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/cd/12/33e59336dca5be0c398a7482335911a33aa0e20776128f038019f1a95f1b/importlib_metadata-8.5.0.tar.gz", hash = "sha256:71522656f0abace1d072b9e5481a48f07c138e00f079c38c8f883823f9c26bd7", size = 55304, upload-time = "2024-09-11T14:56:08.937Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/a0/d9/a1e041c5e7caa9a05c925f4bdbdfb7f006d1f74996af53467bc394c97be7/importlib_metadata-8.5.0-py3-none-any.whl", hash = "sha256:45e54197d28b7a7f1559e60b95e7c567032b602131fbd588f1497f47880aa68b", size = 26514, upload-time = "2024-09-11T14:56:07.019Z" },
-]
-
[[package]]
name = "importlib-metadata"
version = "8.7.0"
source = { registry = "https://pypi.org/simple" }
-resolution-markers = [
- "python_full_version == '3.10.*'",
- "python_full_version == '3.9.*'",
-]
dependencies = [
- { name = "zipp", version = "3.23.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9' and python_full_version < '3.11'" },
+ { name = "zipp", marker = "python_full_version < '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/76/66/650a33bd90f786193e4de4b3ad86ea60b53c89b669a5c7be931fac31cdb0/importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000", size = 56641, upload-time = "2025-04-27T15:29:01.736Z" }
wheels = [
@@ -536,8 +591,7 @@ name = "jinja2"
version = "3.1.6"
source = { registry = "https://pypi.org/simple" }
dependencies = [
- { name = "markupsafe", version = "2.1.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" },
- { name = "markupsafe", version = "3.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" },
+ { name = "markupsafe" },
]
sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" }
wheels = [
@@ -557,34 +611,10 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/72/b9/313e8f2f2e9517ae050a692ae7b3e4b3f17cc5e6dfea0db51fe14e586580/jinja2_ansible_filters-1.3.2-py3-none-any.whl", hash = "sha256:e1082f5564917649c76fed239117820610516ec10f87735d0338688800a55b34", size = 18975, upload-time = "2022-06-30T14:08:49.571Z" },
]
-[[package]]
-name = "markdown"
-version = "3.7"
-source = { registry = "https://pypi.org/simple" }
-resolution-markers = [
- "python_full_version >= '3.8.1' and python_full_version < '3.9'",
- "python_full_version < '3.8.1'",
-]
-dependencies = [
- { name = "importlib-metadata", version = "8.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/54/28/3af612670f82f4c056911fbbbb42760255801b3068c48de792d354ff4472/markdown-3.7.tar.gz", hash = "sha256:2ae2471477cfd02dbbf038d5d9bc226d40def84b4fe2986e49b59b6b472bbed2", size = 357086, upload-time = "2024-08-16T15:55:17.812Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/3f/08/83871f3c50fc983b88547c196d11cf8c3340e37c32d2e9d6152abe2c61f7/Markdown-3.7-py3-none-any.whl", hash = "sha256:7eb6df5690b81a1d7942992c97fad2938e956e79df20cbc6186e9c3a77b1c803", size = 106349, upload-time = "2024-08-16T15:55:16.176Z" },
-]
-
[[package]]
name = "markdown"
version = "3.8.2"
source = { registry = "https://pypi.org/simple" }
-resolution-markers = [
- "python_full_version >= '3.11'",
- "python_full_version == '3.10.*'",
- "python_full_version == '3.9.*'",
-]
-dependencies = [
- { name = "importlib-metadata", version = "8.7.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" },
-]
sdist = { url = "https://files.pythonhosted.org/packages/d7/c2/4ab49206c17f75cb08d6311171f2d65798988db4360c4d1485bd0eedd67c/markdown-3.8.2.tar.gz", hash = "sha256:247b9a70dd12e27f67431ce62523e675b866d254f900c4fe75ce3dda62237c45", size = 362071, upload-time = "2025-06-19T17:12:44.483Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/96/2b/34cc11786bc00d0f04d0f5fdc3a2b1ae0b6239eef72d3d345805f9ad92a1/markdown-3.8.2-py3-none-any.whl", hash = "sha256:5c83764dbd4e00bdd94d85a19b8d55ccca20fe35b2e678a1422b380324dd5f24", size = 106827, upload-time = "2025-06-19T17:12:42.994Z" },
@@ -602,77 +632,10 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528, upload-time = "2023-06-03T06:41:11.019Z" },
]
-[[package]]
-name = "markupsafe"
-version = "2.1.5"
-source = { registry = "https://pypi.org/simple" }
-resolution-markers = [
- "python_full_version >= '3.8.1' and python_full_version < '3.9'",
- "python_full_version < '3.8.1'",
-]
-sdist = { url = "https://files.pythonhosted.org/packages/87/5b/aae44c6655f3801e81aa3eef09dbbf012431987ba564d7231722f68df02d/MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b", size = 19384, upload-time = "2024-02-02T16:31:22.863Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/e4/54/ad5eb37bf9d51800010a74e4665425831a9db4e7c4e0fde4352e391e808e/MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc", size = 18206, upload-time = "2024-02-02T16:30:04.105Z" },
- { url = "https://files.pythonhosted.org/packages/6a/4a/a4d49415e600bacae038c67f9fecc1d5433b9d3c71a4de6f33537b89654c/MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5", size = 14079, upload-time = "2024-02-02T16:30:06.5Z" },
- { url = "https://files.pythonhosted.org/packages/0a/7b/85681ae3c33c385b10ac0f8dd025c30af83c78cec1c37a6aa3b55e67f5ec/MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46", size = 26620, upload-time = "2024-02-02T16:30:08.31Z" },
- { url = "https://files.pythonhosted.org/packages/7c/52/2b1b570f6b8b803cef5ac28fdf78c0da318916c7d2fe9402a84d591b394c/MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f", size = 25818, upload-time = "2024-02-02T16:30:09.577Z" },
- { url = "https://files.pythonhosted.org/packages/29/fe/a36ba8c7ca55621620b2d7c585313efd10729e63ef81e4e61f52330da781/MarkupSafe-2.1.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900", size = 25493, upload-time = "2024-02-02T16:30:11.488Z" },
- { url = "https://files.pythonhosted.org/packages/60/ae/9c60231cdfda003434e8bd27282b1f4e197ad5a710c14bee8bea8a9ca4f0/MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff", size = 30630, upload-time = "2024-02-02T16:30:13.144Z" },
- { url = "https://files.pythonhosted.org/packages/65/dc/1510be4d179869f5dafe071aecb3f1f41b45d37c02329dfba01ff59e5ac5/MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad", size = 29745, upload-time = "2024-02-02T16:30:14.222Z" },
- { url = "https://files.pythonhosted.org/packages/30/39/8d845dd7d0b0613d86e0ef89549bfb5f61ed781f59af45fc96496e897f3a/MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd", size = 30021, upload-time = "2024-02-02T16:30:16.032Z" },
- { url = "https://files.pythonhosted.org/packages/c7/5c/356a6f62e4f3c5fbf2602b4771376af22a3b16efa74eb8716fb4e328e01e/MarkupSafe-2.1.5-cp310-cp310-win32.whl", hash = "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4", size = 16659, upload-time = "2024-02-02T16:30:17.079Z" },
- { url = "https://files.pythonhosted.org/packages/69/48/acbf292615c65f0604a0c6fc402ce6d8c991276e16c80c46a8f758fbd30c/MarkupSafe-2.1.5-cp310-cp310-win_amd64.whl", hash = "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5", size = 17213, upload-time = "2024-02-02T16:30:18.251Z" },
- { url = "https://files.pythonhosted.org/packages/11/e7/291e55127bb2ae67c64d66cef01432b5933859dfb7d6949daa721b89d0b3/MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f", size = 18219, upload-time = "2024-02-02T16:30:19.988Z" },
- { url = "https://files.pythonhosted.org/packages/6b/cb/aed7a284c00dfa7c0682d14df85ad4955a350a21d2e3b06d8240497359bf/MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2", size = 14098, upload-time = "2024-02-02T16:30:21.063Z" },
- { url = "https://files.pythonhosted.org/packages/1c/cf/35fe557e53709e93feb65575c93927942087e9b97213eabc3fe9d5b25a55/MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced", size = 29014, upload-time = "2024-02-02T16:30:22.926Z" },
- { url = "https://files.pythonhosted.org/packages/97/18/c30da5e7a0e7f4603abfc6780574131221d9148f323752c2755d48abad30/MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5", size = 28220, upload-time = "2024-02-02T16:30:24.76Z" },
- { url = "https://files.pythonhosted.org/packages/0c/40/2e73e7d532d030b1e41180807a80d564eda53babaf04d65e15c1cf897e40/MarkupSafe-2.1.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c", size = 27756, upload-time = "2024-02-02T16:30:25.877Z" },
- { url = "https://files.pythonhosted.org/packages/18/46/5dca760547e8c59c5311b332f70605d24c99d1303dd9a6e1fc3ed0d73561/MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f", size = 33988, upload-time = "2024-02-02T16:30:26.935Z" },
- { url = "https://files.pythonhosted.org/packages/6d/c5/27febe918ac36397919cd4a67d5579cbbfa8da027fa1238af6285bb368ea/MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a", size = 32718, upload-time = "2024-02-02T16:30:28.111Z" },
- { url = "https://files.pythonhosted.org/packages/f8/81/56e567126a2c2bc2684d6391332e357589a96a76cb9f8e5052d85cb0ead8/MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f", size = 33317, upload-time = "2024-02-02T16:30:29.214Z" },
- { url = "https://files.pythonhosted.org/packages/00/0b/23f4b2470accb53285c613a3ab9ec19dc944eaf53592cb6d9e2af8aa24cc/MarkupSafe-2.1.5-cp311-cp311-win32.whl", hash = "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906", size = 16670, upload-time = "2024-02-02T16:30:30.915Z" },
- { url = "https://files.pythonhosted.org/packages/b7/a2/c78a06a9ec6d04b3445a949615c4c7ed86a0b2eb68e44e7541b9d57067cc/MarkupSafe-2.1.5-cp311-cp311-win_amd64.whl", hash = "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617", size = 17224, upload-time = "2024-02-02T16:30:32.09Z" },
- { url = "https://files.pythonhosted.org/packages/53/bd/583bf3e4c8d6a321938c13f49d44024dbe5ed63e0a7ba127e454a66da974/MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1", size = 18215, upload-time = "2024-02-02T16:30:33.081Z" },
- { url = "https://files.pythonhosted.org/packages/48/d6/e7cd795fc710292c3af3a06d80868ce4b02bfbbf370b7cee11d282815a2a/MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4", size = 14069, upload-time = "2024-02-02T16:30:34.148Z" },
- { url = "https://files.pythonhosted.org/packages/51/b5/5d8ec796e2a08fc814a2c7d2584b55f889a55cf17dd1a90f2beb70744e5c/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee", size = 29452, upload-time = "2024-02-02T16:30:35.149Z" },
- { url = "https://files.pythonhosted.org/packages/0a/0d/2454f072fae3b5a137c119abf15465d1771319dfe9e4acbb31722a0fff91/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5", size = 28462, upload-time = "2024-02-02T16:30:36.166Z" },
- { url = "https://files.pythonhosted.org/packages/2d/75/fd6cb2e68780f72d47e6671840ca517bda5ef663d30ada7616b0462ad1e3/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b", size = 27869, upload-time = "2024-02-02T16:30:37.834Z" },
- { url = "https://files.pythonhosted.org/packages/b0/81/147c477391c2750e8fc7705829f7351cf1cd3be64406edcf900dc633feb2/MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a", size = 33906, upload-time = "2024-02-02T16:30:39.366Z" },
- { url = "https://files.pythonhosted.org/packages/8b/ff/9a52b71839d7a256b563e85d11050e307121000dcebc97df120176b3ad93/MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f", size = 32296, upload-time = "2024-02-02T16:30:40.413Z" },
- { url = "https://files.pythonhosted.org/packages/88/07/2dc76aa51b481eb96a4c3198894f38b480490e834479611a4053fbf08623/MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169", size = 33038, upload-time = "2024-02-02T16:30:42.243Z" },
- { url = "https://files.pythonhosted.org/packages/96/0c/620c1fb3661858c0e37eb3cbffd8c6f732a67cd97296f725789679801b31/MarkupSafe-2.1.5-cp312-cp312-win32.whl", hash = "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad", size = 16572, upload-time = "2024-02-02T16:30:43.326Z" },
- { url = "https://files.pythonhosted.org/packages/3f/14/c3554d512d5f9100a95e737502f4a2323a1959f6d0d01e0d0997b35f7b10/MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb", size = 17127, upload-time = "2024-02-02T16:30:44.418Z" },
- { url = "https://files.pythonhosted.org/packages/f8/ff/2c942a82c35a49df5de3a630ce0a8456ac2969691b230e530ac12314364c/MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a", size = 18192, upload-time = "2024-02-02T16:30:57.715Z" },
- { url = "https://files.pythonhosted.org/packages/4f/14/6f294b9c4f969d0c801a4615e221c1e084722ea6114ab2114189c5b8cbe0/MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46", size = 14072, upload-time = "2024-02-02T16:30:58.844Z" },
- { url = "https://files.pythonhosted.org/packages/81/d4/fd74714ed30a1dedd0b82427c02fa4deec64f173831ec716da11c51a50aa/MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532", size = 26928, upload-time = "2024-02-02T16:30:59.922Z" },
- { url = "https://files.pythonhosted.org/packages/c7/bd/50319665ce81bb10e90d1cf76f9e1aa269ea6f7fa30ab4521f14d122a3df/MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab", size = 26106, upload-time = "2024-02-02T16:31:01.582Z" },
- { url = "https://files.pythonhosted.org/packages/4c/6f/f2b0f675635b05f6afd5ea03c094557bdb8622fa8e673387444fe8d8e787/MarkupSafe-2.1.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68", size = 25781, upload-time = "2024-02-02T16:31:02.71Z" },
- { url = "https://files.pythonhosted.org/packages/51/e0/393467cf899b34a9d3678e78961c2c8cdf49fb902a959ba54ece01273fb1/MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0", size = 30518, upload-time = "2024-02-02T16:31:04.392Z" },
- { url = "https://files.pythonhosted.org/packages/f6/02/5437e2ad33047290dafced9df741d9efc3e716b75583bbd73a9984f1b6f7/MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4", size = 29669, upload-time = "2024-02-02T16:31:05.53Z" },
- { url = "https://files.pythonhosted.org/packages/0e/7d/968284145ffd9d726183ed6237c77938c021abacde4e073020f920e060b2/MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3", size = 29933, upload-time = "2024-02-02T16:31:06.636Z" },
- { url = "https://files.pythonhosted.org/packages/bf/f3/ecb00fc8ab02b7beae8699f34db9357ae49d9f21d4d3de6f305f34fa949e/MarkupSafe-2.1.5-cp38-cp38-win32.whl", hash = "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff", size = 16656, upload-time = "2024-02-02T16:31:07.767Z" },
- { url = "https://files.pythonhosted.org/packages/92/21/357205f03514a49b293e214ac39de01fadd0970a6e05e4bf1ddd0ffd0881/MarkupSafe-2.1.5-cp38-cp38-win_amd64.whl", hash = "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029", size = 17206, upload-time = "2024-02-02T16:31:08.843Z" },
- { url = "https://files.pythonhosted.org/packages/0f/31/780bb297db036ba7b7bbede5e1d7f1e14d704ad4beb3ce53fb495d22bc62/MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf", size = 18193, upload-time = "2024-02-02T16:31:10.155Z" },
- { url = "https://files.pythonhosted.org/packages/6c/77/d77701bbef72892affe060cdacb7a2ed7fd68dae3b477a8642f15ad3b132/MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2", size = 14073, upload-time = "2024-02-02T16:31:11.442Z" },
- { url = "https://files.pythonhosted.org/packages/d9/a7/1e558b4f78454c8a3a0199292d96159eb4d091f983bc35ef258314fe7269/MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8", size = 26486, upload-time = "2024-02-02T16:31:12.488Z" },
- { url = "https://files.pythonhosted.org/packages/5f/5a/360da85076688755ea0cceb92472923086993e86b5613bbae9fbc14136b0/MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3", size = 25685, upload-time = "2024-02-02T16:31:13.726Z" },
- { url = "https://files.pythonhosted.org/packages/6a/18/ae5a258e3401f9b8312f92b028c54d7026a97ec3ab20bfaddbdfa7d8cce8/MarkupSafe-2.1.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465", size = 25338, upload-time = "2024-02-02T16:31:14.812Z" },
- { url = "https://files.pythonhosted.org/packages/0b/cc/48206bd61c5b9d0129f4d75243b156929b04c94c09041321456fd06a876d/MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e", size = 30439, upload-time = "2024-02-02T16:31:15.946Z" },
- { url = "https://files.pythonhosted.org/packages/d1/06/a41c112ab9ffdeeb5f77bc3e331fdadf97fa65e52e44ba31880f4e7f983c/MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea", size = 29531, upload-time = "2024-02-02T16:31:17.13Z" },
- { url = "https://files.pythonhosted.org/packages/02/8c/ab9a463301a50dab04d5472e998acbd4080597abc048166ded5c7aa768c8/MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6", size = 29823, upload-time = "2024-02-02T16:31:18.247Z" },
- { url = "https://files.pythonhosted.org/packages/bc/29/9bc18da763496b055d8e98ce476c8e718dcfd78157e17f555ce6dd7d0895/MarkupSafe-2.1.5-cp39-cp39-win32.whl", hash = "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf", size = 16658, upload-time = "2024-02-02T16:31:19.583Z" },
- { url = "https://files.pythonhosted.org/packages/f6/f8/4da07de16f10551ca1f640c92b5f316f9394088b183c6a57183df6de5ae4/MarkupSafe-2.1.5-cp39-cp39-win_amd64.whl", hash = "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5", size = 17211, upload-time = "2024-02-02T16:31:20.96Z" },
-]
-
[[package]]
name = "markupsafe"
version = "3.0.2"
source = { registry = "https://pypi.org/simple" }
-resolution-markers = [
- "python_full_version >= '3.11'",
- "python_full_version == '3.10.*'",
- "python_full_version == '3.9.*'",
-]
sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537, upload-time = "2024-10-18T15:21:54.129Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/04/90/d08277ce111dd22f77149fd1a5d4653eeb3b3eaacbdfcbae5afb2600eebd/MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8", size = 14357, upload-time = "2024-10-18T15:20:51.44Z" },
@@ -725,16 +688,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098, upload-time = "2024-10-18T15:21:40.813Z" },
{ url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208, upload-time = "2024-10-18T15:21:41.814Z" },
{ url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739, upload-time = "2024-10-18T15:21:42.784Z" },
- { url = "https://files.pythonhosted.org/packages/a7/ea/9b1530c3fdeeca613faeb0fb5cbcf2389d816072fab72a71b45749ef6062/MarkupSafe-3.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a", size = 14344, upload-time = "2024-10-18T15:21:43.721Z" },
- { url = "https://files.pythonhosted.org/packages/4b/c2/fbdbfe48848e7112ab05e627e718e854d20192b674952d9042ebd8c9e5de/MarkupSafe-3.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff", size = 12389, upload-time = "2024-10-18T15:21:44.666Z" },
- { url = "https://files.pythonhosted.org/packages/f0/25/7a7c6e4dbd4f867d95d94ca15449e91e52856f6ed1905d58ef1de5e211d0/MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13", size = 21607, upload-time = "2024-10-18T15:21:45.452Z" },
- { url = "https://files.pythonhosted.org/packages/53/8f/f339c98a178f3c1e545622206b40986a4c3307fe39f70ccd3d9df9a9e425/MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144", size = 20728, upload-time = "2024-10-18T15:21:46.295Z" },
- { url = "https://files.pythonhosted.org/packages/1a/03/8496a1a78308456dbd50b23a385c69b41f2e9661c67ea1329849a598a8f9/MarkupSafe-3.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29", size = 20826, upload-time = "2024-10-18T15:21:47.134Z" },
- { url = "https://files.pythonhosted.org/packages/e6/cf/0a490a4bd363048c3022f2f475c8c05582179bb179defcee4766fb3dcc18/MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0", size = 21843, upload-time = "2024-10-18T15:21:48.334Z" },
- { url = "https://files.pythonhosted.org/packages/19/a3/34187a78613920dfd3cdf68ef6ce5e99c4f3417f035694074beb8848cd77/MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0", size = 21219, upload-time = "2024-10-18T15:21:49.587Z" },
- { url = "https://files.pythonhosted.org/packages/17/d8/5811082f85bb88410ad7e452263af048d685669bbbfb7b595e8689152498/MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178", size = 20946, upload-time = "2024-10-18T15:21:50.441Z" },
- { url = "https://files.pythonhosted.org/packages/7c/31/bd635fb5989440d9365c5e3c47556cfea121c7803f5034ac843e8f37c2f2/MarkupSafe-3.0.2-cp39-cp39-win32.whl", hash = "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f", size = 15063, upload-time = "2024-10-18T15:21:51.385Z" },
- { url = "https://files.pythonhosted.org/packages/b3/73/085399401383ce949f727afec55ec3abd76648d04b9f22e1c0e99cb4bec3/MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a", size = 15506, upload-time = "2024-10-18T15:21:52.974Z" },
]
[[package]]
@@ -778,24 +731,18 @@ name = "mkdocs"
version = "1.6.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
- { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
- { name = "click", version = "8.2.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
+ { name = "click" },
{ name = "colorama", marker = "sys_platform == 'win32'" },
{ name = "ghp-import" },
- { name = "importlib-metadata", version = "8.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" },
- { name = "importlib-metadata", version = "8.7.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" },
{ name = "jinja2" },
- { name = "markdown", version = "3.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" },
- { name = "markdown", version = "3.8.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" },
- { name = "markupsafe", version = "2.1.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" },
- { name = "markupsafe", version = "3.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" },
+ { name = "markdown" },
+ { name = "markupsafe" },
{ name = "mergedeep" },
{ name = "mkdocs-get-deps" },
{ name = "packaging" },
{ name = "pathspec" },
{ name = "pyyaml" },
- { name = "pyyaml-env-tag", version = "0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" },
- { name = "pyyaml-env-tag", version = "1.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" },
+ { name = "pyyaml-env-tag" },
{ name = "watchdog" },
]
sdist = { url = "https://files.pythonhosted.org/packages/bc/c6/bbd4f061bd16b378247f12953ffcb04786a618ce5e904b8c5a01a0309061/mkdocs-1.6.1.tar.gz", hash = "sha256:7b432f01d928c084353ab39c57282f29f92136665bdd6abf7c1ec8d822ef86f2", size = 3889159, upload-time = "2024-08-30T12:24:06.899Z" }
@@ -803,37 +750,14 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/22/5b/dbc6a8cddc9cfa9c4971d59fb12bb8d42e161b7e7f8cc89e49137c5b279c/mkdocs-1.6.1-py3-none-any.whl", hash = "sha256:db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e", size = 3864451, upload-time = "2024-08-30T12:24:05.054Z" },
]
-[[package]]
-name = "mkdocs-autorefs"
-version = "1.2.0"
-source = { registry = "https://pypi.org/simple" }
-resolution-markers = [
- "python_full_version >= '3.8.1' and python_full_version < '3.9'",
- "python_full_version < '3.8.1'",
-]
-dependencies = [
- { name = "markdown", version = "3.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" },
- { name = "markupsafe", version = "2.1.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" },
- { name = "mkdocs", marker = "python_full_version < '3.9'" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/fb/ae/0f1154c614d6a8b8a36fff084e5b82af3a15f7d2060cf0dcdb1c53297a71/mkdocs_autorefs-1.2.0.tar.gz", hash = "sha256:a86b93abff653521bda71cf3fc5596342b7a23982093915cb74273f67522190f", size = 40262, upload-time = "2024-09-01T18:29:18.514Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/71/26/4d39d52ea2219604053a4d05b98e90d6a335511cc01806436ec4886b1028/mkdocs_autorefs-1.2.0-py3-none-any.whl", hash = "sha256:d588754ae89bd0ced0c70c06f58566a4ee43471eeeee5202427da7de9ef85a2f", size = 16522, upload-time = "2024-09-01T18:29:16.605Z" },
-]
-
[[package]]
name = "mkdocs-autorefs"
version = "1.4.2"
source = { registry = "https://pypi.org/simple" }
-resolution-markers = [
- "python_full_version >= '3.11'",
- "python_full_version == '3.10.*'",
- "python_full_version == '3.9.*'",
-]
dependencies = [
- { name = "markdown", version = "3.8.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" },
- { name = "markupsafe", version = "3.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" },
- { name = "mkdocs", marker = "python_full_version >= '3.9'" },
+ { name = "markdown" },
+ { name = "markupsafe" },
+ { name = "mkdocs" },
]
sdist = { url = "https://files.pythonhosted.org/packages/47/0c/c9826f35b99c67fa3a7cddfa094c1a6c43fafde558c309c6e4403e5b37dc/mkdocs_autorefs-1.4.2.tar.gz", hash = "sha256:e2ebe1abd2b67d597ed19378c0fff84d73d1dbce411fce7a7cc6f161888b6749", size = 54961, upload-time = "2025-05-20T13:09:09.886Z" }
wheels = [
@@ -845,10 +769,7 @@ name = "mkdocs-entangled-plugin"
version = "0.2.0"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
- "python_full_version == '3.10.*'",
- "python_full_version == '3.9.*'",
- "python_full_version >= '3.8.1' and python_full_version < '3.9'",
- "python_full_version < '3.8.1'",
+ "python_full_version < '3.11'",
]
dependencies = [
{ name = "mkdocs", marker = "python_full_version < '3.11'" },
@@ -863,7 +784,9 @@ name = "mkdocs-entangled-plugin"
version = "0.4.0"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
- "python_full_version >= '3.11'",
+ "python_full_version >= '3.14' and platform_python_implementation != 'PyPy'",
+ "python_full_version >= '3.11' and python_full_version < '3.14' and platform_python_implementation != 'PyPy'",
+ "python_full_version >= '3.11' and platform_python_implementation == 'PyPy'",
]
dependencies = [
{ name = "entangled-cli", marker = "python_full_version >= '3.11'" },
@@ -880,11 +803,8 @@ name = "mkdocs-get-deps"
version = "0.2.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
- { name = "importlib-metadata", version = "8.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" },
- { name = "importlib-metadata", version = "8.7.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" },
{ name = "mergedeep" },
- { name = "platformdirs", version = "4.3.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" },
- { name = "platformdirs", version = "4.3.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" },
+ { name = "platformdirs" },
{ name = "pyyaml" },
]
sdist = { url = "https://files.pythonhosted.org/packages/98/f5/ed29cd50067784976f25ed0ed6fcd3c2ce9eb90650aa3b2796ddf7b6870b/mkdocs_get_deps-0.2.0.tar.gz", hash = "sha256:162b3d129c7fad9b19abfdcb9c1458a651628e4b1dea628ac68790fb3061c60c", size = 10239, upload-time = "2023-11-20T17:51:09.981Z" }
@@ -892,35 +812,13 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/9f/d4/029f984e8d3f3b6b726bd33cafc473b75e9e44c0f7e80a5b29abc466bdea/mkdocs_get_deps-0.2.0-py3-none-any.whl", hash = "sha256:2bf11d0b133e77a0dd036abeeb06dec8775e46efa526dc70667d8863eefc6134", size = 9521, upload-time = "2023-11-20T17:51:08.587Z" },
]
-[[package]]
-name = "mkdocs-include-markdown-plugin"
-version = "6.2.2"
-source = { registry = "https://pypi.org/simple" }
-resolution-markers = [
- "python_full_version >= '3.8.1' and python_full_version < '3.9'",
- "python_full_version < '3.8.1'",
-]
-dependencies = [
- { name = "mkdocs", marker = "python_full_version < '3.9'" },
- { name = "wcmatch", version = "10.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/ee/fe/4bb438d0f58995f81e2616d640f7efe0df9b1f992cba706a9453676c9140/mkdocs_include_markdown_plugin-6.2.2.tar.gz", hash = "sha256:f2bd5026650492a581d2fd44be6c22f90391910d76582b96a34c264f2d17875d", size = 21045, upload-time = "2024-08-10T23:36:41.503Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/50/d9/7b2b09b4870a2cd5a80628c74553307205a8474aabe128b66e305b56ac30/mkdocs_include_markdown_plugin-6.2.2-py3-none-any.whl", hash = "sha256:d293950f6499d2944291ca7b9bc4a60e652bbfd3e3a42b564f6cceee268694e7", size = 24643, upload-time = "2024-08-10T23:36:39.736Z" },
-]
-
[[package]]
name = "mkdocs-include-markdown-plugin"
version = "7.1.6"
source = { registry = "https://pypi.org/simple" }
-resolution-markers = [
- "python_full_version >= '3.11'",
- "python_full_version == '3.10.*'",
- "python_full_version == '3.9.*'",
-]
dependencies = [
- { name = "mkdocs", marker = "python_full_version >= '3.9'" },
- { name = "wcmatch", version = "10.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" },
+ { name = "mkdocs" },
+ { name = "wcmatch" },
]
sdist = { url = "https://files.pythonhosted.org/packages/2c/17/988d97ac6849b196f54d45ca9c60ca894880c160a512785f03834704b3d9/mkdocs_include_markdown_plugin-7.1.6.tar.gz", hash = "sha256:a0753cb82704c10a287f1e789fc9848f82b6beb8749814b24b03dd9f67816677", size = 23391, upload-time = "2025-06-13T18:25:51.193Z" }
wheels = [
@@ -933,18 +831,15 @@ version = "9.6.15"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "babel" },
- { name = "backrefs", version = "5.7.post1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" },
- { name = "backrefs", version = "5.9", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" },
+ { name = "backrefs" },
{ name = "colorama" },
{ name = "jinja2" },
- { name = "markdown", version = "3.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" },
- { name = "markdown", version = "3.8.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" },
+ { name = "markdown" },
{ name = "mkdocs" },
{ name = "mkdocs-material-extensions" },
{ name = "paginate" },
{ name = "pygments" },
- { name = "pymdown-extensions", version = "10.15", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" },
- { name = "pymdown-extensions", version = "10.16", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" },
+ { name = "pymdown-extensions" },
{ name = "requests" },
]
sdist = { url = "https://files.pythonhosted.org/packages/95/c1/f804ba2db2ddc2183e900befe7dad64339a34fa935034e1ab405289d0a97/mkdocs_material-9.6.15.tar.gz", hash = "sha256:64adf8fa8dba1a17905b6aee1894a5aafd966d4aeb44a11088519b0f5ca4f1b5", size = 3951836, upload-time = "2025-07-01T10:14:15.671Z" }
@@ -961,53 +856,17 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/5b/54/662a4743aa81d9582ee9339d4ffa3c8fd40a4965e033d77b9da9774d3960/mkdocs_material_extensions-1.3.1-py3-none-any.whl", hash = "sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31", size = 8728, upload-time = "2023-11-22T19:09:43.465Z" },
]
-[[package]]
-name = "mkdocstrings"
-version = "0.26.1"
-source = { registry = "https://pypi.org/simple" }
-resolution-markers = [
- "python_full_version >= '3.8.1' and python_full_version < '3.9'",
- "python_full_version < '3.8.1'",
-]
-dependencies = [
- { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" },
- { name = "importlib-metadata", version = "8.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" },
- { name = "jinja2", marker = "python_full_version < '3.9'" },
- { name = "markdown", version = "3.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" },
- { name = "markupsafe", version = "2.1.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" },
- { name = "mkdocs", marker = "python_full_version < '3.9'" },
- { name = "mkdocs-autorefs", version = "1.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" },
- { name = "platformdirs", version = "4.3.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" },
- { name = "pymdown-extensions", version = "10.15", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" },
- { name = "typing-extensions", version = "4.13.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/e6/bf/170ff04de72227f715d67da32950c7b8434449f3805b2ec3dd1085db4d7c/mkdocstrings-0.26.1.tar.gz", hash = "sha256:bb8b8854d6713d5348ad05b069a09f3b79edbc6a0f33a34c6821141adb03fe33", size = 92677, upload-time = "2024-09-06T10:26:06.736Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/23/cc/8ba127aaee5d1e9046b0d33fa5b3d17da95a9d705d44902792e0569257fd/mkdocstrings-0.26.1-py3-none-any.whl", hash = "sha256:29738bfb72b4608e8e55cc50fb8a54f325dc7ebd2014e4e3881a49892d5983cf", size = 29643, upload-time = "2024-09-06T10:26:04.498Z" },
-]
-
-[package.optional-dependencies]
-python = [
- { name = "mkdocstrings-python", version = "1.11.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" },
-]
-
[[package]]
name = "mkdocstrings"
version = "0.30.0"
source = { registry = "https://pypi.org/simple" }
-resolution-markers = [
- "python_full_version >= '3.11'",
- "python_full_version == '3.10.*'",
- "python_full_version == '3.9.*'",
-]
dependencies = [
- { name = "importlib-metadata", version = "8.7.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" },
- { name = "jinja2", marker = "python_full_version >= '3.9'" },
- { name = "markdown", version = "3.8.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" },
- { name = "markupsafe", version = "3.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" },
- { name = "mkdocs", marker = "python_full_version >= '3.9'" },
- { name = "mkdocs-autorefs", version = "1.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" },
- { name = "pymdown-extensions", version = "10.16", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" },
+ { name = "jinja2" },
+ { name = "markdown" },
+ { name = "markupsafe" },
+ { name = "mkdocs" },
+ { name = "mkdocs-autorefs" },
+ { name = "pymdown-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/e2/0a/7e4776217d4802009c8238c75c5345e23014a4706a8414a62c0498858183/mkdocstrings-0.30.0.tar.gz", hash = "sha256:5d8019b9c31ddacd780b6784ffcdd6f21c408f34c0bd1103b5351d609d5b4444", size = 106597, upload-time = "2025-07-22T23:48:45.998Z" }
wheels = [
@@ -1016,41 +875,18 @@ wheels = [
[package.optional-dependencies]
python = [
- { name = "mkdocstrings-python", version = "1.16.12", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" },
-]
-
-[[package]]
-name = "mkdocstrings-python"
-version = "1.11.1"
-source = { registry = "https://pypi.org/simple" }
-resolution-markers = [
- "python_full_version >= '3.8.1' and python_full_version < '3.9'",
- "python_full_version < '3.8.1'",
-]
-dependencies = [
- { name = "griffe", version = "1.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" },
- { name = "mkdocs-autorefs", version = "1.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" },
- { name = "mkdocstrings", version = "0.26.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/fc/ba/534c934cd0a809f51c91332d6ed278782ee4126b8ba8db02c2003f162b47/mkdocstrings_python-1.11.1.tar.gz", hash = "sha256:8824b115c5359304ab0b5378a91f6202324a849e1da907a3485b59208b797322", size = 166890, upload-time = "2024-09-03T17:20:54.904Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/2f/f2/2a2c48fda645ac6bbe73bcc974587a579092b6868e6ff8bc6d177f4db38a/mkdocstrings_python-1.11.1-py3-none-any.whl", hash = "sha256:a21a1c05acef129a618517bb5aae3e33114f569b11588b1e7af3e9d4061a71af", size = 109297, upload-time = "2024-09-03T17:20:52.621Z" },
+ { name = "mkdocstrings-python" },
]
[[package]]
name = "mkdocstrings-python"
version = "1.16.12"
source = { registry = "https://pypi.org/simple" }
-resolution-markers = [
- "python_full_version >= '3.11'",
- "python_full_version == '3.10.*'",
- "python_full_version == '3.9.*'",
-]
dependencies = [
- { name = "griffe", version = "1.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" },
- { name = "mkdocs-autorefs", version = "1.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" },
- { name = "mkdocstrings", version = "0.30.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" },
- { name = "typing-extensions", version = "4.14.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9' and python_full_version < '3.11'" },
+ { name = "griffe" },
+ { name = "mkdocs-autorefs" },
+ { name = "mkdocstrings" },
+ { name = "typing-extensions", marker = "python_full_version < '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/bf/ed/b886f8c714fd7cccc39b79646b627dbea84cd95c46be43459ef46852caf0/mkdocstrings_python-1.16.12.tar.gz", hash = "sha256:9b9eaa066e0024342d433e332a41095c4e429937024945fea511afe58f63175d", size = 206065, upload-time = "2025-06-03T12:52:49.276Z" }
wheels = [
@@ -1064,8 +900,7 @@ source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "mypy-extensions" },
{ name = "tomli", marker = "python_full_version < '3.11'" },
- { name = "typing-extensions", version = "4.13.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" },
- { name = "typing-extensions", version = "4.14.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" },
+ { name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/e8/21/7e9e523537991d145ab8a0a2fd98548d67646dc2aaaf6091c31ad883e7c1/mypy-1.13.0.tar.gz", hash = "sha256:0291a61b6fbf3e6673e3405cfcc0e7650bebc7939659fdca2702958038bd835e", size = 3152532, upload-time = "2024-10-22T21:55:47.458Z" }
wheels = [
@@ -1089,16 +924,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/d1/1f/6b76be289a5a521bb1caedc1f08e76ff17ab59061007f201a8a18cc514d1/mypy-1.13.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de2904956dac40ced10931ac967ae63c5089bd498542194b436eb097a9f77bc8", size = 12584043, upload-time = "2024-10-22T21:55:06.231Z" },
{ url = "https://files.pythonhosted.org/packages/a6/83/5a85c9a5976c6f96e3a5a7591aa28b4a6ca3a07e9e5ba0cec090c8b596d6/mypy-1.13.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:7bfd8836970d33c2105562650656b6846149374dc8ed77d98424b40b09340ba7", size = 13036996, upload-time = "2024-10-22T21:55:25.811Z" },
{ url = "https://files.pythonhosted.org/packages/b4/59/c39a6f752f1f893fccbcf1bdd2aca67c79c842402b5283563d006a67cf76/mypy-1.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:9f73dba9ec77acb86457a8fc04b5239822df0c14a082564737833d2963677dbc", size = 9737709, upload-time = "2024-10-22T21:55:21.246Z" },
- { url = "https://files.pythonhosted.org/packages/5e/2a/13e9ad339131c0fba5c70584f639005a47088f5eed77081a3d00479df0ca/mypy-1.13.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:100fac22ce82925f676a734af0db922ecfea991e1d7ec0ceb1e115ebe501301a", size = 10955147, upload-time = "2024-10-22T21:55:39.445Z" },
- { url = "https://files.pythonhosted.org/packages/94/39/02929067dc16b72d78109195cfed349ac4ec85f3d52517ac62b9a5263685/mypy-1.13.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7bcb0bb7f42a978bb323a7c88f1081d1b5dee77ca86f4100735a6f541299d8fb", size = 10138373, upload-time = "2024-10-22T21:54:56.889Z" },
- { url = "https://files.pythonhosted.org/packages/4a/cc/066709bb01734e3dbbd1375749f8789bf9693f8b842344fc0cf52109694f/mypy-1.13.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bde31fc887c213e223bbfc34328070996061b0833b0a4cfec53745ed61f3519b", size = 12543621, upload-time = "2024-10-22T21:54:25.798Z" },
- { url = "https://files.pythonhosted.org/packages/f5/a2/124df839025348c7b9877d0ce134832a9249968e3ab36bb826bab0e9a1cf/mypy-1.13.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:07de989f89786f62b937851295ed62e51774722e5444a27cecca993fc3f9cd74", size = 13050348, upload-time = "2024-10-22T21:54:40.801Z" },
- { url = "https://files.pythonhosted.org/packages/45/86/cc94b1e7f7e756a63043cf425c24fb7470013ee1c032180282db75b1b335/mypy-1.13.0-cp38-cp38-win_amd64.whl", hash = "sha256:4bde84334fbe19bad704b3f5b78c4abd35ff1026f8ba72b29de70dda0916beb6", size = 9615311, upload-time = "2024-10-22T21:54:31.74Z" },
- { url = "https://files.pythonhosted.org/packages/5f/d4/b33ddd40dad230efb317898a2d1c267c04edba73bc5086bf77edeb410fb2/mypy-1.13.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0246bcb1b5de7f08f2826451abd947bf656945209b140d16ed317f65a17dc7dc", size = 11013906, upload-time = "2024-10-22T21:55:28.105Z" },
- { url = "https://files.pythonhosted.org/packages/f4/e6/f414bca465b44d01cd5f4a82761e15044bedd1bf8025c5af3cc64518fac5/mypy-1.13.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7f5b7deae912cf8b77e990b9280f170381fdfbddf61b4ef80927edd813163732", size = 10180657, upload-time = "2024-10-22T21:55:03.931Z" },
- { url = "https://files.pythonhosted.org/packages/38/e9/fc3865e417722f98d58409770be01afb961e2c1f99930659ff4ae7ca8b7e/mypy-1.13.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7029881ec6ffb8bc233a4fa364736789582c738217b133f1b55967115288a2bc", size = 12586394, upload-time = "2024-10-22T21:54:49.173Z" },
- { url = "https://files.pythonhosted.org/packages/2e/35/f4d8b6d2cb0b3dad63e96caf159419dda023f45a358c6c9ac582ccaee354/mypy-1.13.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:3e38b980e5681f28f033f3be86b099a247b13c491f14bb8b1e1e134d23bb599d", size = 13103591, upload-time = "2024-10-22T21:55:01.642Z" },
- { url = "https://files.pythonhosted.org/packages/22/1d/80594aef135f921dd52e142fa0acd19df197690bd0cde42cea7b88cf5aa2/mypy-1.13.0-cp39-cp39-win_amd64.whl", hash = "sha256:a6789be98a2017c912ae6ccb77ea553bbaf13d27605d2ca20a76dfbced631b24", size = 9634690, upload-time = "2024-10-22T21:54:28.814Z" },
{ url = "https://files.pythonhosted.org/packages/3b/86/72ce7f57431d87a7ff17d442f521146a6585019eb8f4f31b7c02801f78ad/mypy-1.13.0-py3-none-any.whl", hash = "sha256:9c250883f9fd81d212e0952c92dbfcc96fc237f4b7c92f56ac81fd48460b3e5a", size = 2647043, upload-time = "2024-10-22T21:55:16.617Z" },
]
@@ -1150,82 +975,28 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", size = 63772, upload-time = "2023-11-25T06:56:14.81Z" },
]
-[[package]]
-name = "pip"
-version = "25.0.1"
-source = { registry = "https://pypi.org/simple" }
-resolution-markers = [
- "python_full_version >= '3.8.1' and python_full_version < '3.9'",
- "python_full_version < '3.8.1'",
-]
-sdist = { url = "https://files.pythonhosted.org/packages/70/53/b309b4a497b09655cb7e07088966881a57d082f48ac3cb54ea729fd2c6cf/pip-25.0.1.tar.gz", hash = "sha256:88f96547ea48b940a3a385494e181e29fb8637898f88d88737c5049780f196ea", size = 1950850, upload-time = "2025-02-09T17:14:04.423Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/c9/bc/b7db44f5f39f9d0494071bddae6880eb645970366d0a200022a1a93d57f5/pip-25.0.1-py3-none-any.whl", hash = "sha256:c46efd13b6aa8279f33f2864459c8ce587ea6a1a59ee20de055868d8f7688f7f", size = 1841526, upload-time = "2025-02-09T17:14:01.463Z" },
-]
-
[[package]]
name = "pip"
version = "25.1.1"
source = { registry = "https://pypi.org/simple" }
-resolution-markers = [
- "python_full_version >= '3.11'",
- "python_full_version == '3.10.*'",
- "python_full_version == '3.9.*'",
-]
sdist = { url = "https://files.pythonhosted.org/packages/59/de/241caa0ca606f2ec5fe0c1f4261b0465df78d786a38da693864a116c37f4/pip-25.1.1.tar.gz", hash = "sha256:3de45d411d308d5054c2168185d8da7f9a2cd753dbac8acbfa88a8909ecd9077", size = 1940155, upload-time = "2025-05-02T15:14:02.057Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/29/a2/d40fb2460e883eca5199c62cfc2463fd261f760556ae6290f88488c362c0/pip-25.1.1-py3-none-any.whl", hash = "sha256:2913a38a2abf4ea6b64ab507bd9e967f3b53dc1ede74b01b0931e1ce548751af", size = 1825227, upload-time = "2025-05-02T15:13:59.102Z" },
]
-[[package]]
-name = "platformdirs"
-version = "4.3.6"
-source = { registry = "https://pypi.org/simple" }
-resolution-markers = [
- "python_full_version >= '3.8.1' and python_full_version < '3.9'",
- "python_full_version < '3.8.1'",
-]
-sdist = { url = "https://files.pythonhosted.org/packages/13/fc/128cc9cb8f03208bdbf93d3aa862e16d376844a14f9a0ce5cf4507372de4/platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907", size = 21302, upload-time = "2024-09-17T19:06:50.688Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/3c/a6/bc1012356d8ece4d66dd75c4b9fc6c1f6650ddd5991e421177d9f8f671be/platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb", size = 18439, upload-time = "2024-09-17T19:06:49.212Z" },
-]
-
[[package]]
name = "platformdirs"
version = "4.3.8"
source = { registry = "https://pypi.org/simple" }
-resolution-markers = [
- "python_full_version >= '3.11'",
- "python_full_version == '3.10.*'",
- "python_full_version == '3.9.*'",
-]
sdist = { url = "https://files.pythonhosted.org/packages/fe/8b/3c73abc9c759ecd3f1f7ceff6685840859e8070c4d947c93fae71f6a0bf2/platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc", size = 21362, upload-time = "2025-05-07T22:47:42.121Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/fe/39/979e8e21520d4e47a0bbe349e2713c0aac6f3d853d0e5b34d76206c439aa/platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4", size = 18567, upload-time = "2025-05-07T22:47:40.376Z" },
]
-[[package]]
-name = "pluggy"
-version = "1.5.0"
-source = { registry = "https://pypi.org/simple" }
-resolution-markers = [
- "python_full_version >= '3.8.1' and python_full_version < '3.9'",
- "python_full_version < '3.8.1'",
-]
-sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955, upload-time = "2024-04-20T21:34:42.531Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556, upload-time = "2024-04-20T21:34:40.434Z" },
-]
-
[[package]]
name = "pluggy"
version = "1.6.0"
source = { registry = "https://pypi.org/simple" }
-resolution-markers = [
- "python_full_version >= '3.11'",
- "python_full_version == '3.10.*'",
- "python_full_version == '3.9.*'",
-]
sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
@@ -1266,40 +1037,20 @@ wheels = [
[[package]]
name = "pycodestyle"
-version = "2.9.1"
-source = { registry = "https://pypi.org/simple" }
-resolution-markers = [
- "python_full_version < '3.8.1'",
-]
-sdist = { url = "https://files.pythonhosted.org/packages/b6/83/5bcaedba1f47200f0665ceb07bcb00e2be123192742ee0edfb66b600e5fd/pycodestyle-2.9.1.tar.gz", hash = "sha256:2c9607871d58c76354b697b42f5d57e1ada7d261c261efac224b664affdc5785", size = 102127, upload-time = "2022-08-03T23:13:29.715Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/67/e4/fc77f1039c34b3612c4867b69cbb2b8a4e569720b1f19b0637002ee03aff/pycodestyle-2.9.1-py2.py3-none-any.whl", hash = "sha256:d1735fc58b418fd7c5f658d28d943854f8a849b01a5d0a1e6f3f3fdd0166804b", size = 41493, upload-time = "2022-08-03T23:13:27.416Z" },
-]
-
-[[package]]
-name = "pycodestyle"
-version = "2.12.1"
+version = "2.14.0"
source = { registry = "https://pypi.org/simple" }
-resolution-markers = [
- "python_full_version >= '3.8.1' and python_full_version < '3.9'",
-]
-sdist = { url = "https://files.pythonhosted.org/packages/43/aa/210b2c9aedd8c1cbeea31a50e42050ad56187754b34eb214c46709445801/pycodestyle-2.12.1.tar.gz", hash = "sha256:6838eae08bbce4f6accd5d5572075c63626a15ee3e6f842df996bf62f6d73521", size = 39232, upload-time = "2024-08-04T20:26:54.576Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/11/e0/abfd2a0d2efe47670df87f3e3a0e2edda42f055053c85361f19c0e2c1ca8/pycodestyle-2.14.0.tar.gz", hash = "sha256:c4b5b517d278089ff9d0abdec919cd97262a3367449ea1c8b49b91529167b783", size = 39472, upload-time = "2025-06-20T18:49:48.75Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/3a/d8/a211b3f85e99a0daa2ddec96c949cac6824bd305b040571b82a03dd62636/pycodestyle-2.12.1-py2.py3-none-any.whl", hash = "sha256:46f0fb92069a7c28ab7bb558f05bfc0110dac69a0cd23c61ea0040283a9d78b3", size = 31284, upload-time = "2024-08-04T20:26:53.173Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/27/a58ddaf8c588a3ef080db9d0b7e0b97215cee3a45df74f3a94dbbf5c893a/pycodestyle-2.14.0-py2.py3-none-any.whl", hash = "sha256:dd6bf7cb4ee77f8e016f9c8e74a35ddd9f67e1d5fd4184d86c3b98e07099f42d", size = 31594, upload-time = "2025-06-20T18:49:47.491Z" },
]
[[package]]
-name = "pycodestyle"
-version = "2.14.0"
+name = "pycparser"
+version = "2.23"
source = { registry = "https://pypi.org/simple" }
-resolution-markers = [
- "python_full_version >= '3.11'",
- "python_full_version == '3.10.*'",
- "python_full_version == '3.9.*'",
-]
-sdist = { url = "https://files.pythonhosted.org/packages/11/e0/abfd2a0d2efe47670df87f3e3a0e2edda42f055053c85361f19c0e2c1ca8/pycodestyle-2.14.0.tar.gz", hash = "sha256:c4b5b517d278089ff9d0abdec919cd97262a3367449ea1c8b49b91529167b783", size = 39472, upload-time = "2025-06-20T18:49:48.75Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/fe/cf/d2d3b9f5699fb1e4615c8e32ff220203e43b248e1dfcc6736ad9057731ca/pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2", size = 173734, upload-time = "2025-09-09T13:23:47.91Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/d7/27/a58ddaf8c588a3ef080db9d0b7e0b97215cee3a45df74f3a94dbbf5c893a/pycodestyle-2.14.0-py2.py3-none-any.whl", hash = "sha256:dd6bf7cb4ee77f8e016f9c8e74a35ddd9f67e1d5fd4184d86c3b98e07099f42d", size = 31594, upload-time = "2025-06-20T18:49:47.491Z" },
+ { url = "https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934", size = 118140, upload-time = "2025-09-09T13:23:46.651Z" },
]
[[package]]
@@ -1309,7 +1060,7 @@ source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "annotated-types", marker = "python_full_version >= '3.11'" },
{ name = "pydantic-core", marker = "python_full_version >= '3.11'" },
- { name = "typing-extensions", version = "4.14.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
+ { name = "typing-extensions", marker = "python_full_version >= '3.11'" },
{ name = "typing-inspection", marker = "python_full_version >= '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/00/dd/4325abf92c39ba8623b5af936ddb36ffcfe0beae70405d456ab1fb2f5b8c/pydantic-2.11.7.tar.gz", hash = "sha256:d989c3c6cb79469287b1569f7447a17848c998458d49ebe294e975b9baf0f0db", size = 788350, upload-time = "2025-06-14T08:33:17.137Z" }
@@ -1322,7 +1073,7 @@ name = "pydantic-core"
version = "2.33.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
- { name = "typing-extensions", version = "4.14.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
+ { name = "typing-extensions", marker = "python_full_version >= '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", size = 435195, upload-time = "2025-04-23T18:33:52.104Z" }
wheels = [
@@ -1384,19 +1135,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/a4/7d/e09391c2eebeab681df2b74bfe6c43422fffede8dc74187b2b0bf6fd7571/pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac", size = 1806162, upload-time = "2025-04-23T18:32:20.188Z" },
{ url = "https://files.pythonhosted.org/packages/f1/3d/847b6b1fed9f8ed3bb95a9ad04fbd0b212e832d4f0f50ff4d9ee5a9f15cf/pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5", size = 1981560, upload-time = "2025-04-23T18:32:22.354Z" },
{ url = "https://files.pythonhosted.org/packages/6f/9a/e73262f6c6656262b5fdd723ad90f518f579b7bc8622e43a942eec53c938/pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9", size = 1935777, upload-time = "2025-04-23T18:32:25.088Z" },
- { url = "https://files.pythonhosted.org/packages/53/ea/bbe9095cdd771987d13c82d104a9c8559ae9aec1e29f139e286fd2e9256e/pydantic_core-2.33.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:a2b911a5b90e0374d03813674bf0a5fbbb7741570dcd4b4e85a2e48d17def29d", size = 2028677, upload-time = "2025-04-23T18:32:27.227Z" },
- { url = "https://files.pythonhosted.org/packages/49/1d/4ac5ed228078737d457a609013e8f7edc64adc37b91d619ea965758369e5/pydantic_core-2.33.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6fa6dfc3e4d1f734a34710f391ae822e0a8eb8559a85c6979e14e65ee6ba2954", size = 1864735, upload-time = "2025-04-23T18:32:29.019Z" },
- { url = "https://files.pythonhosted.org/packages/23/9a/2e70d6388d7cda488ae38f57bc2f7b03ee442fbcf0d75d848304ac7e405b/pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c54c939ee22dc8e2d545da79fc5381f1c020d6d3141d3bd747eab59164dc89fb", size = 1898467, upload-time = "2025-04-23T18:32:31.119Z" },
- { url = "https://files.pythonhosted.org/packages/ff/2e/1568934feb43370c1ffb78a77f0baaa5a8b6897513e7a91051af707ffdc4/pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:53a57d2ed685940a504248187d5685e49eb5eef0f696853647bf37c418c538f7", size = 1983041, upload-time = "2025-04-23T18:32:33.655Z" },
- { url = "https://files.pythonhosted.org/packages/01/1a/1a1118f38ab64eac2f6269eb8c120ab915be30e387bb561e3af904b12499/pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:09fb9dd6571aacd023fe6aaca316bd01cf60ab27240d7eb39ebd66a3a15293b4", size = 2136503, upload-time = "2025-04-23T18:32:35.519Z" },
- { url = "https://files.pythonhosted.org/packages/5c/da/44754d1d7ae0f22d6d3ce6c6b1486fc07ac2c524ed8f6eca636e2e1ee49b/pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0e6116757f7959a712db11f3e9c0a99ade00a5bbedae83cb801985aa154f071b", size = 2736079, upload-time = "2025-04-23T18:32:37.659Z" },
- { url = "https://files.pythonhosted.org/packages/4d/98/f43cd89172220ec5aa86654967b22d862146bc4d736b1350b4c41e7c9c03/pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d55ab81c57b8ff8548c3e4947f119551253f4e3787a7bbc0b6b3ca47498a9d3", size = 2006508, upload-time = "2025-04-23T18:32:39.637Z" },
- { url = "https://files.pythonhosted.org/packages/2b/cc/f77e8e242171d2158309f830f7d5d07e0531b756106f36bc18712dc439df/pydantic_core-2.33.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c20c462aa4434b33a2661701b861604913f912254e441ab8d78d30485736115a", size = 2113693, upload-time = "2025-04-23T18:32:41.818Z" },
- { url = "https://files.pythonhosted.org/packages/54/7a/7be6a7bd43e0a47c147ba7fbf124fe8aaf1200bc587da925509641113b2d/pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:44857c3227d3fb5e753d5fe4a3420d6376fa594b07b621e220cd93703fe21782", size = 2074224, upload-time = "2025-04-23T18:32:44.033Z" },
- { url = "https://files.pythonhosted.org/packages/2a/07/31cf8fadffbb03be1cb520850e00a8490c0927ec456e8293cafda0726184/pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:eb9b459ca4df0e5c87deb59d37377461a538852765293f9e6ee834f0435a93b9", size = 2245403, upload-time = "2025-04-23T18:32:45.836Z" },
- { url = "https://files.pythonhosted.org/packages/b6/8d/bbaf4c6721b668d44f01861f297eb01c9b35f612f6b8e14173cb204e6240/pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9fcd347d2cc5c23b06de6d3b7b8275be558a0c90549495c699e379a80bf8379e", size = 2242331, upload-time = "2025-04-23T18:32:47.618Z" },
- { url = "https://files.pythonhosted.org/packages/bb/93/3cc157026bca8f5006250e74515119fcaa6d6858aceee8f67ab6dc548c16/pydantic_core-2.33.2-cp39-cp39-win32.whl", hash = "sha256:83aa99b1285bc8f038941ddf598501a86f1536789740991d7d8756e34f1e74d9", size = 1910571, upload-time = "2025-04-23T18:32:49.401Z" },
- { url = "https://files.pythonhosted.org/packages/5b/90/7edc3b2a0d9f0dda8806c04e511a67b0b7a41d2187e2003673a996fb4310/pydantic_core-2.33.2-cp39-cp39-win_amd64.whl", hash = "sha256:f481959862f57f29601ccced557cc2e817bce7533ab8e01a797a48b49c9692b3", size = 1956504, upload-time = "2025-04-23T18:32:51.287Z" },
{ url = "https://files.pythonhosted.org/packages/30/68/373d55e58b7e83ce371691f6eaa7175e3a24b956c44628eb25d7da007917/pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5c4aa4e82353f65e548c476b37e64189783aa5384903bfea4f41580f255fddfa", size = 2023982, upload-time = "2025-04-23T18:32:53.14Z" },
{ url = "https://files.pythonhosted.org/packages/a4/16/145f54ac08c96a63d8ed6442f9dec17b2773d19920b627b18d4f10a061ea/pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d946c8bf0d5c24bf4fe333af284c59a19358aa3ec18cb3dc4370080da1e8ad29", size = 1858412, upload-time = "2025-04-23T18:32:55.52Z" },
{ url = "https://files.pythonhosted.org/packages/41/b1/c6dc6c3e2de4516c0bb2c46f6a373b91b5660312342a0cf5826e38ad82fa/pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87b31b6846e361ef83fedb187bb5b4372d0da3f7e28d85415efa92d6125d6e6d", size = 1892749, upload-time = "2025-04-23T18:32:57.546Z" },
@@ -1415,53 +1153,31 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/b8/e9/1f7efbe20d0b2b10f6718944b5d8ece9152390904f29a78e68d4e7961159/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:de4b83bb311557e439b9e186f733f6c645b9417c84e2eb8203f3f820a4b988bf", size = 2239013, upload-time = "2025-04-23T18:33:26.621Z" },
{ url = "https://files.pythonhosted.org/packages/3c/b2/5309c905a93811524a49b4e031e9851a6b00ff0fb668794472ea7746b448/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:82f68293f055f51b51ea42fafc74b6aad03e70e191799430b90c13d643059ebb", size = 2238715, upload-time = "2025-04-23T18:33:28.656Z" },
{ url = "https://files.pythonhosted.org/packages/32/56/8a7ca5d2cd2cda1d245d34b1c9a942920a718082ae8e54e5f3e5a58b7add/pydantic_core-2.33.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:329467cecfb529c925cf2bbd4d60d2c509bc2fb52a20c1045bf09bb70971a9c1", size = 2066757, upload-time = "2025-04-23T18:33:30.645Z" },
- { url = "https://files.pythonhosted.org/packages/08/98/dbf3fdfabaf81cda5622154fda78ea9965ac467e3239078e0dcd6df159e7/pydantic_core-2.33.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:87acbfcf8e90ca885206e98359d7dca4bcbb35abdc0ff66672a293e1d7a19101", size = 2024034, upload-time = "2025-04-23T18:33:32.843Z" },
- { url = "https://files.pythonhosted.org/packages/8d/99/7810aa9256e7f2ccd492590f86b79d370df1e9292f1f80b000b6a75bd2fb/pydantic_core-2.33.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:7f92c15cd1e97d4b12acd1cc9004fa092578acfa57b67ad5e43a197175d01a64", size = 1858578, upload-time = "2025-04-23T18:33:34.912Z" },
- { url = "https://files.pythonhosted.org/packages/d8/60/bc06fa9027c7006cc6dd21e48dbf39076dc39d9abbaf718a1604973a9670/pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3f26877a748dc4251cfcfda9dfb5f13fcb034f5308388066bcfe9031b63ae7d", size = 1892858, upload-time = "2025-04-23T18:33:36.933Z" },
- { url = "https://files.pythonhosted.org/packages/f2/40/9d03997d9518816c68b4dfccb88969756b9146031b61cd37f781c74c9b6a/pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dac89aea9af8cd672fa7b510e7b8c33b0bba9a43186680550ccf23020f32d535", size = 2068498, upload-time = "2025-04-23T18:33:38.997Z" },
- { url = "https://files.pythonhosted.org/packages/d8/62/d490198d05d2d86672dc269f52579cad7261ced64c2df213d5c16e0aecb1/pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:970919794d126ba8645f3837ab6046fb4e72bbc057b3709144066204c19a455d", size = 2108428, upload-time = "2025-04-23T18:33:41.18Z" },
- { url = "https://files.pythonhosted.org/packages/9a/ec/4cd215534fd10b8549015f12ea650a1a973da20ce46430b68fc3185573e8/pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:3eb3fe62804e8f859c49ed20a8451342de53ed764150cb14ca71357c765dc2a6", size = 2069854, upload-time = "2025-04-23T18:33:43.446Z" },
- { url = "https://files.pythonhosted.org/packages/1a/1a/abbd63d47e1d9b0d632fee6bb15785d0889c8a6e0a6c3b5a8e28ac1ec5d2/pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:3abcd9392a36025e3bd55f9bd38d908bd17962cc49bc6da8e7e96285336e2bca", size = 2237859, upload-time = "2025-04-23T18:33:45.56Z" },
- { url = "https://files.pythonhosted.org/packages/80/1c/fa883643429908b1c90598fd2642af8839efd1d835b65af1f75fba4d94fe/pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:3a1c81334778f9e3af2f8aeb7a960736e5cab1dfebfb26aabca09afd2906c039", size = 2239059, upload-time = "2025-04-23T18:33:47.735Z" },
- { url = "https://files.pythonhosted.org/packages/d4/29/3cade8a924a61f60ccfa10842f75eb12787e1440e2b8660ceffeb26685e7/pydantic_core-2.33.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2807668ba86cb38c6817ad9bc66215ab8584d1d304030ce4f0887336f28a5e27", size = 2066661, upload-time = "2025-04-23T18:33:49.995Z" },
]
[[package]]
name = "pyflakes"
-version = "2.5.0"
-source = { registry = "https://pypi.org/simple" }
-resolution-markers = [
- "python_full_version < '3.8.1'",
-]
-sdist = { url = "https://files.pythonhosted.org/packages/07/92/f0cb5381f752e89a598dd2850941e7f570ac3cb8ea4a344854de486db152/pyflakes-2.5.0.tar.gz", hash = "sha256:491feb020dca48ccc562a8c0cbe8df07ee13078df59813b83959cbdada312ea3", size = 66388, upload-time = "2022-07-30T17:29:05.816Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/dc/13/63178f59f74e53acc2165aee4b002619a3cfa7eeaeac989a9eb41edf364e/pyflakes-2.5.0-py2.py3-none-any.whl", hash = "sha256:4579f67d887f804e67edb544428f264b7b24f435b263c4614f384135cea553d2", size = 66116, upload-time = "2022-07-30T17:29:04.179Z" },
-]
-
-[[package]]
-name = "pyflakes"
-version = "3.2.0"
+version = "3.4.0"
source = { registry = "https://pypi.org/simple" }
-resolution-markers = [
- "python_full_version >= '3.8.1' and python_full_version < '3.9'",
-]
-sdist = { url = "https://files.pythonhosted.org/packages/57/f9/669d8c9c86613c9d568757c7f5824bd3197d7b1c6c27553bc5618a27cce2/pyflakes-3.2.0.tar.gz", hash = "sha256:1c61603ff154621fb2a9172037d84dca3500def8c8b630657d1701f026f8af3f", size = 63788, upload-time = "2024-01-05T00:28:47.703Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/45/dc/fd034dc20b4b264b3d015808458391acbf9df40b1e54750ef175d39180b1/pyflakes-3.4.0.tar.gz", hash = "sha256:b24f96fafb7d2ab0ec5075b7350b3d2d2218eab42003821c06344973d3ea2f58", size = 64669, upload-time = "2025-06-20T18:45:27.834Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/d4/d7/f1b7db88d8e4417c5d47adad627a93547f44bdc9028372dbd2313f34a855/pyflakes-3.2.0-py2.py3-none-any.whl", hash = "sha256:84b5be138a2dfbb40689ca07e2152deb896a65c3a3e24c251c5c62489568074a", size = 62725, upload-time = "2024-01-05T00:28:45.903Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/2f/81d580a0fb83baeb066698975cb14a618bdbed7720678566f1b046a95fe8/pyflakes-3.4.0-py2.py3-none-any.whl", hash = "sha256:f742a7dbd0d9cb9ea41e9a24a918996e8170c799fa528688d40dd582c8265f4f", size = 63551, upload-time = "2025-06-20T18:45:26.937Z" },
]
[[package]]
-name = "pyflakes"
-version = "3.4.0"
+name = "pygithub"
+version = "2.8.1"
source = { registry = "https://pypi.org/simple" }
-resolution-markers = [
- "python_full_version >= '3.11'",
- "python_full_version == '3.10.*'",
- "python_full_version == '3.9.*'",
+dependencies = [
+ { name = "pyjwt", extra = ["crypto"] },
+ { name = "pynacl" },
+ { name = "requests" },
+ { name = "typing-extensions" },
+ { name = "urllib3" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/45/dc/fd034dc20b4b264b3d015808458391acbf9df40b1e54750ef175d39180b1/pyflakes-3.4.0.tar.gz", hash = "sha256:b24f96fafb7d2ab0ec5075b7350b3d2d2218eab42003821c06344973d3ea2f58", size = 64669, upload-time = "2025-06-20T18:45:27.834Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/c1/74/e560bdeffea72ecb26cff27f0fad548bbff5ecc51d6a155311ea7f9e4c4c/pygithub-2.8.1.tar.gz", hash = "sha256:341b7c78521cb07324ff670afd1baa2bf5c286f8d9fd302c1798ba594a5400c9", size = 2246994, upload-time = "2025-09-02T17:41:54.674Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/c2/2f/81d580a0fb83baeb066698975cb14a618bdbed7720678566f1b046a95fe8/pyflakes-3.4.0-py2.py3-none-any.whl", hash = "sha256:f742a7dbd0d9cb9ea41e9a24a918996e8170c799fa528688d40dd582c8265f4f", size = 63551, upload-time = "2025-06-20T18:45:26.937Z" },
+ { url = "https://files.pythonhosted.org/packages/07/ba/7049ce39f653f6140aac4beb53a5aaf08b4407b6a3019aae394c1c5244ff/pygithub-2.8.1-py3-none-any.whl", hash = "sha256:23a0a5bca93baef082e03411bf0ce27204c32be8bfa7abc92fe4a3e132936df0", size = 432709, upload-time = "2025-09-02T17:41:52.947Z" },
]
[[package]]
@@ -1474,40 +1190,63 @@ wheels = [
]
[[package]]
-name = "pymdown-extensions"
-version = "10.15"
+name = "pyjwt"
+version = "2.10.1"
source = { registry = "https://pypi.org/simple" }
-resolution-markers = [
- "python_full_version >= '3.8.1' and python_full_version < '3.9'",
- "python_full_version < '3.8.1'",
-]
-dependencies = [
- { name = "markdown", version = "3.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" },
- { name = "pyyaml", marker = "python_full_version < '3.9'" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/08/92/a7296491dbf5585b3a987f3f3fc87af0e632121ff3e490c14b5f2d2b4eb5/pymdown_extensions-10.15.tar.gz", hash = "sha256:0e5994e32155f4b03504f939e501b981d306daf7ec2aa1cd2eb6bd300784f8f7", size = 852320, upload-time = "2025-04-27T23:48:29.183Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/e7/46/bd74733ff231675599650d3e47f361794b22ef3e3770998dda30d3b63726/pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953", size = 87785, upload-time = "2024-11-28T03:43:29.933Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/a7/d1/c54e608505776ce4e7966d03358ae635cfd51dff1da6ee421c090dbc797b/pymdown_extensions-10.15-py3-none-any.whl", hash = "sha256:46e99bb272612b0de3b7e7caf6da8dd5f4ca5212c0b273feb9304e236c484e5f", size = 265845, upload-time = "2025-04-27T23:48:27.359Z" },
+ { url = "https://files.pythonhosted.org/packages/61/ad/689f02752eeec26aed679477e80e632ef1b682313be70793d798c1d5fc8f/PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb", size = 22997, upload-time = "2024-11-28T03:43:27.893Z" },
+]
+
+[package.optional-dependencies]
+crypto = [
+ { name = "cryptography" },
]
[[package]]
name = "pymdown-extensions"
version = "10.16"
source = { registry = "https://pypi.org/simple" }
-resolution-markers = [
- "python_full_version >= '3.11'",
- "python_full_version == '3.10.*'",
- "python_full_version == '3.9.*'",
-]
dependencies = [
- { name = "markdown", version = "3.8.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" },
- { name = "pyyaml", marker = "python_full_version >= '3.9'" },
+ { name = "markdown" },
+ { name = "pyyaml" },
]
sdist = { url = "https://files.pythonhosted.org/packages/1a/0a/c06b542ac108bfc73200677309cd9188a3a01b127a63f20cadc18d873d88/pymdown_extensions-10.16.tar.gz", hash = "sha256:71dac4fca63fabeffd3eb9038b756161a33ec6e8d230853d3cecf562155ab3de", size = 853197, upload-time = "2025-06-21T17:56:36.974Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/98/d4/10bb14004d3c792811e05e21b5e5dcae805aacb739bd12a0540967b99592/pymdown_extensions-10.16-py3-none-any.whl", hash = "sha256:f5dd064a4db588cb2d95229fc4ee63a1b16cc8b4d0e6145c0899ed8723da1df2", size = 266143, upload-time = "2025-06-21T17:56:35.356Z" },
]
+[[package]]
+name = "pynacl"
+version = "1.6.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/06/c6/a3124dee667a423f2c637cfd262a54d67d8ccf3e160f3c50f622a85b7723/pynacl-1.6.0.tar.gz", hash = "sha256:cb36deafe6e2bce3b286e5d1f3e1c246e0ccdb8808ddb4550bb2792f2df298f2", size = 3505641, upload-time = "2025-09-10T23:39:22.308Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/70/24/1b639176401255605ba7c2b93a7b1eb1e379e0710eca62613633eb204201/pynacl-1.6.0-cp314-cp314t-macosx_10_10_universal2.whl", hash = "sha256:f46386c24a65383a9081d68e9c2de909b1834ec74ff3013271f1bca9c2d233eb", size = 384141, upload-time = "2025-09-10T23:38:28.675Z" },
+ { url = "https://files.pythonhosted.org/packages/5e/7b/874efdf57d6bf172db0df111b479a553c3d9e8bb4f1f69eb3ffff772d6e8/pynacl-1.6.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:dea103a1afcbc333bc0e992e64233d360d393d1e63d0bc88554f572365664348", size = 808132, upload-time = "2025-09-10T23:38:38.995Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/61/9b53f5913f3b75ac3d53170cdb897101b2b98afc76f4d9d3c8de5aa3ac05/pynacl-1.6.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:04f20784083014e265ad58c1b2dd562c3e35864b5394a14ab54f5d150ee9e53e", size = 1407253, upload-time = "2025-09-10T23:38:40.492Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/0a/b138916b22bbf03a1bdbafecec37d714e7489dd7bcaf80cd17852f8b67be/pynacl-1.6.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bbcc4452a1eb10cd5217318c822fde4be279c9de8567f78bad24c773c21254f8", size = 843719, upload-time = "2025-09-10T23:38:30.87Z" },
+ { url = "https://files.pythonhosted.org/packages/01/3b/17c368197dfb2c817ce033f94605a47d0cc27901542109e640cef263f0af/pynacl-1.6.0-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:51fed9fe1bec9e7ff9af31cd0abba179d0e984a2960c77e8e5292c7e9b7f7b5d", size = 1445441, upload-time = "2025-09-10T23:38:33.078Z" },
+ { url = "https://files.pythonhosted.org/packages/35/3c/f79b185365ab9be80cd3cd01dacf30bf5895f9b7b001e683b369e0bb6d3d/pynacl-1.6.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:10d755cf2a455d8c0f8c767a43d68f24d163b8fe93ccfaabfa7bafd26be58d73", size = 825691, upload-time = "2025-09-10T23:38:34.832Z" },
+ { url = "https://files.pythonhosted.org/packages/f7/1f/8b37d25e95b8f2a434a19499a601d4d272b9839ab8c32f6b0fc1e40c383f/pynacl-1.6.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:536703b8f90e911294831a7fbcd0c062b837f3ccaa923d92a6254e11178aaf42", size = 1410726, upload-time = "2025-09-10T23:38:36.893Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/93/5a4a4cf9913014f83d615ad6a2df9187330f764f606246b3a744c0788c03/pynacl-1.6.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6b08eab48c9669d515a344fb0ef27e2cbde847721e34bba94a343baa0f33f1f4", size = 801035, upload-time = "2025-09-10T23:38:42.109Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/60/40da6b0fe6a4d5fd88f608389eb1df06492ba2edca93fca0b3bebff9b948/pynacl-1.6.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5789f016e08e5606803161ba24de01b5a345d24590a80323379fc4408832d290", size = 1371854, upload-time = "2025-09-10T23:38:44.16Z" },
+ { url = "https://files.pythonhosted.org/packages/63/37/87c72df19857c5b3b47ace6f211a26eb862ada495cc96daa372d96048fca/pynacl-1.6.0-cp38-abi3-macosx_10_10_universal2.whl", hash = "sha256:f4b3824920e206b4f52abd7de621ea7a44fd3cb5c8daceb7c3612345dfc54f2e", size = 382610, upload-time = "2025-09-10T23:38:49.459Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/64/3ce958a5817fd3cc6df4ec14441c43fd9854405668d73babccf77f9597a3/pynacl-1.6.0-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:16dd347cdc8ae0b0f6187a2608c0af1c8b7ecbbe6b4a06bff8253c192f696990", size = 798744, upload-time = "2025-09-10T23:38:58.531Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/8a/3f0dd297a0a33fa3739c255feebd0206bb1df0b44c52fbe2caf8e8bc4425/pynacl-1.6.0-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:16c60daceee88d04f8d41d0a4004a7ed8d9a5126b997efd2933e08e93a3bd850", size = 1397879, upload-time = "2025-09-10T23:39:00.44Z" },
+ { url = "https://files.pythonhosted.org/packages/41/94/028ff0434a69448f61348d50d2c147dda51aabdd4fbc93ec61343332174d/pynacl-1.6.0-cp38-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:25720bad35dfac34a2bcdd61d9e08d6bfc6041bebc7751d9c9f2446cf1e77d64", size = 833907, upload-time = "2025-09-10T23:38:50.936Z" },
+ { url = "https://files.pythonhosted.org/packages/52/bc/a5cff7f8c30d5f4c26a07dfb0bcda1176ab8b2de86dda3106c00a02ad787/pynacl-1.6.0-cp38-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8bfaa0a28a1ab718bad6239979a5a57a8d1506d0caf2fba17e524dbb409441cf", size = 1436649, upload-time = "2025-09-10T23:38:52.783Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/20/c397be374fd5d84295046e398de4ba5f0722dc14450f65db76a43c121471/pynacl-1.6.0-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:ef214b90556bb46a485b7da8258e59204c244b1b5b576fb71848819b468c44a7", size = 817142, upload-time = "2025-09-10T23:38:54.4Z" },
+ { url = "https://files.pythonhosted.org/packages/12/30/5efcef3406940cda75296c6d884090b8a9aad2dcc0c304daebb5ae99fb4a/pynacl-1.6.0-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:49c336dd80ea54780bcff6a03ee1a476be1612423010472e60af83452aa0f442", size = 1401794, upload-time = "2025-09-10T23:38:56.614Z" },
+ { url = "https://files.pythonhosted.org/packages/be/e1/a8fe1248cc17ccb03b676d80fa90763760a6d1247da434844ea388d0816c/pynacl-1.6.0-cp38-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:f3482abf0f9815e7246d461fab597aa179b7524628a4bc36f86a7dc418d2608d", size = 772161, upload-time = "2025-09-10T23:39:01.93Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/76/8a62702fb657d6d9104ce13449db221a345665d05e6a3fdefb5a7cafd2ad/pynacl-1.6.0-cp38-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:140373378e34a1f6977e573033d1dd1de88d2a5d90ec6958c9485b2fd9f3eb90", size = 1370720, upload-time = "2025-09-10T23:39:03.531Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/38/9e9e9b777a1c4c8204053733e1a0269672c0bd40852908c9ad6b6eaba82c/pynacl-1.6.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:6b393bc5e5a0eb86bb85b533deb2d2c815666665f840a09e0aa3362bb6088736", size = 791252, upload-time = "2025-09-10T23:39:05.058Z" },
+ { url = "https://files.pythonhosted.org/packages/63/ef/d972ce3d92ae05c9091363cf185e8646933f91c376e97b8be79ea6e96c22/pynacl-1.6.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:4a25cfede801f01e54179b8ff9514bd7b5944da560b7040939732d1804d25419", size = 1362910, upload-time = "2025-09-10T23:39:06.924Z" },
+]
+
[[package]]
name = "pyproject-hooks"
version = "1.2.0"
@@ -1519,46 +1258,34 @@ wheels = [
[[package]]
name = "pytest"
-version = "8.3.5"
+version = "8.4.1"
source = { registry = "https://pypi.org/simple" }
-resolution-markers = [
- "python_full_version >= '3.8.1' and python_full_version < '3.9'",
- "python_full_version < '3.8.1'",
-]
dependencies = [
- { name = "colorama", marker = "python_full_version < '3.9' and sys_platform == 'win32'" },
- { name = "exceptiongroup", marker = "python_full_version < '3.9'" },
- { name = "iniconfig", marker = "python_full_version < '3.9'" },
- { name = "packaging", marker = "python_full_version < '3.9'" },
- { name = "pluggy", version = "1.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" },
- { name = "tomli", marker = "python_full_version < '3.9'" },
+ { name = "colorama", marker = "sys_platform == 'win32'" },
+ { name = "exceptiongroup", marker = "python_full_version < '3.11'" },
+ { name = "iniconfig" },
+ { name = "packaging" },
+ { name = "pluggy" },
+ { name = "pygments" },
+ { name = "tomli", marker = "python_full_version < '3.11'" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891, upload-time = "2025-03-02T12:54:54.503Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/08/ba/45911d754e8eba3d5a841a5ce61a65a685ff1798421ac054f85aa8747dfb/pytest-8.4.1.tar.gz", hash = "sha256:7c67fd69174877359ed9371ec3af8a3d2b04741818c51e5e99cc1742251fa93c", size = 1517714, upload-time = "2025-06-18T05:48:06.109Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634, upload-time = "2025-03-02T12:54:52.069Z" },
+ { url = "https://files.pythonhosted.org/packages/29/16/c8a903f4c4dffe7a12843191437d7cd8e32751d5de349d45d3fe69544e87/pytest-8.4.1-py3-none-any.whl", hash = "sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7", size = 365474, upload-time = "2025-06-18T05:48:03.955Z" },
]
[[package]]
-name = "pytest"
-version = "8.4.1"
+name = "pytest-cov"
+version = "7.0.0"
source = { registry = "https://pypi.org/simple" }
-resolution-markers = [
- "python_full_version >= '3.11'",
- "python_full_version == '3.10.*'",
- "python_full_version == '3.9.*'",
-]
dependencies = [
- { name = "colorama", marker = "python_full_version >= '3.9' and sys_platform == 'win32'" },
- { name = "exceptiongroup", marker = "python_full_version >= '3.9' and python_full_version < '3.11'" },
- { name = "iniconfig", marker = "python_full_version >= '3.9'" },
- { name = "packaging", marker = "python_full_version >= '3.9'" },
- { name = "pluggy", version = "1.6.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" },
- { name = "pygments", marker = "python_full_version >= '3.9'" },
- { name = "tomli", marker = "python_full_version >= '3.9' and python_full_version < '3.11'" },
+ { name = "coverage", extra = ["toml"] },
+ { name = "pluggy" },
+ { name = "pytest" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/08/ba/45911d754e8eba3d5a841a5ce61a65a685ff1798421ac054f85aa8747dfb/pytest-8.4.1.tar.gz", hash = "sha256:7c67fd69174877359ed9371ec3af8a3d2b04741818c51e5e99cc1742251fa93c", size = 1517714, upload-time = "2025-06-18T05:48:06.109Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/5e/f7/c933acc76f5208b3b00089573cf6a2bc26dc80a8aece8f52bb7d6b1855ca/pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1", size = 54328, upload-time = "2025-09-09T10:57:02.113Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/29/16/c8a903f4c4dffe7a12843191437d7cd8e32751d5de349d45d3fe69544e87/pytest-8.4.1-py3-none-any.whl", hash = "sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7", size = 365474, upload-time = "2025-06-18T05:48:03.955Z" },
+ { url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" },
]
[[package]]
@@ -1566,43 +1293,20 @@ name = "pytest-timeout"
version = "2.4.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
- { name = "pytest", version = "8.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" },
- { name = "pytest", version = "8.4.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" },
+ { name = "pytest" },
]
sdist = { url = "https://files.pythonhosted.org/packages/ac/82/4c9ecabab13363e72d880f2fb504c5f750433b2b6f16e99f4ec21ada284c/pytest_timeout-2.4.0.tar.gz", hash = "sha256:7e68e90b01f9eff71332b25001f85c75495fc4e3a836701876183c4bcfd0540a", size = 17973, upload-time = "2025-05-05T19:44:34.99Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/fa/b6/3127540ecdf1464a00e5a01ee60a1b09175f6913f0644ac748494d9c4b21/pytest_timeout-2.4.0-py3-none-any.whl", hash = "sha256:c42667e5cdadb151aeb5b26d114aff6bdf5a907f176a007a30b940d3d865b5c2", size = 14382, upload-time = "2025-05-05T19:44:33.502Z" },
]
-[[package]]
-name = "pytest-xdist"
-version = "3.6.1"
-source = { registry = "https://pypi.org/simple" }
-resolution-markers = [
- "python_full_version >= '3.8.1' and python_full_version < '3.9'",
- "python_full_version < '3.8.1'",
-]
-dependencies = [
- { name = "execnet", marker = "python_full_version < '3.9'" },
- { name = "pytest", version = "8.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/41/c4/3c310a19bc1f1e9ef50075582652673ef2bfc8cd62afef9585683821902f/pytest_xdist-3.6.1.tar.gz", hash = "sha256:ead156a4db231eec769737f57668ef58a2084a34b2e55c4a8fa20d861107300d", size = 84060, upload-time = "2024-04-28T19:29:54.414Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/6d/82/1d96bf03ee4c0fdc3c0cbe61470070e659ca78dc0086fb88b66c185e2449/pytest_xdist-3.6.1-py3-none-any.whl", hash = "sha256:9ed4adfb68a016610848639bb7e02c9352d5d9f03d04809919e2dafc3be4cca7", size = 46108, upload-time = "2024-04-28T19:29:52.813Z" },
-]
-
[[package]]
name = "pytest-xdist"
version = "3.8.0"
source = { registry = "https://pypi.org/simple" }
-resolution-markers = [
- "python_full_version >= '3.11'",
- "python_full_version == '3.10.*'",
- "python_full_version == '3.9.*'",
-]
dependencies = [
- { name = "execnet", marker = "python_full_version >= '3.9'" },
- { name = "pytest", version = "8.4.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" },
+ { name = "execnet" },
+ { name = "pytest" },
]
sdist = { url = "https://files.pythonhosted.org/packages/78/b4/439b179d1ff526791eb921115fca8e44e596a13efeda518b9d845a619450/pytest_xdist-3.8.0.tar.gz", hash = "sha256:7e578125ec9bc6050861aa93f2d59f1d8d085595d6551c2c90b6f4fad8d3a9f1", size = 88069, upload-time = "2025-07-01T13:30:59.346Z" }
wheels = [
@@ -1621,15 +1325,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" },
]
-[[package]]
-name = "pytz"
-version = "2025.2"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884, upload-time = "2025-03-25T02:25:00.538Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" },
-]
-
[[package]]
name = "pywin32"
version = "311"
@@ -1650,11 +1345,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/c9/31/097f2e132c4f16d99a22bfb777e0fd88bd8e1c634304e102f313af69ace5/pywin32-311-cp314-cp314-win32.whl", hash = "sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee", size = 8840714, upload-time = "2025-07-14T20:13:32.449Z" },
{ url = "https://files.pythonhosted.org/packages/90/4b/07c77d8ba0e01349358082713400435347df8426208171ce297da32c313d/pywin32-311-cp314-cp314-win_amd64.whl", hash = "sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87", size = 9656800, upload-time = "2025-07-14T20:13:34.312Z" },
{ url = "https://files.pythonhosted.org/packages/c0/d2/21af5c535501a7233e734b8af901574572da66fcc254cb35d0609c9080dd/pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42", size = 8932540, upload-time = "2025-07-14T20:13:36.379Z" },
- { url = "https://files.pythonhosted.org/packages/75/20/6cd04d636a4c83458ecbb7c8220c13786a1a80d3f5fb568df39310e73e98/pywin32-311-cp38-cp38-win32.whl", hash = "sha256:6c6f2969607b5023b0d9ce2541f8d2cbb01c4f46bc87456017cf63b73f1e2d8c", size = 8766775, upload-time = "2025-07-14T20:12:55.029Z" },
- { url = "https://files.pythonhosted.org/packages/ff/6c/94c10268bae5d0d0c6509bdfb5aa08882d11a9ccdf89ff1cde59a6161afb/pywin32-311-cp38-cp38-win_amd64.whl", hash = "sha256:c8015b09fb9a5e188f83b7b04de91ddca4658cee2ae6f3bc483f0b21a77ef6cd", size = 9594743, upload-time = "2025-07-14T20:12:57.59Z" },
- { url = "https://files.pythonhosted.org/packages/59/42/b86689aac0cdaee7ae1c58d464b0ff04ca909c19bb6502d4973cdd9f9544/pywin32-311-cp39-cp39-win32.whl", hash = "sha256:aba8f82d551a942cb20d4a83413ccbac30790b50efb89a75e4f586ac0bb8056b", size = 8760837, upload-time = "2025-07-14T20:12:59.59Z" },
- { url = "https://files.pythonhosted.org/packages/9f/8a/1403d0353f8c5a2f0829d2b1c4becbf9da2f0a4d040886404fc4a5431e4d/pywin32-311-cp39-cp39-win_amd64.whl", hash = "sha256:e0c4cfb0621281fe40387df582097fd796e80430597cb9944f0ae70447bacd91", size = 9590187, upload-time = "2025-07-14T20:13:01.419Z" },
- { url = "https://files.pythonhosted.org/packages/60/22/e0e8d802f124772cec9c75430b01a212f86f9de7546bda715e54140d5aeb/pywin32-311-cp39-cp39-win_arm64.whl", hash = "sha256:62ea666235135fee79bb154e695f3ff67370afefd71bd7fea7512fc70ef31e3d", size = 8778162, upload-time = "2025-07-14T20:13:03.544Z" },
]
[[package]]
@@ -1699,51 +1389,14 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597, upload-time = "2024-08-06T20:32:56.985Z" },
{ url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527, upload-time = "2024-08-06T20:33:03.001Z" },
{ url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446, upload-time = "2024-08-06T20:33:04.33Z" },
- { url = "https://files.pythonhosted.org/packages/74/d9/323a59d506f12f498c2097488d80d16f4cf965cee1791eab58b56b19f47a/PyYAML-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a", size = 183218, upload-time = "2024-08-06T20:33:06.411Z" },
- { url = "https://files.pythonhosted.org/packages/74/cc/20c34d00f04d785f2028737e2e2a8254e1425102e730fee1d6396f832577/PyYAML-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5", size = 728067, upload-time = "2024-08-06T20:33:07.879Z" },
- { url = "https://files.pythonhosted.org/packages/20/52/551c69ca1501d21c0de51ddafa8c23a0191ef296ff098e98358f69080577/PyYAML-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d", size = 757812, upload-time = "2024-08-06T20:33:12.542Z" },
- { url = "https://files.pythonhosted.org/packages/fd/7f/2c3697bba5d4aa5cc2afe81826d73dfae5f049458e44732c7a0938baa673/PyYAML-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083", size = 746531, upload-time = "2024-08-06T20:33:14.391Z" },
- { url = "https://files.pythonhosted.org/packages/8c/ab/6226d3df99900e580091bb44258fde77a8433511a86883bd4681ea19a858/PyYAML-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706", size = 800820, upload-time = "2024-08-06T20:33:16.586Z" },
- { url = "https://files.pythonhosted.org/packages/a0/99/a9eb0f3e710c06c5d922026f6736e920d431812ace24aae38228d0d64b04/PyYAML-6.0.2-cp38-cp38-win32.whl", hash = "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a", size = 145514, upload-time = "2024-08-06T20:33:22.414Z" },
- { url = "https://files.pythonhosted.org/packages/75/8a/ee831ad5fafa4431099aa4e078d4c8efd43cd5e48fbc774641d233b683a9/PyYAML-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff", size = 162702, upload-time = "2024-08-06T20:33:23.813Z" },
- { url = "https://files.pythonhosted.org/packages/65/d8/b7a1db13636d7fb7d4ff431593c510c8b8fca920ade06ca8ef20015493c5/PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d", size = 184777, upload-time = "2024-08-06T20:33:25.896Z" },
- { url = "https://files.pythonhosted.org/packages/0a/02/6ec546cd45143fdf9840b2c6be8d875116a64076218b61d68e12548e5839/PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f", size = 172318, upload-time = "2024-08-06T20:33:27.212Z" },
- { url = "https://files.pythonhosted.org/packages/0e/9a/8cc68be846c972bda34f6c2a93abb644fb2476f4dcc924d52175786932c9/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290", size = 720891, upload-time = "2024-08-06T20:33:28.974Z" },
- { url = "https://files.pythonhosted.org/packages/e9/6c/6e1b7f40181bc4805e2e07f4abc10a88ce4648e7e95ff1abe4ae4014a9b2/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12", size = 722614, upload-time = "2024-08-06T20:33:34.157Z" },
- { url = "https://files.pythonhosted.org/packages/3d/32/e7bd8535d22ea2874cef6a81021ba019474ace0d13a4819c2a4bce79bd6a/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19", size = 737360, upload-time = "2024-08-06T20:33:35.84Z" },
- { url = "https://files.pythonhosted.org/packages/d7/12/7322c1e30b9be969670b672573d45479edef72c9a0deac3bb2868f5d7469/PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e", size = 699006, upload-time = "2024-08-06T20:33:37.501Z" },
- { url = "https://files.pythonhosted.org/packages/82/72/04fcad41ca56491995076630c3ec1e834be241664c0c09a64c9a2589b507/PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725", size = 723577, upload-time = "2024-08-06T20:33:39.389Z" },
- { url = "https://files.pythonhosted.org/packages/ed/5e/46168b1f2757f1fcd442bc3029cd8767d88a98c9c05770d8b420948743bb/PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631", size = 144593, upload-time = "2024-08-06T20:33:46.63Z" },
- { url = "https://files.pythonhosted.org/packages/19/87/5124b1c1f2412bb95c59ec481eaf936cd32f0fe2a7b16b97b81c4c017a6a/PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8", size = 162312, upload-time = "2024-08-06T20:33:49.073Z" },
-]
-
-[[package]]
-name = "pyyaml-env-tag"
-version = "0.1"
-source = { registry = "https://pypi.org/simple" }
-resolution-markers = [
- "python_full_version >= '3.8.1' and python_full_version < '3.9'",
- "python_full_version < '3.8.1'",
-]
-dependencies = [
- { name = "pyyaml", marker = "python_full_version < '3.9'" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/fb/8e/da1c6c58f751b70f8ceb1eb25bc25d524e8f14fe16edcce3f4e3ba08629c/pyyaml_env_tag-0.1.tar.gz", hash = "sha256:70092675bda14fdec33b31ba77e7543de9ddc88f2e5b99160396572d11525bdb", size = 5631, upload-time = "2020-11-12T02:38:26.239Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/5a/66/bbb1dd374f5c870f59c5bb1db0e18cbe7fa739415a24cbd95b2d1f5ae0c4/pyyaml_env_tag-0.1-py3-none-any.whl", hash = "sha256:af31106dec8a4d68c60207c1886031cbf839b68aa7abccdb19868200532c2069", size = 3911, upload-time = "2020-11-12T02:38:24.638Z" },
]
[[package]]
name = "pyyaml-env-tag"
version = "1.1"
source = { registry = "https://pypi.org/simple" }
-resolution-markers = [
- "python_full_version >= '3.11'",
- "python_full_version == '3.10.*'",
- "python_full_version == '3.9.*'",
-]
dependencies = [
- { name = "pyyaml", marker = "python_full_version >= '3.9'" },
+ { name = "pyyaml" },
]
sdist = { url = "https://files.pythonhosted.org/packages/eb/2e/79c822141bfd05a853236b504869ebc6b70159afc570e1d5a20641782eaa/pyyaml_env_tag-1.1.tar.gz", hash = "sha256:2eb38b75a2d21ee0475d6d97ec19c63287a7e140231e4214969d0eac923cd7ff", size = 5737, upload-time = "2025-05-13T15:24:01.64Z" }
wheels = [
@@ -1770,8 +1423,7 @@ dependencies = [
{ name = "certifi" },
{ name = "charset-normalizer" },
{ name = "idna" },
- { name = "urllib3", version = "2.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" },
- { name = "urllib3", version = "2.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" },
+ { name = "urllib3" },
]
sdist = { url = "https://files.pythonhosted.org/packages/e1/0a/929373653770d8a0d7ea76c37de6e41f11eb07559b103b1c02cafb3f7cf8/requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422", size = 135258, upload-time = "2025-06-09T16:43:07.34Z" }
wheels = [
@@ -1785,8 +1437,7 @@ source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "markdown-it-py" },
{ name = "pygments" },
- { name = "typing-extensions", version = "4.13.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" },
- { name = "typing-extensions", version = "4.14.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9' and python_full_version < '3.11'" },
+ { name = "typing-extensions", marker = "python_full_version < '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/ab/3a/0316b28d0761c6734d6bc14e770d85506c986c85ffb239e688eeaab2c2bc/rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098", size = 223149, upload-time = "2024-11-01T16:43:57.873Z" }
wheels = [
@@ -1830,28 +1481,10 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/cb/5c/799a1efb8b5abab56e8a9f2a0b72d12bd64bb55815e9476c7d0a2887d2f7/ruff-0.12.8-py3-none-win_arm64.whl", hash = "sha256:c90e1a334683ce41b0e7a04f41790c429bf5073b62c1ae701c9dc5b3d14f0749", size = 11884718, upload-time = "2025-08-07T19:05:42.866Z" },
]
-[[package]]
-name = "setuptools"
-version = "75.3.2"
-source = { registry = "https://pypi.org/simple" }
-resolution-markers = [
- "python_full_version >= '3.8.1' and python_full_version < '3.9'",
- "python_full_version < '3.8.1'",
-]
-sdist = { url = "https://files.pythonhosted.org/packages/5c/01/771ea46cce201dd42cff043a5eea929d1c030fb3d1c2ee2729d02ca7814c/setuptools-75.3.2.tar.gz", hash = "sha256:3c1383e1038b68556a382c1e8ded8887cd20141b0eb5708a6c8d277de49364f5", size = 1354489, upload-time = "2025-03-12T00:02:19.004Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/15/65/3f0dba35760d902849d39d38c0a72767794b1963227b69a587f8a336d08c/setuptools-75.3.2-py3-none-any.whl", hash = "sha256:90ab613b6583fc02d5369cbca13ea26ea0e182d1df2d943ee9cbe81d4c61add9", size = 1251198, upload-time = "2025-03-12T00:02:17.554Z" },
-]
-
[[package]]
name = "setuptools"
version = "80.9.0"
source = { registry = "https://pypi.org/simple" }
-resolution-markers = [
- "python_full_version >= '3.11'",
- "python_full_version == '3.10.*'",
- "python_full_version == '3.9.*'",
-]
sdist = { url = "https://files.pythonhosted.org/packages/18/5d/3bf57dcd21979b887f014ea83c24ae194cfcd12b9e0fda66b957c69d1fca/setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c", size = 1319958, upload-time = "2025-05-27T00:56:51.443Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922", size = 1201486, upload-time = "2025-05-27T00:56:49.664Z" },
@@ -1859,14 +1492,13 @@ wheels = [
[[package]]
name = "setuptools-scm"
-source = { editable = "." }
+source = { editable = "setuptools-scm" }
dependencies = [
{ name = "packaging" },
- { name = "setuptools", version = "75.3.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" },
- { name = "setuptools", version = "80.9.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" },
+ { name = "setuptools" },
{ name = "tomli", marker = "python_full_version < '3.11'" },
- { name = "typing-extensions", version = "4.13.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" },
- { name = "typing-extensions", version = "4.14.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" },
+ { name = "typing-extensions", marker = "python_full_version < '3.11'" },
+ { name = "vcs-versioning" },
]
[package.optional-dependencies]
@@ -1876,36 +1508,28 @@ rich = [
[package.dev-dependencies]
docs = [
+ { name = "griffe-public-wildcard-imports" },
{ name = "mkdocs" },
{ name = "mkdocs-entangled-plugin", version = "0.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
{ name = "mkdocs-entangled-plugin", version = "0.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
- { name = "mkdocs-include-markdown-plugin", version = "6.2.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" },
- { name = "mkdocs-include-markdown-plugin", version = "7.1.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" },
+ { name = "mkdocs-include-markdown-plugin" },
{ name = "mkdocs-material" },
- { name = "mkdocstrings", version = "0.26.1", source = { registry = "https://pypi.org/simple" }, extra = ["python"], marker = "python_full_version < '3.9'" },
- { name = "mkdocstrings", version = "0.30.0", source = { registry = "https://pypi.org/simple" }, extra = ["python"], marker = "python_full_version >= '3.9'" },
+ { name = "mkdocstrings", extra = ["python"] },
{ name = "pygments" },
]
test = [
{ name = "build" },
- { name = "flake8", version = "5.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8.1'" },
- { name = "flake8", version = "7.1.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.8.1' and python_full_version < '3.9'" },
- { name = "flake8", version = "7.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" },
- { name = "griffe", version = "1.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" },
- { name = "griffe", version = "1.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" },
+ { name = "flake8" },
+ { name = "griffe" },
+ { name = "griffe-public-wildcard-imports" },
{ name = "mypy" },
- { name = "pip", version = "25.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" },
- { name = "pip", version = "25.1.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" },
- { name = "pytest", version = "8.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" },
- { name = "pytest", version = "8.4.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" },
+ { name = "pip" },
+ { name = "pytest" },
{ name = "pytest-timeout" },
- { name = "pytest-xdist", version = "3.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" },
- { name = "pytest-xdist", version = "3.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" },
+ { name = "pytest-xdist" },
{ name = "rich" },
{ name = "ruff" },
- { name = "typing-extensions", version = "4.13.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" },
- { name = "typing-extensions", version = "4.14.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9' and python_full_version < '3.11'" },
- { name = "wheel" },
+ { name = "typing-extensions", marker = "python_full_version < '3.11'" },
]
[package.metadata]
@@ -1914,12 +1538,14 @@ requires-dist = [
{ name = "rich", marker = "extra == 'rich'" },
{ name = "setuptools" },
{ name = "tomli", marker = "python_full_version < '3.11'", specifier = ">=1" },
- { name = "typing-extensions", marker = "python_full_version < '3.10'" },
+ { name = "typing-extensions", marker = "python_full_version < '3.11'" },
+ { name = "vcs-versioning", editable = "vcs-versioning" },
]
provides-extras = ["rich", "simple", "toml"]
[package.metadata.requires-dev]
docs = [
+ { name = "griffe-public-wildcard-imports" },
{ name = "mkdocs" },
{ name = "mkdocs-entangled-plugin" },
{ name = "mkdocs-include-markdown-plugin" },
@@ -1931,7 +1557,8 @@ test = [
{ name = "build" },
{ name = "flake8" },
{ name = "griffe" },
- { name = "mypy", specifier = "~=1.13.0" },
+ { name = "griffe-public-wildcard-imports" },
+ { name = "mypy" },
{ name = "pip" },
{ name = "pytest" },
{ name = "pytest-timeout" },
@@ -1939,7 +1566,6 @@ test = [
{ name = "rich" },
{ name = "ruff" },
{ name = "typing-extensions", marker = "python_full_version < '3.11'" },
- { name = "wheel" },
]
[[package]]
@@ -2000,27 +1626,32 @@ wheels = [
]
[[package]]
-name = "typing-extensions"
-version = "4.13.2"
+name = "towncrier"
+version = "25.8.0"
source = { registry = "https://pypi.org/simple" }
-resolution-markers = [
- "python_full_version >= '3.8.1' and python_full_version < '3.9'",
- "python_full_version < '3.8.1'",
+dependencies = [
+ { name = "click" },
+ { name = "jinja2" },
+ { name = "tomli", marker = "python_full_version < '3.11'" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/f6/37/23083fcd6e35492953e8d2aaaa68b860eb422b34627b13f2ce3eb6106061/typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef", size = 106967, upload-time = "2025-04-10T14:19:05.416Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/c2/eb/5bf25a34123698d3bbab39c5bc5375f8f8bcbcc5a136964ade66935b8b9d/towncrier-25.8.0.tar.gz", hash = "sha256:eef16d29f831ad57abb3ae32a0565739866219f1ebfbdd297d32894eb9940eb1", size = 76322, upload-time = "2025-08-30T11:41:55.393Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", size = 45806, upload-time = "2025-04-10T14:19:03.967Z" },
+ { url = "https://files.pythonhosted.org/packages/42/06/8ba22ec32c74ac1be3baa26116e3c28bc0e76a5387476921d20b6fdade11/towncrier-25.8.0-py3-none-any.whl", hash = "sha256:b953d133d98f9aeae9084b56a3563fd2519dfc6ec33f61c9cd2c61ff243fb513", size = 65101, upload-time = "2025-08-30T11:41:53.644Z" },
+]
+
+[[package]]
+name = "types-setuptools"
+version = "80.9.0.20250822"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/19/bd/1e5f949b7cb740c9f0feaac430e301b8f1c5f11a81e26324299ea671a237/types_setuptools-80.9.0.20250822.tar.gz", hash = "sha256:070ea7716968ec67a84c7f7768d9952ff24d28b65b6594797a464f1b3066f965", size = 41296, upload-time = "2025-08-22T03:02:08.771Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b6/2d/475bf15c1cdc172e7a0d665b6e373ebfb1e9bf734d3f2f543d668b07a142/types_setuptools-80.9.0.20250822-py3-none-any.whl", hash = "sha256:53bf881cb9d7e46ed12c76ef76c0aaf28cfe6211d3fab12e0b83620b1a8642c3", size = 63179, upload-time = "2025-08-22T03:02:07.643Z" },
]
[[package]]
name = "typing-extensions"
version = "4.14.1"
source = { registry = "https://pypi.org/simple" }
-resolution-markers = [
- "python_full_version >= '3.11'",
- "python_full_version == '3.10.*'",
- "python_full_version == '3.9.*'",
-]
sdist = { url = "https://files.pythonhosted.org/packages/98/5a/da40306b885cc8c09109dc2e1abd358d5684b1425678151cdaed4731c822/typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36", size = 107673, upload-time = "2025-07-04T13:28:34.16Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b5/00/d631e67a838026495268c2f6884f3711a15a9a2a96cd244fdaea53b823fb/typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76", size = 43906, upload-time = "2025-07-04T13:28:32.743Z" },
@@ -2031,7 +1662,7 @@ name = "typing-inspection"
version = "0.4.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
- { name = "typing-extensions", version = "4.14.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
+ { name = "typing-extensions", marker = "python_full_version >= '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/f8/b1/0c11f5058406b3af7609f121aaa6b609744687f1d158b3c3a5bf4cc94238/typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28", size = 75726, upload-time = "2025-05-21T18:55:23.885Z" }
wheels = [
@@ -2040,31 +1671,93 @@ wheels = [
[[package]]
name = "urllib3"
-version = "2.2.3"
+version = "2.5.0"
source = { registry = "https://pypi.org/simple" }
-resolution-markers = [
- "python_full_version >= '3.8.1' and python_full_version < '3.9'",
- "python_full_version < '3.8.1'",
-]
-sdist = { url = "https://files.pythonhosted.org/packages/ed/63/22ba4ebfe7430b76388e7cd448d5478814d3032121827c12a2cc287e2260/urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9", size = 300677, upload-time = "2024-09-12T10:52:18.401Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/ce/d9/5f4c13cecde62396b0d3fe530a50ccea91e7dfc1ccf0e09c228841bb5ba8/urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac", size = 126338, upload-time = "2024-09-12T10:52:16.589Z" },
+ { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" },
]
[[package]]
-name = "urllib3"
-version = "2.5.0"
-source = { registry = "https://pypi.org/simple" }
-resolution-markers = [
- "python_full_version >= '3.11'",
- "python_full_version == '3.10.*'",
- "python_full_version == '3.9.*'",
+name = "vcs-versioning"
+source = { editable = "vcs-versioning" }
+dependencies = [
+ { name = "packaging" },
+ { name = "tomli", marker = "python_full_version < '3.11'" },
+ { name = "typing-extensions", marker = "python_full_version < '3.11'" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" },
+
+[package.dev-dependencies]
+test = [
+ { name = "pytest" },
+ { name = "pytest-cov" },
+ { name = "pytest-xdist" },
+]
+
+[package.metadata]
+requires-dist = [
+ { name = "packaging", specifier = ">=20" },
+ { name = "tomli", marker = "python_full_version < '3.11'", specifier = ">=1" },
+ { name = "typing-extensions", marker = "python_full_version < '3.11'" },
+]
+
+[package.metadata.requires-dev]
+test = [
+ { name = "pytest" },
+ { name = "pytest-cov" },
+ { name = "pytest-xdist" },
]
+[[package]]
+name = "vcs-versioning-workspace"
+version = "0.1+private"
+source = { editable = "." }
+dependencies = [
+ { name = "setuptools-scm" },
+ { name = "vcs-versioning" },
+]
+
+[package.dev-dependencies]
+docs = [
+ { name = "griffe-public-wildcard-imports" },
+ { name = "mkdocs" },
+ { name = "mkdocs-entangled-plugin", version = "0.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
+ { name = "mkdocs-entangled-plugin", version = "0.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
+ { name = "mkdocs-include-markdown-plugin" },
+ { name = "mkdocs-material" },
+ { name = "mkdocstrings", extra = ["python"] },
+ { name = "pygments" },
+]
+release = [
+ { name = "pygithub", marker = "sys_platform != 'win32'" },
+ { name = "towncrier" },
+]
+typing = [
+ { name = "types-setuptools" },
+]
+
+[package.metadata]
+requires-dist = [
+ { name = "setuptools-scm", editable = "setuptools-scm" },
+ { name = "vcs-versioning", editable = "vcs-versioning" },
+]
+
+[package.metadata.requires-dev]
+docs = [
+ { name = "griffe-public-wildcard-imports" },
+ { name = "mkdocs" },
+ { name = "mkdocs-entangled-plugin" },
+ { name = "mkdocs-include-markdown-plugin" },
+ { name = "mkdocs-material" },
+ { name = "mkdocstrings", extras = ["python"] },
+ { name = "pygments" },
+]
+release = [
+ { name = "pygithub", marker = "sys_platform != 'win32'", specifier = ">=2.0.0" },
+ { name = "towncrier", specifier = ">=23.11.0" },
+]
+typing = [{ name = "types-setuptools" }]
+
[[package]]
name = "watchdog"
version = "3.0.0"
@@ -2077,14 +1770,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/00/9e/a9711f35f1ad6571e92dc2e955e7de9dfac21a1b33e9cd212f066a60a387/watchdog-3.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2b57a1e730af3156d13b7fdddfc23dea6487fceca29fc75c5a868beed29177ae", size = 100700, upload-time = "2023-03-20T09:20:29.847Z" },
{ url = "https://files.pythonhosted.org/packages/84/ab/67001e62603bf2ea35ace40023f7c74f61e8b047160d6bb078373cec1a67/watchdog-3.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7ade88d0d778b1b222adebcc0927428f883db07017618a5e684fd03b83342bd9", size = 91251, upload-time = "2023-03-20T09:20:31.892Z" },
{ url = "https://files.pythonhosted.org/packages/58/db/d419fdbd3051b42b0a8091ddf78f70540b6d9d277a84845f7c5955f9de92/watchdog-3.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7e447d172af52ad204d19982739aa2346245cc5ba6f579d16dac4bfec226d2e7", size = 91753, upload-time = "2023-03-20T09:20:33.337Z" },
- { url = "https://files.pythonhosted.org/packages/7f/6e/7ca8ed16928d7b11da69372f55c64a09dce649d2b24b03f7063cd8683c4b/watchdog-3.0.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:8ae9cda41fa114e28faf86cb137d751a17ffd0316d1c34ccf2235e8a84365c7f", size = 100655, upload-time = "2023-03-20T09:20:37.473Z" },
- { url = "https://files.pythonhosted.org/packages/2e/54/48527f3aea4f7ed331072352fee034a7f3d6ec7a2ed873681738b2586498/watchdog-3.0.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:25f70b4aa53bd743729c7475d7ec41093a580528b100e9a8c5b5efe8899592fc", size = 91216, upload-time = "2023-03-20T09:20:39.793Z" },
- { url = "https://files.pythonhosted.org/packages/dc/89/3a3ce6dd01807ff918aec3bbcabc92ed1a7edc5bb2266c720bb39fec1bec/watchdog-3.0.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4f94069eb16657d2c6faada4624c39464f65c05606af50bb7902e036e3219be3", size = 91752, upload-time = "2023-03-20T09:20:41.395Z" },
- { url = "https://files.pythonhosted.org/packages/75/fe/d9a37d8df76878853f68dd665ec6d2c7a984645de460164cb880a93ffe6b/watchdog-3.0.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7c5f84b5194c24dd573fa6472685b2a27cc5a17fe5f7b6fd40345378ca6812e3", size = 100653, upload-time = "2023-03-20T09:20:42.936Z" },
- { url = "https://files.pythonhosted.org/packages/94/ce/70c65a6c4b0330129c402624d42f67ce82d6a0ba2036de67628aeffda3c1/watchdog-3.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3aa7f6a12e831ddfe78cdd4f8996af9cf334fd6346531b16cec61c3b3c0d8da0", size = 91247, upload-time = "2023-03-20T09:20:45.157Z" },
- { url = "https://files.pythonhosted.org/packages/51/b9/444a984b1667013bac41b31b45d9718e069cc7502a43a924896806605d83/watchdog-3.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:233b5817932685d39a7896b1090353fc8efc1ef99c9c054e46c8002561252fb8", size = 91753, upload-time = "2023-03-20T09:20:46.913Z" },
- { url = "https://files.pythonhosted.org/packages/ea/76/bef1c6f6ac18041234a9f3e8bc995d611e255c44f10433bfaf255968c269/watchdog-3.0.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:8f3ceecd20d71067c7fd4c9e832d4e22584318983cabc013dbf3f70ea95de346", size = 90419, upload-time = "2023-03-20T09:20:50.715Z" },
- { url = "https://files.pythonhosted.org/packages/30/65/9e36a3c821d47a22e54a8fc73681586b2d26e82d24ea3af63acf2ef78f97/watchdog-3.0.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:c9d8c8ec7efb887333cf71e328e39cffbf771d8f8f95d308ea4125bf5f90ba64", size = 90428, upload-time = "2023-03-20T09:20:52.216Z" },
{ url = "https://files.pythonhosted.org/packages/92/28/631872d7fbc45527037060db8c838b47a129a6c09d2297d6dddcfa283cf2/watchdog-3.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:0e06ab8858a76e1219e68c7573dfeba9dd1c0219476c5a44d5333b01d7e1743a", size = 82049, upload-time = "2023-03-20T09:20:53.951Z" },
{ url = "https://files.pythonhosted.org/packages/c0/a2/4e3230bdc1fb878b152a2c66aa941732776f4545bd68135d490591d66713/watchdog-3.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:d00e6be486affb5781468457b21a6cbe848c33ef43f9ea4a73b4882e5f188a44", size = 82049, upload-time = "2023-03-20T09:20:55.583Z" },
{ url = "https://files.pythonhosted.org/packages/21/72/46fd174352cd88b9157ade77e3b8835125d4b1e5186fc7f1e8c44664e029/watchdog-3.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:c07253088265c363d1ddf4b3cdb808d59a0468ecd017770ed716991620b8f77a", size = 82052, upload-time = "2023-03-20T09:20:57.124Z" },
@@ -2097,33 +1782,12 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/ba/0c/cd0337069c468f22ef256e768ece74c78b511092f1004ab260268e1af4a9/watchdog-3.0.0-py3-none-win_ia64.whl", hash = "sha256:5d9f3a10e02d7371cd929b5d8f11e87d4bad890212ed3901f9b4d68767bee759", size = 82040, upload-time = "2023-03-20T09:21:09.178Z" },
]
-[[package]]
-name = "wcmatch"
-version = "10.0"
-source = { registry = "https://pypi.org/simple" }
-resolution-markers = [
- "python_full_version >= '3.8.1' and python_full_version < '3.9'",
- "python_full_version < '3.8.1'",
-]
-dependencies = [
- { name = "bracex", version = "2.5.post1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/41/ab/b3a52228538ccb983653c446c1656eddf1d5303b9cb8b9aef6a91299f862/wcmatch-10.0.tar.gz", hash = "sha256:e72f0de09bba6a04e0de70937b0cf06e55f36f37b3deb422dfaf854b867b840a", size = 115578, upload-time = "2024-09-26T18:39:52.505Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/ab/df/4ee467ab39cc1de4b852c212c1ed3becfec2e486a51ac1ce0091f85f38d7/wcmatch-10.0-py3-none-any.whl", hash = "sha256:0dd927072d03c0a6527a20d2e6ad5ba8d0380e60870c383bc533b71744df7b7a", size = 39347, upload-time = "2024-09-26T18:39:51.002Z" },
-]
-
[[package]]
name = "wcmatch"
version = "10.1"
source = { registry = "https://pypi.org/simple" }
-resolution-markers = [
- "python_full_version >= '3.11'",
- "python_full_version == '3.10.*'",
- "python_full_version == '3.9.*'",
-]
dependencies = [
- { name = "bracex", version = "2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" },
+ { name = "bracex" },
]
sdist = { url = "https://files.pythonhosted.org/packages/79/3e/c0bdc27cf06f4e47680bd5803a07cb3dfd17de84cde92dd217dcb9e05253/wcmatch-10.1.tar.gz", hash = "sha256:f11f94208c8c8484a16f4f48638a85d771d9513f4ab3f37595978801cb9465af", size = 117421, upload-time = "2025-06-22T19:14:02.49Z" }
wheels = [
@@ -2139,36 +1803,10 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/fd/84/fd2ba7aafacbad3c4201d395674fc6348826569da3c0937e75505ead3528/wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859", size = 34166, upload-time = "2024-01-06T02:10:55.763Z" },
]
-[[package]]
-name = "wheel"
-version = "0.45.1"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/8a/98/2d9906746cdc6a6ef809ae6338005b3f21bb568bea3165cfc6a243fdc25c/wheel-0.45.1.tar.gz", hash = "sha256:661e1abd9198507b1409a20c02106d9670b2576e916d58f520316666abca6729", size = 107545, upload-time = "2024-11-23T00:18:23.513Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/0b/2c/87f3254fd8ffd29e4c02732eee68a83a1d3c346ae39bc6822dcbcb697f2b/wheel-0.45.1-py3-none-any.whl", hash = "sha256:708e7481cc80179af0e556bbf0cc00b8444c7321e2700b8d8580231d13017248", size = 72494, upload-time = "2024-11-23T00:18:21.207Z" },
-]
-
-[[package]]
-name = "zipp"
-version = "3.20.2"
-source = { registry = "https://pypi.org/simple" }
-resolution-markers = [
- "python_full_version >= '3.8.1' and python_full_version < '3.9'",
- "python_full_version < '3.8.1'",
-]
-sdist = { url = "https://files.pythonhosted.org/packages/54/bf/5c0000c44ebc80123ecbdddba1f5dcd94a5ada602a9c225d84b5aaa55e86/zipp-3.20.2.tar.gz", hash = "sha256:bc9eb26f4506fda01b81bcde0ca78103b6e62f991b381fec825435c836edbc29", size = 24199, upload-time = "2024-09-13T13:44:16.101Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/62/8b/5ba542fa83c90e09eac972fc9baca7a88e7e7ca4b221a89251954019308b/zipp-3.20.2-py3-none-any.whl", hash = "sha256:a817ac80d6cf4b23bf7f2828b7cabf326f15a001bea8b1f9b49631780ba28350", size = 9200, upload-time = "2024-09-13T13:44:14.38Z" },
-]
-
[[package]]
name = "zipp"
version = "3.23.0"
source = { registry = "https://pypi.org/simple" }
-resolution-markers = [
- "python_full_version == '3.10.*'",
- "python_full_version == '3.9.*'",
-]
sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547, upload-time = "2025-06-08T17:06:39.4Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" },
diff --git a/vcs-versioning/CHANGELOG.md b/vcs-versioning/CHANGELOG.md
new file mode 100644
index 00000000..f0dec6e4
--- /dev/null
+++ b/vcs-versioning/CHANGELOG.md
@@ -0,0 +1,8 @@
+# Changelog
+
+
+
+## 0.1.1
+
+Initial release of vcs-versioning as a separate package extracted from setuptools-scm.
+
diff --git a/nextgen/vcs-versioning/LICENSE.txt b/vcs-versioning/LICENSE.txt
similarity index 100%
rename from nextgen/vcs-versioning/LICENSE.txt
rename to vcs-versioning/LICENSE.txt
diff --git a/nextgen/vcs-versioning/README.md b/vcs-versioning/README.md
similarity index 100%
rename from nextgen/vcs-versioning/README.md
rename to vcs-versioning/README.md
diff --git a/vcs-versioning/_own_version_of_vcs_versioning.py b/vcs-versioning/_own_version_of_vcs_versioning.py
new file mode 100644
index 00000000..d8ee6b7c
--- /dev/null
+++ b/vcs-versioning/_own_version_of_vcs_versioning.py
@@ -0,0 +1,84 @@
+"""
+Version helper for vcs-versioning package.
+
+This module allows vcs-versioning to use VCS metadata for its own version,
+with the tag prefix 'vcs-versioning-'.
+
+Used by hatchling's code version source.
+"""
+
+from __future__ import annotations
+
+import logging
+import os
+from collections.abc import Callable
+
+from vcs_versioning import Configuration
+from vcs_versioning import _types as _t
+from vcs_versioning._backends import _git as git
+from vcs_versioning._backends import _hg as hg
+from vcs_versioning._fallbacks import fallback_version, parse_pkginfo
+from vcs_versioning._get_version_impl import get_version
+from vcs_versioning._version_schemes import (
+ ScmVersion,
+ get_local_node_and_date,
+ get_no_local_node,
+ guess_next_dev_version,
+)
+
+log = logging.getLogger("vcs_versioning")
+
+# Try these parsers in order for vcs-versioning's own version
+try_parse: list[Callable[[_t.PathT, Configuration], ScmVersion | None]] = [
+ parse_pkginfo,
+ git.parse,
+ hg.parse,
+ git.parse_archival,
+ hg.parse_archival,
+ fallback_version, # Last resort: use fallback_version from config
+]
+
+
+def parse(root: str, config: Configuration) -> ScmVersion | None:
+ for maybe_parse in try_parse:
+ try:
+ parsed = maybe_parse(root, config)
+ except OSError as e:
+ log.warning("parse with %s failed with: %s", maybe_parse, e)
+ else:
+ if parsed is not None:
+ return parsed
+ return None
+
+
+def _get_version() -> str:
+ """Get version from VCS with vcs-versioning- tag prefix."""
+ # Use no-local-version if VCS_VERSIONING_NO_LOCAL is set (for CI uploads)
+ local_scheme = (
+ get_no_local_node
+ if os.environ.get("VCS_VERSIONING_NO_LOCAL")
+ else get_local_node_and_date
+ )
+
+ # __file__ is nextgen/vcs-versioning/_own_version_helper.py
+ # pyproject.toml is in nextgen/vcs-versioning/pyproject.toml
+ pyproject_path = os.path.join(os.path.dirname(__file__), "pyproject.toml")
+
+ # root is the git repo root (../..)
+ # fallback_root is the vcs-versioning package dir (.)
+ # relative_to anchors to pyproject.toml
+ # fallback_version is used when no vcs-versioning- tags exist yet
+ return get_version(
+ root="../..",
+ fallback_root=".",
+ relative_to=pyproject_path,
+ parse=parse,
+ version_scheme=guess_next_dev_version,
+ local_scheme=local_scheme,
+ tag_regex=r"^vcs-versioning-(?P[vV]?\d+(?:\.\d+){0,2}[^\+]*)(?:\+.*)?$",
+ git_describe_command="git describe --dirty --tags --long --match 'vcs-versioning-*'",
+ fallback_version="0.1.1+pre.tag",
+ )
+
+
+__version__: str = _get_version()
diff --git a/vcs-versioning/changelog.d/.gitkeep b/vcs-versioning/changelog.d/.gitkeep
new file mode 100644
index 00000000..c199cfd6
--- /dev/null
+++ b/vcs-versioning/changelog.d/.gitkeep
@@ -0,0 +1,8 @@
+# Changelog fragments directory
+# Add your changelog fragments here following the naming convention:
+# {issue_number}.{type}.md
+#
+# Where type is one of: feature, bugfix, deprecation, removal, doc, misc
+#
+# Example: 123.feature.md
+
diff --git a/vcs-versioning/changelog.d/README.md b/vcs-versioning/changelog.d/README.md
new file mode 100644
index 00000000..3ea52129
--- /dev/null
+++ b/vcs-versioning/changelog.d/README.md
@@ -0,0 +1,32 @@
+# Changelog Fragments
+
+This directory contains changelog fragments that will be assembled into the CHANGELOG.md file during release.
+
+## Fragment Types
+
+- **feature**: New features or enhancements
+- **bugfix**: Bug fixes
+- **deprecation**: Deprecation warnings
+- **removal**: Removed features (breaking changes)
+- **doc**: Documentation improvements
+- **misc**: Internal changes, refactoring, etc.
+
+## Naming Convention
+
+Fragments should be named: `{issue_number}.{type}.md`
+
+Examples:
+- `123.feature.md` - New feature related to issue #123
+- `456.bugfix.md` - Bug fix for issue #456
+- `789.doc.md` - Documentation update for issue #789
+
+## Content
+
+Each fragment should contain a brief description of the change:
+
+```markdown
+Add support for custom version schemes via plugin system
+```
+
+Do not include issue numbers in the content - they will be added automatically.
+
diff --git a/vcs-versioning/changelog.d/cli-package.misc.md b/vcs-versioning/changelog.d/cli-package.misc.md
new file mode 100644
index 00000000..4615653a
--- /dev/null
+++ b/vcs-versioning/changelog.d/cli-package.misc.md
@@ -0,0 +1,2 @@
+Converted _cli module into a package with improved structure. Archival templates moved to resource files. Added CliNamespace for typed arguments.
+
diff --git a/vcs-versioning/changelog.d/cli-typesafety.misc.md b/vcs-versioning/changelog.d/cli-typesafety.misc.md
new file mode 100644
index 00000000..f032bc70
--- /dev/null
+++ b/vcs-versioning/changelog.d/cli-typesafety.misc.md
@@ -0,0 +1,2 @@
+Improved CLI type safety with OutputData TypedDict and better type annotations throughout CLI handling.
+
diff --git a/vcs-versioning/changelog.d/env-reader.feature.md b/vcs-versioning/changelog.d/env-reader.feature.md
new file mode 100644
index 00000000..15f3763f
--- /dev/null
+++ b/vcs-versioning/changelog.d/env-reader.feature.md
@@ -0,0 +1,2 @@
+Add EnvReader class for structured reading of environment variable overrides with tool prefixes and distribution-specific variants (e.g., SETUPTOOLS_SCM_PRETEND vs VCS_VERSIONING_PRETEND).
+
diff --git a/vcs-versioning/changelog.d/initial-release.feature.md b/vcs-versioning/changelog.d/initial-release.feature.md
new file mode 100644
index 00000000..57cad615
--- /dev/null
+++ b/vcs-versioning/changelog.d/initial-release.feature.md
@@ -0,0 +1,2 @@
+Initial release of vcs-versioning as a standalone package. Core version inference logic extracted from setuptools-scm for reuse by other build backends and tools.
+
diff --git a/vcs-versioning/changelog.d/integrator-api.feature.md b/vcs-versioning/changelog.d/integrator-api.feature.md
new file mode 100644
index 00000000..cc1bcf61
--- /dev/null
+++ b/vcs-versioning/changelog.d/integrator-api.feature.md
@@ -0,0 +1,2 @@
+Add experimental integrator workflow API for composable configuration building. Allows build backends to progressively build Configuration objects from pyproject.toml, distribution metadata, and manual overrides.
+
diff --git a/vcs-versioning/changelog.d/modernize-types.misc.md b/vcs-versioning/changelog.d/modernize-types.misc.md
new file mode 100644
index 00000000..439f2e4f
--- /dev/null
+++ b/vcs-versioning/changelog.d/modernize-types.misc.md
@@ -0,0 +1,2 @@
+Modernized type annotations to Python 3.10+ syntax throughout codebase. Generated version files now use modern `tuple[int | str, ...]` syntax with `from __future__ import annotations`.
+
diff --git a/vcs-versioning/changelog.d/overrides-validation.misc.md b/vcs-versioning/changelog.d/overrides-validation.misc.md
new file mode 100644
index 00000000..70d7c933
--- /dev/null
+++ b/vcs-versioning/changelog.d/overrides-validation.misc.md
@@ -0,0 +1,2 @@
+Enhanced GlobalOverrides: env_reader is now a required validated field. additional_loggers changed from string to tuple of logger instances for better type safety.
+
diff --git a/vcs-versioning/changelog.d/py310.feature.md b/vcs-versioning/changelog.d/py310.feature.md
new file mode 100644
index 00000000..7b60acb5
--- /dev/null
+++ b/vcs-versioning/changelog.d/py310.feature.md
@@ -0,0 +1,2 @@
+Requires Python 3.10 or newer. Modern type annotations and language features used throughout.
+
diff --git a/vcs-versioning/changelog.d/template.md b/vcs-versioning/changelog.d/template.md
new file mode 100644
index 00000000..41a46689
--- /dev/null
+++ b/vcs-versioning/changelog.d/template.md
@@ -0,0 +1,21 @@
+{% for section, _ in sections.items() %}
+{% set underline = underlines[0] %}{% if section %}{{section}}
+{{ underline * section|length }}{% set underline = underlines[1] %}
+
+{% endif %}
+{% if sections[section] %}
+{% for category, val in definitions.items() if category in sections[section]%}
+
+### {{ definitions[category]['name'] }}
+
+{% for text, values in sections[section][category].items() %}
+- {{ text }} ({{ values|join(', ') }})
+{% endfor %}
+
+{% endfor %}
+{% else %}
+No significant changes.
+
+{% endif %}
+{% endfor %}
+
diff --git a/vcs-versioning/changelog.d/towncrier-scheme.feature.md b/vcs-versioning/changelog.d/towncrier-scheme.feature.md
new file mode 100644
index 00000000..9f8bcc9d
--- /dev/null
+++ b/vcs-versioning/changelog.d/towncrier-scheme.feature.md
@@ -0,0 +1,2 @@
+Add towncrier-fragments version scheme that infers version bumps based on changelog fragment types (feature=minor, bugfix=patch, removal=major).
+
diff --git a/vcs-versioning/pyproject.toml b/vcs-versioning/pyproject.toml
new file mode 100644
index 00000000..f54f892d
--- /dev/null
+++ b/vcs-versioning/pyproject.toml
@@ -0,0 +1,170 @@
+[build-system]
+build-backend = "hatchling.build"
+requires = [
+ "hatchling",
+ "packaging>=20",
+ 'typing-extensions; python_version < "3.11"',
+]
+
+[project]
+name = "vcs-versioning"
+description = "the blessed package to manage your versions by vcs metadata"
+readme = "README.md"
+keywords = [
+]
+license = "MIT"
+authors = [
+ { name = "Ronny Pfannschmidt", email = "opensource@ronnypfannschmidt.de" },
+]
+requires-python = ">=3.10"
+classifiers = [
+ "Development Status :: 1 - Planning",
+ "Programming Language :: Python",
+ "Programming Language :: Python :: 3 :: Only",
+ "Programming Language :: Python :: 3.10",
+ "Programming Language :: Python :: 3.11",
+ "Programming Language :: Python :: 3.12",
+ "Programming Language :: Python :: 3.13",
+]
+dynamic = [
+ "version",
+]
+dependencies = [
+ "packaging>=20",
+ 'tomli>=1; python_version < "3.11"',
+ 'typing-extensions; python_version < "3.11"',
+]
+
+[dependency-groups]
+test = [
+ "pytest",
+ "pytest-cov",
+ "pytest-xdist",
+]
+
+[project.urls]
+Documentation = "https://github.com/pypa/setuptools-scm#readme"
+Issues = "https://github.com/pypa/setuptools-scm/issues"
+Source = "https://github.com/pypa/setuptools-scm"
+
+[project.scripts]
+"vcs-versioning" = "vcs_versioning._cli:main"
+
+[project.entry-points."setuptools_scm.parse_scm"]
+".git" = "vcs_versioning._backends._git:parse"
+".hg" = "vcs_versioning._backends._hg:parse"
+
+[project.entry-points."setuptools_scm.parse_scm_fallback"]
+".git_archival.txt" = "vcs_versioning._backends._git:parse_archival"
+".hg_archival.txt" = "vcs_versioning._backends._hg:parse_archival"
+"PKG-INFO" = "vcs_versioning._fallbacks:parse_pkginfo"
+"pyproject.toml" = "vcs_versioning._fallbacks:fallback_version"
+"setup.py" = "vcs_versioning._fallbacks:fallback_version"
+
+[project.entry-points."setuptools_scm.local_scheme"]
+dirty-tag = "vcs_versioning._version_schemes:get_local_dirty_tag"
+no-local-version = "vcs_versioning._version_schemes:get_no_local_node"
+node-and-date = "vcs_versioning._version_schemes:get_local_node_and_date"
+node-and-timestamp = "vcs_versioning._version_schemes:get_local_node_and_timestamp"
+
+[project.entry-points."setuptools_scm.version_scheme"]
+"calver-by-date" = "vcs_versioning._version_schemes:calver_by_date"
+"guess-next-dev" = "vcs_versioning._version_schemes:guess_next_dev_version"
+"no-guess-dev" = "vcs_versioning._version_schemes:no_guess_dev_version"
+"only-version" = "vcs_versioning._version_schemes:only_version"
+"post-release" = "vcs_versioning._version_schemes:postrelease_version"
+"python-simplified-semver" = "vcs_versioning._version_schemes:simplified_semver_version"
+"release-branch-semver" = "vcs_versioning._version_schemes:release_branch_semver_version"
+"towncrier-fragments" = "vcs_versioning._version_schemes._towncrier:version_from_fragments"
+
+[tool.hatch.version]
+source = "code"
+path = "_own_version_of_vcs_versioning.py"
+search-paths = ["src"]
+
+[tool.hatch.build.targets.wheel]
+packages = ["src/vcs_versioning"]
+
+[tool.hatch.envs.default]
+dependencies = [
+ "pytest",
+ "pytest-cov",
+]
+[tool.hatch.envs.default.scripts]
+cov = "pytest --cov-report=term-missing --cov-config=pyproject.toml --cov=vcs_versioning --cov=tests {args}"
+no-cov = "cov --no-cov {args}"
+
+[[tool.hatch.envs.test.matrix]]
+python = [ "38", "39", "310", "311", "312", "313" ]
+
+[tool.vcs-versioning]
+root = ".."
+version_scheme = "towncrier-fragments"
+tag_regex = "^vcs-versioning-(?Pv?\\d+(?:\\.\\d+){0,2}[^\\+]*)(?:\\+.*)?$"
+fallback_version = "0.1.0"
+scm.git.describe_command = ["git", "describe", "--dirty", "--tags", "--long", "--match", "vcs-versioning-*"]
+
+[tool.coverage.run]
+branch = true
+parallel = true
+omit = [
+ "vcs_versioning/__about__.py",
+]
+
+[tool.coverage.report]
+exclude_lines = [
+ "no cov",
+ "if __name__ == .__main__.:",
+ "if TYPE_CHECKING:",
+]
+
+[tool.pytest.ini_options]
+testpaths = ["testing_vcs"]
+python_files = ["test_*.py"]
+addopts = ["-ra", "--strict-markers", "-p", "vcs_versioning.test_api"]
+markers = [
+ "issue: marks tests related to specific issues",
+ "skip_commit: allows to skip committing in the helpers",
+]
+
+[tool.uv]
+default-groups = ["test"]
+
+[tool.towncrier]
+directory = "changelog.d"
+filename = "CHANGELOG.md"
+start_string = "\n"
+template = "changelog.d/template.md"
+title_format = "## {version} ({project_date})"
+issue_format = "[#{issue}](https://github.com/pypa/setuptools-scm/issues/{issue})"
+underlines = ["", "", ""]
+
+[[tool.towncrier.type]]
+directory = "removal"
+name = "Removed"
+showcontent = true
+
+[[tool.towncrier.type]]
+directory = "deprecation"
+name = "Deprecated"
+showcontent = true
+
+[[tool.towncrier.type]]
+directory = "feature"
+name = "Added"
+showcontent = true
+
+[[tool.towncrier.type]]
+directory = "bugfix"
+name = "Fixed"
+showcontent = true
+
+[[tool.towncrier.type]]
+directory = "doc"
+name = "Documentation"
+showcontent = true
+
+[[tool.towncrier.type]]
+directory = "misc"
+name = "Miscellaneous"
+showcontent = true
diff --git a/vcs-versioning/src/vcs_versioning/__init__.py b/vcs-versioning/src/vcs_versioning/__init__.py
new file mode 100644
index 00000000..53184038
--- /dev/null
+++ b/vcs-versioning/src/vcs_versioning/__init__.py
@@ -0,0 +1,110 @@
+"""VCS-based versioning for Python packages
+
+Core functionality for version management based on VCS metadata.
+"""
+
+from __future__ import annotations
+
+from typing import Any
+
+# Public API exports
+from ._config import DEFAULT_LOCAL_SCHEME, DEFAULT_VERSION_SCHEME, Configuration
+from ._pyproject_reading import PyProjectData
+from ._scm_version import ScmVersion
+from ._version_cls import NonNormalizedVersion, Version
+from ._version_inference import infer_version_string
+
+
+def build_configuration_from_pyproject(
+ pyproject_data: PyProjectData,
+ *,
+ dist_name: str | None = None,
+ **integrator_overrides: Any,
+) -> Configuration:
+ """Build Configuration from PyProjectData with full workflow.
+
+ EXPERIMENTAL API for integrators.
+
+ This helper orchestrates the complete configuration building workflow:
+ 1. Extract config from pyproject_data.section
+ 2. Determine dist_name (argument > pyproject.project_name)
+ 3. Apply integrator overrides (override config file)
+ 4. Apply environment TOML overrides (highest priority)
+ 5. Create and validate Configuration instance
+
+ Integrators create PyProjectData themselves:
+
+ Example 1 - From file:
+ >>> from vcs_versioning import PyProjectData, build_configuration_from_pyproject
+ >>> from vcs_versioning.overrides import GlobalOverrides
+ >>>
+ >>> with GlobalOverrides.from_env("HATCH_VCS", dist_name="my-pkg"):
+ ... pyproject = PyProjectData.from_file("pyproject.toml")
+ ... config = build_configuration_from_pyproject(
+ ... pyproject_data=pyproject,
+ ... dist_name="my-pkg",
+ ... )
+
+ Example 2 - Manual composition:
+ >>> from pathlib import Path
+ >>> from vcs_versioning import PyProjectData, build_configuration_from_pyproject
+ >>>
+ >>> pyproject = PyProjectData(
+ ... path=Path("pyproject.toml"),
+ ... tool_name="vcs-versioning",
+ ... project={"name": "my-pkg"},
+ ... section={"local_scheme": "no-local-version"},
+ ... is_required=True,
+ ... section_present=True,
+ ... project_present=True,
+ ... build_requires=[],
+ ... )
+ >>> config = build_configuration_from_pyproject(
+ ... pyproject_data=pyproject,
+ ... version_scheme="release-branch-semver", # Integrator override
+ ... )
+
+ Args:
+ pyproject_data: Parsed pyproject data (integrator creates this)
+ dist_name: Distribution name (overrides pyproject_data.project_name)
+ **integrator_overrides: Integrator-provided config overrides
+ (override config file, but overridden by env)
+
+ Returns:
+ Configured Configuration instance ready for version inference
+
+ Priority order (highest to lowest):
+ 1. Environment TOML overrides (TOOL_OVERRIDES_FOR_DIST, TOOL_OVERRIDES)
+ 2. Integrator **overrides arguments
+ 3. pyproject_data.section configuration
+ 4. Configuration defaults
+
+ This allows integrators to provide their own transformations
+ while still respecting user environment variable overrides.
+ """
+ from ._integrator_helpers import build_configuration_from_pyproject_internal
+
+ return build_configuration_from_pyproject_internal(
+ pyproject_data=pyproject_data,
+ dist_name=dist_name,
+ **integrator_overrides,
+ )
+
+
+__all__ = [
+ "DEFAULT_LOCAL_SCHEME",
+ "DEFAULT_VERSION_SCHEME",
+ "Configuration",
+ "NonNormalizedVersion",
+ "PyProjectData",
+ "ScmVersion",
+ "Version",
+ "build_configuration_from_pyproject",
+ "infer_version_string",
+]
+
+# Experimental API markers for documentation
+__experimental__ = [
+ "PyProjectData",
+ "build_configuration_from_pyproject",
+]
diff --git a/src/setuptools_scm/__main__.py b/vcs-versioning/src/vcs_versioning/__main__.py
similarity index 100%
rename from src/setuptools_scm/__main__.py
rename to vcs-versioning/src/vcs_versioning/__main__.py
diff --git a/nextgen/vcs-versioning/vcs_versioning/__about__.py b/vcs-versioning/src/vcs_versioning/_backends/__init__.py
similarity index 50%
rename from nextgen/vcs-versioning/vcs_versioning/__about__.py
rename to vcs-versioning/src/vcs_versioning/_backends/__init__.py
index eba4921f..4fc1cea6 100644
--- a/nextgen/vcs-versioning/vcs_versioning/__about__.py
+++ b/vcs-versioning/src/vcs_versioning/_backends/__init__.py
@@ -1,3 +1,3 @@
-from __future__ import annotations
+"""VCS backends (private module)"""
-__version__ = "0.0.1"
+from __future__ import annotations
diff --git a/src/setuptools_scm/git.py b/vcs-versioning/src/vcs_versioning/_backends/_git.py
similarity index 94%
rename from src/setuptools_scm/git.py
rename to vcs-versioning/src/vcs_versioning/_backends/_git.py
index 966ab69c..ebff0ea2 100644
--- a/src/setuptools_scm/git.py
+++ b/vcs-versioning/src/vcs_versioning/_backends/_git.py
@@ -7,32 +7,25 @@
import shlex
import sys
import warnings
-
-from datetime import date
-from datetime import datetime
-from datetime import timezone
+from collections.abc import Callable, Sequence
+from datetime import date, datetime, timezone
from enum import Enum
from os.path import samefile
from pathlib import Path
from typing import TYPE_CHECKING
-from typing import Callable
-from typing import Sequence
-
-from . import Configuration
-from . import _types as _t
-from . import discover
-from ._run_cmd import CompletedProcess as _CompletedProcess
-from ._run_cmd import require_command as _require_command
-from ._run_cmd import run as _run
-from .integration import data_from_mime
-from .scm_workdir import Workdir
-from .scm_workdir import get_latest_file_mtime
-from .version import ScmVersion
-from .version import meta
-from .version import tag_to_version
+
+from .. import _discover as discover
+from .. import _types as _t
+from .._config import Configuration
+from .._integration import data_from_mime
+from .._run_cmd import CompletedProcess as _CompletedProcess
+from .._run_cmd import require_command as _require_command
+from .._run_cmd import run as _run
+from .._scm_version import ScmVersion, meta, tag_to_version
+from ._scm_workdir import Workdir, get_latest_file_mtime
if TYPE_CHECKING:
- from . import hg_git
+ from . import _hg_git as hg_git
log = logging.getLogger(__name__)
REF_TAG_RE = re.compile(r"(?<=\btag: )([^,]+)\b")
@@ -93,7 +86,7 @@ def from_potential_worktree(cls, wd: _t.PathT) -> GitWorkdir | None:
real_wd = os.fspath(wd)
else:
str_wd = os.fspath(wd)
- from ._compat import strip_path_suffix
+ from .._compat import strip_path_suffix
real_wd = strip_path_suffix(str_wd, real_wd)
log.debug("real root %s", real_wd)
@@ -199,13 +192,15 @@ def default_describe(self) -> _CompletedProcess:
def warn_on_shallow(wd: GitWorkdir) -> None:
"""experimental, may change at any time"""
if wd.is_shallow():
- warnings.warn(f'"{wd.path}" is shallow and may cause errors')
+ warnings.warn(f'"{wd.path}" is shallow and may cause errors', stacklevel=2)
def fetch_on_shallow(wd: GitWorkdir) -> None:
"""experimental, may change at any time"""
if wd.is_shallow():
- warnings.warn(f'"{wd.path}" was shallow, git fetch was used to rectify')
+ warnings.warn(
+ f'"{wd.path}" was shallow, git fetch was used to rectify', stacklevel=2
+ )
wd.fetch_shallow()
@@ -424,7 +419,7 @@ def archival_to_version(
log.debug("data %s", data)
archival_describe = data.get("describe-name", DESCRIBE_UNSUPPORTED)
if DESCRIBE_UNSUPPORTED in archival_describe:
- warnings.warn("git archive did not support describe output")
+ warnings.warn("git archive did not support describe output", stacklevel=2)
else:
tag, number, node, _ = _git_parse_describe(archival_describe)
return meta(
@@ -442,7 +437,9 @@ def archival_to_version(
if node is None:
return None
elif "$FORMAT" in node.upper():
- warnings.warn("unprocessed git archival found (no export subst applied)")
+ warnings.warn(
+ "unprocessed git archival found (no export subst applied)", stacklevel=2
+ )
return None
else:
return meta("0.0", node=node, config=config)
diff --git a/src/setuptools_scm/hg.py b/vcs-versioning/src/vcs_versioning/_backends/_hg.py
similarity index 92%
rename from src/setuptools_scm/hg.py
rename to vcs-versioning/src/vcs_versioning/_backends/_hg.py
index 42320516..38d7a807 100644
--- a/src/setuptools_scm/hg.py
+++ b/vcs-versioning/src/vcs_versioning/_backends/_hg.py
@@ -3,33 +3,27 @@
import datetime
import logging
import os
-
from pathlib import Path
-from typing import TYPE_CHECKING
from typing import Any
-from . import Configuration
-from ._version_cls import Version
-from .integration import data_from_mime
-from .scm_workdir import Workdir
-from .scm_workdir import get_latest_file_mtime
-from .version import ScmVersion
-from .version import meta
-from .version import tag_to_version
-
-if TYPE_CHECKING:
- from . import _types as _t
-
-from ._run_cmd import CompletedProcess
-from ._run_cmd import require_command as _require_command
-from ._run_cmd import run as _run
+from .. import _types as _t
+from .._config import Configuration
+from .._integration import data_from_mime
+from .._run_cmd import CompletedProcess
+from .._run_cmd import require_command as _require_command
+from .._run_cmd import run as _run
+from .._scm_version import ScmVersion, meta, tag_to_version
+from .._version_cls import Version
+from ._scm_workdir import Workdir, get_latest_file_mtime
log = logging.getLogger(__name__)
def _get_hg_command() -> str:
- """Get the hg command from environment, allowing runtime configuration."""
- return os.environ.get("SETUPTOOLS_SCM_HG_COMMAND", "hg")
+ """Get the hg command from override context or environment."""
+ from ..overrides import get_hg_command
+
+ return get_hg_command()
def run_hg(args: list[str], cwd: _t.PathT, **kwargs: Any) -> CompletedProcess:
@@ -268,8 +262,8 @@ def parse(root: _t.PathT, config: Configuration) -> ScmVersion | None:
if line.startswith("default ="):
path = Path(line.split()[2])
if path.name.endswith(".git") or (path / ".git").exists():
- from .git import _git_parse_inner
- from .hg_git import GitWorkdirHgClient
+ from ._git import _git_parse_inner
+ from ._hg_git import GitWorkdirHgClient
wd_hggit = GitWorkdirHgClient.from_potential_worktree(root)
if wd_hggit:
diff --git a/src/setuptools_scm/hg_git.py b/vcs-versioning/src/vcs_versioning/_backends/_hg_git.py
similarity index 96%
rename from src/setuptools_scm/hg_git.py
rename to vcs-versioning/src/vcs_versioning/_backends/_hg_git.py
index 3e91b20f..a186dbaa 100644
--- a/src/setuptools_scm/hg_git.py
+++ b/vcs-versioning/src/vcs_versioning/_backends/_hg_git.py
@@ -2,17 +2,15 @@
import logging
import os
-
from contextlib import suppress
from datetime import date
from pathlib import Path
-from . import _types as _t
-from ._run_cmd import CompletedProcess as _CompletedProcess
-from .git import GitWorkdir
-from .hg import HgWorkdir
-from .hg import run_hg
-from .scm_workdir import get_latest_file_mtime
+from .. import _types as _t
+from .._run_cmd import CompletedProcess as _CompletedProcess
+from ._git import GitWorkdir
+from ._hg import HgWorkdir, run_hg
+from ._scm_workdir import get_latest_file_mtime
log = logging.getLogger(__name__)
diff --git a/src/setuptools_scm/scm_workdir.py b/vcs-versioning/src/vcs_versioning/_backends/_scm_workdir.py
similarity index 89%
rename from src/setuptools_scm/scm_workdir.py
rename to vcs-versioning/src/vcs_versioning/_backends/_scm_workdir.py
index b3ca7aa8..683adeb6 100644
--- a/src/setuptools_scm/scm_workdir.py
+++ b/vcs-versioning/src/vcs_versioning/_backends/_scm_workdir.py
@@ -1,15 +1,12 @@
from __future__ import annotations
import logging
-
from dataclasses import dataclass
-from datetime import date
-from datetime import datetime
-from datetime import timezone
+from datetime import date, datetime, timezone
from pathlib import Path
-from ._config import Configuration
-from .version import ScmVersion
+from .._config import Configuration
+from .._scm_version import ScmVersion
log = logging.getLogger(__name__)
diff --git a/vcs-versioning/src/vcs_versioning/_cli/__init__.py b/vcs-versioning/src/vcs_versioning/_cli/__init__.py
new file mode 100644
index 00000000..39bd5028
--- /dev/null
+++ b/vcs-versioning/src/vcs_versioning/_cli/__init__.py
@@ -0,0 +1,224 @@
+from __future__ import annotations
+
+import json
+import os
+import sys
+from collections.abc import Iterable
+from importlib.resources import files
+from pathlib import Path
+from typing import TypedDict
+
+from vcs_versioning._overrides import ConfigOverridesDict
+
+from .. import _discover as discover
+from .._config import Configuration
+from .._get_version_impl import _get_version
+from .._pyproject_reading import PyProjectData
+from ._args import CliNamespace, get_cli_parser
+
+
+class OutputData(TypedDict, ConfigOverridesDict, total=False):
+ version: str
+ files: list[str]
+ queries: list[str]
+
+
+def _get_version_for_cli(config: Configuration, opts: CliNamespace) -> str:
+ """Get version string for CLI output, handling special cases and exceptions."""
+ if opts.no_version:
+ return "0.0.0+no-version-was-requested.fake-version"
+
+ version = _get_version(
+ config, force_write_version_files=opts.force_write_version_files
+ )
+ if version is None:
+ raise SystemExit("ERROR: no version found for", opts)
+
+ if opts.strip_dev:
+ version = version.partition(".dev")[0]
+
+ return version
+
+
+def main(
+ args: list[str] | None = None, *, _given_pyproject_data: PyProjectData | None = None
+) -> int:
+ from ..overrides import GlobalOverrides
+
+ # Apply global overrides for the entire CLI execution
+ # Logging is automatically configured when entering the context
+ with GlobalOverrides.from_env("SETUPTOOLS_SCM"):
+ parser = get_cli_parser("python -m vcs_versioning")
+ opts = parser.parse_args(args, namespace=CliNamespace())
+ inferred_root: str = opts.root or "."
+
+ pyproject = opts.config or _find_pyproject(inferred_root)
+
+ try:
+ config = Configuration.from_file(
+ pyproject,
+ root=(os.path.abspath(opts.root) if opts.root is not None else None),
+ pyproject_data=_given_pyproject_data,
+ )
+ except (LookupError, FileNotFoundError) as ex:
+ # no pyproject.toml OR no [tool.setuptools_scm]
+ print(
+ f"Warning: could not use {os.path.relpath(pyproject)},"
+ " using default configuration.\n"
+ f" Reason: {ex}.",
+ file=sys.stderr,
+ )
+ config = Configuration(root=inferred_root)
+
+ version = _get_version_for_cli(config, opts)
+ return command(opts, version, config)
+
+
+# flake8: noqa: C901
+def command(opts: CliNamespace, version: str, config: Configuration) -> int:
+ data: OutputData = {}
+
+ if opts.command == "ls":
+ opts.query = ["files"]
+
+ if opts.command == "create-archival-file":
+ return _create_archival_file(opts, config)
+
+ if opts.query == []:
+ opts.no_version = True
+ sys.stderr.write("Available queries:\n\n")
+ opts.query = ["queries"]
+ data["queries"] = ["files", *config.__dataclass_fields__]
+
+ if opts.query is None:
+ opts.query = []
+
+ if not opts.no_version:
+ data["version"] = version
+
+ if "files" in opts.query:
+ from .._file_finders import find_files
+
+ data["files"] = find_files(config.root)
+
+ for q in opts.query:
+ if q in ["files", "queries", "version"]:
+ continue
+
+ try:
+ if q.startswith("_"):
+ raise AttributeError()
+ data[q] = getattr(config, q) # type: ignore[literal-required]
+ except AttributeError:
+ sys.stderr.write(f"Error: unknown query: '{q}'\n")
+ return 1
+
+ PRINT_FUNCTIONS[opts.format](data)
+
+ return 0
+
+
+def print_json(data: OutputData) -> None:
+ print(json.dumps(data, indent=2))
+
+
+def print_plain(data: OutputData) -> None:
+ version = data.pop("version", None)
+ if version:
+ print(version)
+ files = data.pop("files", [])
+ for file_ in files:
+ print(file_)
+ queries = data.pop("queries", [])
+ for query in queries:
+ print(query)
+ if data:
+ print("\n".join(map(str, data.values())))
+
+
+def print_key_value(data: OutputData) -> None:
+ for key, value in data.items():
+ if isinstance(value, str):
+ print(f"{key} = {value}")
+ else:
+ assert isinstance(value, Iterable)
+ str_value = "\n ".join(map(str, value))
+ print(f"{key} = {str_value}")
+
+
+PRINT_FUNCTIONS = {
+ "json": print_json,
+ "plain": print_plain,
+ "key-value": print_key_value,
+}
+
+
+def _find_pyproject(parent: str) -> str:
+ for directory in discover.walk_potential_roots(os.path.abspath(parent)):
+ pyproject = os.path.join(directory, "pyproject.toml")
+ if os.path.isfile(pyproject):
+ return pyproject
+
+ return os.path.abspath(
+ "pyproject.toml"
+ ) # use default name to trigger the default errors
+
+
+def _create_archival_file(opts: CliNamespace, config: Configuration) -> int:
+ """Create .git_archival.txt file with appropriate content."""
+ archival_path = Path(config.root, ".git_archival.txt")
+
+ # Check if file exists and force flag
+ if archival_path.exists() and not opts.force:
+ print(
+ f"Error: {archival_path} already exists. Use --force to overwrite.",
+ file=sys.stderr,
+ )
+ return 1
+
+ # archival_template is guaranteed to be set by required mutually exclusive group
+ assert opts.archival_template is not None
+
+ # Load template content from package resources
+ content = files(__package__).joinpath(opts.archival_template).read_text("utf-8")
+
+ # Print appropriate message based on template
+ if opts.archival_template == "git_archival_stable.txt":
+ print("Creating stable .git_archival.txt (recommended for releases)")
+ elif opts.archival_template == "git_archival_full.txt":
+ print("Creating full .git_archival.txt with branch information")
+ print("WARNING: This can cause archive checksums to be unstable!")
+
+ try:
+ archival_path.write_text(content, encoding="utf-8")
+ print(f"Created: {archival_path}")
+
+ gitattributes_path = Path(config.root, ".gitattributes")
+ needs_gitattributes = True
+
+ if gitattributes_path.exists():
+ # TODO: more nuanced check later
+ gitattributes_content = gitattributes_path.read_text("utf-8")
+ if (
+ ".git_archival.txt" in gitattributes_content
+ and "export-subst" in gitattributes_content
+ ):
+ needs_gitattributes = False
+
+ if needs_gitattributes:
+ print("\nNext steps:")
+ print("1. Add this line to .gitattributes:")
+ print(" .git_archival.txt export-subst")
+ print("2. Commit both files:")
+ print(" git add .git_archival.txt .gitattributes")
+ print(" git commit -m 'add git archive support'")
+ else:
+ print("\nNext step:")
+ print("Commit the archival file:")
+ print(" git add .git_archival.txt")
+ print(" git commit -m 'update git archival file'")
+
+ return 0
+ except OSError as e:
+ print(f"Error: Could not create {archival_path}: {e}", file=sys.stderr)
+ return 1
diff --git a/vcs-versioning/src/vcs_versioning/_cli/_args.py b/vcs-versioning/src/vcs_versioning/_cli/_args.py
new file mode 100644
index 00000000..086e996d
--- /dev/null
+++ b/vcs-versioning/src/vcs_versioning/_cli/_args.py
@@ -0,0 +1,107 @@
+from __future__ import annotations
+
+import argparse
+
+
+class CliNamespace(argparse.Namespace):
+ """Typed namespace for CLI arguments."""
+
+ # Main arguments
+ root: str | None
+ config: str | None
+ strip_dev: bool
+ no_version: bool
+ format: str
+ query: list[str] | None
+ force_write_version_files: bool
+ command: str | None
+
+ # create-archival-file subcommand arguments
+ archival_template: str | None
+ force: bool
+
+
+def get_cli_parser(prog: str) -> argparse.ArgumentParser:
+ desc = "Print project version according to SCM metadata"
+ parser = argparse.ArgumentParser(prog, description=desc)
+ # By default, help for `--help` starts with lower case, so we keep the pattern:
+ parser.add_argument(
+ "-r",
+ "--root",
+ default=None,
+ help='directory managed by the SCM, default: inferred from config file, or "."',
+ )
+ parser.add_argument(
+ "-c",
+ "--config",
+ default=None,
+ metavar="PATH",
+ help="path to 'pyproject.toml' with setuptools-scm config, "
+ "default: looked up in the current or parent directories",
+ )
+ parser.add_argument(
+ "--strip-dev",
+ action="store_true",
+ help="remove the dev/local parts of the version before printing the version",
+ )
+ parser.add_argument(
+ "-N",
+ "--no-version",
+ action="store_true",
+ help="do not include package version in the output",
+ )
+ output_formats = ["json", "plain", "key-value"]
+ parser.add_argument(
+ "-f",
+ "--format",
+ type=str.casefold,
+ default="plain",
+ help="specify output format",
+ choices=output_formats,
+ )
+ parser.add_argument(
+ "-q",
+ "--query",
+ type=str.casefold,
+ nargs="*",
+ help="display setuptools-scm settings according to query, "
+ "e.g. dist_name, do not supply an argument in order to "
+ "print a list of valid queries.",
+ )
+ parser.add_argument(
+ "--force-write-version-files",
+ action="store_true",
+ help="trigger to write the content of the version files\n"
+ "its recommended to use normal/editable installation instead)",
+ )
+ sub = parser.add_subparsers(title="extra commands", dest="command", metavar="")
+ # We avoid `metavar` to prevent printing repetitive information
+ desc = "List information about the package, e.g. included files"
+ sub.add_parser("ls", help=desc[0].lower() + desc[1:], description=desc)
+
+ # Add create-archival-file subcommand
+ archival_desc = "Create .git_archival.txt file for git archive support"
+ archival_parser = sub.add_parser(
+ "create-archival-file",
+ help=archival_desc[0].lower() + archival_desc[1:],
+ description=archival_desc,
+ )
+ archival_group = archival_parser.add_mutually_exclusive_group(required=True)
+ archival_group.add_argument(
+ "--stable",
+ action="store_const",
+ const="git_archival_stable.txt",
+ dest="archival_template",
+ help="create stable archival file (recommended, no branch names)",
+ )
+ archival_group.add_argument(
+ "--full",
+ action="store_const",
+ const="git_archival_full.txt",
+ dest="archival_template",
+ help="create full archival file with branch information (can cause instability)",
+ )
+ archival_parser.add_argument(
+ "--force", action="store_true", help="overwrite existing .git_archival.txt file"
+ )
+ return parser
diff --git a/vcs-versioning/src/vcs_versioning/_cli/git_archival_full.txt b/vcs-versioning/src/vcs_versioning/_cli/git_archival_full.txt
new file mode 100644
index 00000000..1ef6ba5c
--- /dev/null
+++ b/vcs-versioning/src/vcs_versioning/_cli/git_archival_full.txt
@@ -0,0 +1,7 @@
+# WARNING: Including ref-names can make archive checksums unstable
+# after commits are added post-release. Use only if describe-name is insufficient.
+node: $Format:%H$
+node-date: $Format:%cI$
+describe-name: $Format:%(describe:tags=true,match=*[0-9]*)$
+ref-names: $Format:%D$
+
diff --git a/vcs-versioning/src/vcs_versioning/_cli/git_archival_stable.txt b/vcs-versioning/src/vcs_versioning/_cli/git_archival_stable.txt
new file mode 100644
index 00000000..2b181ff6
--- /dev/null
+++ b/vcs-versioning/src/vcs_versioning/_cli/git_archival_stable.txt
@@ -0,0 +1,4 @@
+node: $Format:%H$
+node-date: $Format:%cI$
+describe-name: $Format:%(describe:tags=true,match=*[0-9]*)$
+
diff --git a/src/setuptools_scm/_compat.py b/vcs-versioning/src/vcs_versioning/_compat.py
similarity index 75%
rename from src/setuptools_scm/_compat.py
rename to vcs-versioning/src/vcs_versioning/_compat.py
index 4e9e301f..25a071a6 100644
--- a/src/setuptools_scm/_compat.py
+++ b/vcs-versioning/src/vcs_versioning/_compat.py
@@ -2,6 +2,12 @@
from __future__ import annotations
+import os
+from typing import TypeAlias
+
+# Path type for accepting both strings and PathLike objects
+PathT: TypeAlias = os.PathLike[str] | str
+
def normalize_path_for_assertion(path: str) -> str:
"""Normalize path separators for cross-platform assertions.
@@ -63,3 +69,23 @@ def assert_path_endswith(
def compute_path_prefix(full_path: str, suffix_path: str) -> str:
"""Legacy alias - use strip_path_suffix instead."""
return strip_path_suffix(full_path, suffix_path)
+
+
+def norm_real(path: PathT) -> str:
+ """Normalize and resolve a path (combining normcase and realpath).
+
+ This combines os.path.normcase() and os.path.realpath() to produce
+ a canonical path string that is normalized for the platform and has
+ all symbolic links resolved.
+
+ Args:
+ path: The path to normalize and resolve
+
+ Returns:
+ The normalized, resolved absolute path
+
+ Examples:
+ >>> norm_real("/path/to/../to/file.txt") # doctest: +SKIP
+ '/path/to/file.txt'
+ """
+ return os.path.normcase(os.path.realpath(path))
diff --git a/src/setuptools_scm/_config.py b/vcs-versioning/src/vcs_versioning/_config.py
similarity index 90%
rename from src/setuptools_scm/_config.py
rename to vcs-versioning/src/vcs_versioning/_config.py
index 49fac2a4..e2db2216 100644
--- a/src/setuptools_scm/_config.py
+++ b/vcs-versioning/src/vcs_versioning/_config.py
@@ -3,32 +3,25 @@
from __future__ import annotations
import dataclasses
+import logging
import os
import re
import warnings
-
from pathlib import Path
-from typing import TYPE_CHECKING
-from typing import Any
-from typing import Pattern
-from typing import Protocol
+from re import Pattern
+from typing import TYPE_CHECKING, Any, Protocol
if TYPE_CHECKING:
- from . import git
+ from ._backends import _git
-from . import _log
from . import _types as _t
-from ._integration.pyproject_reading import PyProjectData
-from ._integration.pyproject_reading import (
- get_args_for_pyproject as _get_args_for_pyproject,
-)
-from ._integration.pyproject_reading import read_pyproject as _read_pyproject
from ._overrides import read_toml_overrides
+from ._pyproject_reading import PyProjectData, get_args_for_pyproject, read_pyproject
from ._version_cls import Version as _Version
from ._version_cls import _validate_version_cls
-from ._version_cls import _VersionT
+from ._version_cls import _Version as _VersionAlias
-log = _log.log.getChild("config")
+log = logging.getLogger(__name__)
def _is_called_from_dataclasses() -> bool:
@@ -107,11 +100,11 @@ def _check_tag_regex(value: str | Pattern[str] | None) -> Pattern[str]:
return regex
-def _get_default_git_pre_parse() -> git.GitPreParse:
+def _get_default_git_pre_parse() -> _git.GitPreParse:
"""Get the default git pre_parse enum value"""
- from . import git
+ from ._backends import _git
- return git.GitPreParse.WARN_ON_SHALLOW
+ return _git.GitPreParse.WARN_ON_SHALLOW
class ParseFunction(Protocol):
@@ -129,13 +122,15 @@ def _check_absolute_root(root: _t.PathT, relative_to: _t.PathT | None) -> str:
and not os.path.commonpath([root, relative_to]) == root
):
warnings.warn(
- f"absolute root path '{root}' overrides relative_to '{relative_to}'"
+ f"absolute root path '{root}' overrides relative_to '{relative_to}'",
+ stacklevel=2,
)
if os.path.isdir(relative_to):
warnings.warn(
"relative_to is expected to be a file,"
f" its the directory {relative_to}\n"
- "assuming the parent directory was passed"
+ "assuming the parent directory was passed",
+ stacklevel=2,
)
log.debug("dir %s", relative_to)
root = os.path.join(relative_to, root)
@@ -149,7 +144,7 @@ def _check_absolute_root(root: _t.PathT, relative_to: _t.PathT | None) -> str:
class GitConfiguration:
"""Git-specific configuration options"""
- pre_parse: git.GitPreParse = dataclasses.field(
+ pre_parse: _git.GitPreParse = dataclasses.field(
default_factory=lambda: _get_default_git_pre_parse()
)
describe_command: _t.CMD_TYPE | None = None
@@ -161,12 +156,12 @@ def from_data(cls, data: dict[str, Any]) -> GitConfiguration:
# Convert string pre_parse values to enum instances
if "pre_parse" in git_data and isinstance(git_data["pre_parse"], str):
- from . import git
+ from ._backends import _git
try:
- git_data["pre_parse"] = git.GitPreParse(git_data["pre_parse"])
+ git_data["pre_parse"] = _git.GitPreParse(git_data["pre_parse"])
except ValueError as e:
- valid_options = [option.value for option in git.GitPreParse]
+ valid_options = [option.value for option in _git.GitPreParse]
raise ValueError(
f"Invalid git pre_parse function '{git_data['pre_parse']}'. "
f"Valid options are: {', '.join(valid_options)}"
@@ -215,7 +210,7 @@ class Configuration:
)
dist_name: str | None = None
- version_cls: type[_VersionT] = _Version
+ version_cls: type[_VersionAlias] = _Version
search_parent_directories: bool = False
parent: _t.PathT | None = None
@@ -285,8 +280,8 @@ def from_file(
"""
if pyproject_data is None:
- pyproject_data = _read_pyproject(Path(name))
- args = _get_args_for_pyproject(pyproject_data, dist_name, kwargs)
+ pyproject_data = read_pyproject(Path(name))
+ args = get_args_for_pyproject(pyproject_data, dist_name, kwargs)
args.update(read_toml_overrides(args["dist_name"]))
relative_to = args.pop("relative_to", name)
diff --git a/src/setuptools_scm/discover.py b/vcs-versioning/src/vcs_versioning/_discover.py
similarity index 88%
rename from src/setuptools_scm/discover.py
rename to vcs-versioning/src/vcs_versioning/_discover.py
index e8208ca4..91e6a368 100644
--- a/src/setuptools_scm/discover.py
+++ b/vcs-versioning/src/vcs_versioning/_discover.py
@@ -1,22 +1,16 @@
from __future__ import annotations
+import logging
import os
-
+from collections.abc import Iterable, Iterator
+from importlib.metadata import EntryPoint
from pathlib import Path
-from typing import TYPE_CHECKING
-from typing import Iterable
-from typing import Iterator
from . import _entrypoints
-from . import _log
from . import _types as _t
from ._config import Configuration
-if TYPE_CHECKING:
- from ._entrypoints import im
-
-
-log = _log.log.getChild("discover")
+log = logging.getLogger(__name__)
def walk_potential_roots(root: _t.PathT, search_parents: bool = True) -> Iterator[Path]:
@@ -53,7 +47,7 @@ def match_entrypoint(root: _t.PathT, name: str) -> bool:
def iter_matching_entrypoints(
root: _t.PathT, entrypoint: str, config: Configuration
-) -> Iterable[im.EntryPoint]:
+) -> Iterable[EntryPoint]:
"""
Consider different entry-points in ``root`` and optionally its parents.
:param root: File path.
diff --git a/src/setuptools_scm/_integration/dump_version.py b/vcs-versioning/src/vcs_versioning/_dump_version.py
similarity index 59%
rename from src/setuptools_scm/_integration/dump_version.py
rename to vcs-versioning/src/vcs_versioning/_dump_version.py
index 06081c9f..3b5c2b2b 100644
--- a/src/setuptools_scm/_integration/dump_version.py
+++ b/vcs-versioning/src/vcs_versioning/_dump_version.py
@@ -1,21 +1,26 @@
+"""Core functionality for writing version information to files."""
+
from __future__ import annotations
+import logging
import warnings
-
from pathlib import Path
+from typing import TYPE_CHECKING
-from .. import _types as _t
-from .._log import log as parent_log
-from .._version_cls import _version_as_tuple
-from ..version import ScmVersion
+from ._version_cls import _version_as_tuple
-log = parent_log.getChild("dump_version")
+if TYPE_CHECKING:
+ from . import _types as _t
+ from ._scm_version import ScmVersion
+log = logging.getLogger(__name__)
-TEMPLATES = {
+
+DEFAULT_TEMPLATES = {
".py": """\
-# file generated by setuptools-scm
+# file generated by vcs-versioning
# don't change, don't track in version control
+from __future__ import annotations
__all__ = [
"__version__",
@@ -26,23 +31,12 @@
"commit_id",
]
-TYPE_CHECKING = False
-if TYPE_CHECKING:
- from typing import Tuple
- from typing import Union
-
- VERSION_TUPLE = Tuple[Union[int, str], ...]
- COMMIT_ID = Union[str, None]
-else:
- VERSION_TUPLE = object
- COMMIT_ID = object
-
version: str
__version__: str
-__version_tuple__: VERSION_TUPLE
-version_tuple: VERSION_TUPLE
-commit_id: COMMIT_ID
-__commit_id__: COMMIT_ID
+__version_tuple__: tuple[int | str, ...]
+version_tuple: tuple[int | str, ...]
+commit_id: str | None
+__commit_id__: str | None
__version__ = version = {version!r}
__version_tuple__ = version_tuple = {version_tuple!r}
@@ -53,38 +47,34 @@
}
-def dump_version(
- root: _t.PathT,
- version: str,
- write_to: _t.PathT,
- template: str | None = None,
- scm_version: ScmVersion | None = None,
-) -> None:
- assert isinstance(version, str)
- root = Path(root)
- write_to = Path(write_to)
- if write_to.is_absolute():
- # trigger warning on escape
- write_to.relative_to(root)
- warnings.warn(
- f"{write_to=!s} is a absolute path,"
- " please switch to using a relative version file",
- DeprecationWarning,
- )
- target = write_to
- else:
- target = Path(root).joinpath(write_to)
- write_version_to_path(
- target, template=template, version=version, scm_version=scm_version
- )
+class DummyScmVersion:
+ """Placeholder for when no ScmVersion is available."""
+
+ @property
+ def short_node(self) -> str | None:
+ return None
def _validate_template(target: Path, template: str | None) -> str:
+ """Validate and return the template to use for writing the version file.
+
+ Args:
+ target: The target file path
+ template: User-provided template or None to use default
+
+ Returns:
+ The template string to use
+
+ Raises:
+ ValueError: If no suitable template is found
+ """
if template == "":
- warnings.warn(f"{template=} looks like a error, using default instead")
+ warnings.warn(
+ f"{template=} looks like a error, using default instead", stacklevel=2
+ )
template = None
if template is None:
- template = TEMPLATES.get(target.suffix)
+ template = DEFAULT_TEMPLATES.get(target.suffix)
if template is None:
raise ValueError(
@@ -95,18 +85,20 @@ def _validate_template(target: Path, template: str | None) -> str:
return template
-class DummyScmVersion:
- @property
- def short_node(self) -> str | None:
- return None
-
-
def write_version_to_path(
target: Path,
template: str | None,
version: str,
scm_version: ScmVersion | None = None,
) -> None:
+ """Write version information to a file using a template.
+
+ Args:
+ target: The target file path to write to
+ template: Template string or None to use default based on file extension
+ version: The version string to write
+ scm_version: Optional ScmVersion object for additional metadata
+ """
final_template = _validate_template(target, template)
log.debug("dump %s into %s", version, target)
version_tuple = _version_as_tuple(version)
@@ -126,3 +118,39 @@ def write_version_to_path(
)
target.write_text(content, encoding="utf-8")
+
+
+def dump_version(
+ root: _t.PathT,
+ version: str,
+ write_to: _t.PathT,
+ template: str | None = None,
+ scm_version: ScmVersion | None = None,
+) -> None:
+ """Write version information to a file relative to root.
+
+ Args:
+ root: The root directory (project root)
+ version: The version string to write
+ write_to: The target file path (relative to root or absolute)
+ template: Template string or None to use default
+ scm_version: Optional ScmVersion object for additional metadata
+ """
+ assert isinstance(version, str)
+ root = Path(root)
+ write_to = Path(write_to)
+ if write_to.is_absolute():
+ # trigger warning on escape
+ write_to.relative_to(root)
+ warnings.warn(
+ f"{write_to=!s} is a absolute path,"
+ " please switch to using a relative version file",
+ DeprecationWarning,
+ stacklevel=2,
+ )
+ target = write_to
+ else:
+ target = Path(root).joinpath(write_to)
+ write_version_to_path(
+ target, template=template, version=version, scm_version=scm_version
+ )
diff --git a/src/setuptools_scm/_entrypoints.py b/vcs-versioning/src/vcs_versioning/_entrypoints.py
similarity index 66%
rename from src/setuptools_scm/_entrypoints.py
rename to vcs-versioning/src/vcs_versioning/_entrypoints.py
index 74a18a7d..7a6c49e0 100644
--- a/src/setuptools_scm/_entrypoints.py
+++ b/vcs-versioning/src/vcs_versioning/_entrypoints.py
@@ -1,15 +1,10 @@
from __future__ import annotations
-import sys
-
-from typing import TYPE_CHECKING
-from typing import Any
-from typing import Callable
-from typing import Iterator
-from typing import cast
-
-from . import _log
-from . import version
+import logging
+from collections.abc import Callable, Iterator
+from importlib import metadata as im
+from importlib.metadata import entry_points
+from typing import TYPE_CHECKING, Any, cast
__all__ = [
"entry_points",
@@ -17,43 +12,21 @@
]
if TYPE_CHECKING:
from . import _types as _t
- from ._config import Configuration
- from ._config import ParseFunction
-
-from importlib import metadata as im
-
-log = _log.log.getChild("entrypoints")
-
-
-if sys.version_info[:2] < (3, 10):
-
- def entry_points(*, group: str, name: str | None = None) -> list[im.EntryPoint]:
- # Python 3.9: entry_points() returns dict, need to handle filtering manually
-
- eps = im.entry_points() # Returns dict
-
- group_eps = eps.get(group, [])
- if name is not None:
- return [ep for ep in group_eps if ep.name == name]
- return group_eps
-else:
+ from ._config import Configuration, ParseFunction
+ from ._scm_version import ScmVersion
- def entry_points(*, group: str, name: str | None = None) -> im.EntryPoints:
- kw = {"group": group}
- if name is not None:
- kw["name"] = name
- return im.entry_points(**kw)
+log = logging.getLogger(__name__)
def version_from_entrypoint(
config: Configuration, *, entrypoint: str, root: _t.PathT
-) -> version.ScmVersion | None:
- from .discover import iter_matching_entrypoints
+) -> ScmVersion | None:
+ from ._discover import iter_matching_entrypoints
log.debug("version_from_ep %s in %s", entrypoint, root)
for ep in iter_matching_entrypoints(root, entrypoint, config):
fn: ParseFunction = ep.load()
- maybe_version: version.ScmVersion | None = fn(root, config=config)
+ maybe_version: ScmVersion | None = fn(root, config=config)
log.debug("%s found %r", ep, maybe_version)
if maybe_version is not None:
return maybe_version
@@ -82,7 +55,7 @@ def _iter_version_schemes(
entrypoint: str,
scheme_value: _t.VERSION_SCHEMES,
_memo: set[object] | None = None,
-) -> Iterator[Callable[[version.ScmVersion], str]]:
+) -> Iterator[Callable[[ScmVersion], str]]:
if _memo is None:
_memo = set()
if isinstance(scheme_value, str):
@@ -92,7 +65,7 @@ def _iter_version_schemes(
or _get_from_object_reference_str(scheme_value, entrypoint),
)
- if isinstance(scheme_value, (list, tuple)):
+ if isinstance(scheme_value, list | tuple):
for variant in scheme_value:
if variant not in _memo:
_memo.add(variant)
@@ -102,7 +75,7 @@ def _iter_version_schemes(
def _call_version_scheme(
- version: version.ScmVersion,
+ version: ScmVersion,
entrypoint: str,
given_value: _t.VERSION_SCHEMES,
default: str | None = None,
diff --git a/src/setuptools_scm/fallbacks.py b/vcs-versioning/src/vcs_versioning/_fallbacks.py
similarity index 88%
rename from src/setuptools_scm/fallbacks.py
rename to vcs-versioning/src/vcs_versioning/_fallbacks.py
index 45a75351..b2b89450 100644
--- a/src/setuptools_scm/fallbacks.py
+++ b/vcs-versioning/src/vcs_versioning/_fallbacks.py
@@ -2,17 +2,14 @@
import logging
import os
-
from pathlib import Path
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from . import _types as _t
-from . import Configuration
-from .integration import data_from_mime
-from .version import ScmVersion
-from .version import meta
-from .version import tag_to_version
+from ._config import Configuration
+from ._integration import data_from_mime
+from ._scm_version import ScmVersion, meta, tag_to_version
log = logging.getLogger(__name__)
diff --git a/src/setuptools_scm/_file_finders/__init__.py b/vcs-versioning/src/vcs_versioning/_file_finders/__init__.py
similarity index 76%
rename from src/setuptools_scm/_file_finders/__init__.py
rename to vcs-versioning/src/vcs_versioning/_file_finders/__init__.py
index e19afc81..6d653c11 100644
--- a/src/setuptools_scm/_file_finders/__init__.py
+++ b/vcs-versioning/src/vcs_versioning/_file_finders/__init__.py
@@ -1,25 +1,15 @@
from __future__ import annotations
+import logging
import os
+from collections.abc import Callable
+from typing import TypeGuard
-from typing import TYPE_CHECKING
-from typing import Callable
-
-from .. import _log
from .. import _types as _t
+from .._compat import norm_real
from .._entrypoints import entry_points
-from .pathtools import norm_real
-
-if TYPE_CHECKING:
- import sys
-
- if sys.version_info >= (3, 10):
- from typing import TypeGuard
- else:
- from typing_extensions import TypeGuard
-
-log = _log.log.getChild("file_finder")
+log = logging.getLogger("vcs_versioning.file_finder")
def scm_find_files(
@@ -28,7 +18,7 @@ def scm_find_files(
scm_dirs: set[str],
force_all_files: bool = False,
) -> list[str]:
- """ setuptools compatible file finder that follows symlinks
+ """Core file discovery logic that follows symlinks
- path: the root directory from which to search
- scm_files: set of scm controlled files and symlinks
@@ -39,9 +29,6 @@ def scm_find_files(
scm_files and scm_dirs must be absolute with symlinks resolved (realpath),
with normalized case (normcase)
-
- Spec here: https://setuptools.pypa.io/en/latest/userguide/extension.html#\
- adding-support-for-revision-control-systems
"""
realpath = norm_real(path)
seen: set[str] = set()
@@ -86,21 +73,33 @@ def _link_not_in_scm(n: str, realdirpath: str = realdirpath) -> bool:
def is_toplevel_acceptable(toplevel: str | None) -> TypeGuard[str]:
- """ """
+ """Check if a VCS toplevel directory is acceptable (not in ignore list)"""
+ import os
+
if toplevel is None:
return False
- ignored: list[str] = os.environ.get("SETUPTOOLS_SCM_IGNORE_VCS_ROOTS", "").split(
- os.pathsep
+ # Use the env_reader from the active GlobalOverrides context
+ # This ensures we respect the current environment configuration
+ from ..overrides import get_active_overrides
+
+ overrides = get_active_overrides()
+ ignored_raw = overrides.env_reader.read(
+ "IGNORE_VCS_ROOTS", split=os.pathsep, default=[]
)
- ignored = [os.path.normcase(p) for p in ignored]
+ ignored = [os.path.normcase(p) for p in ignored_raw]
- log.debug("toplevel: %r\n ignored %s", toplevel, ignored)
+ log.debug(
+ "toplevel: %r\n ignored %s",
+ toplevel,
+ ignored,
+ )
return toplevel not in ignored
def find_files(path: _t.PathT = "") -> list[str]:
+ """Discover files using registered file finder entry points"""
eps = [
*entry_points(group="setuptools_scm.files_command"),
*entry_points(group="setuptools_scm.files_command_fallback"),
@@ -111,3 +110,6 @@ def find_files(path: _t.PathT = "") -> list[str]:
if res:
return res
return []
+
+
+__all__ = ["scm_find_files", "is_toplevel_acceptable", "find_files"]
diff --git a/src/setuptools_scm/_file_finders/git.py b/vcs-versioning/src/vcs_versioning/_file_finders/_git.py
similarity index 92%
rename from src/setuptools_scm/_file_finders/git.py
rename to vcs-versioning/src/vcs_versioning/_file_finders/_git.py
index 4379c21a..151e6d90 100644
--- a/src/setuptools_scm/_file_finders/git.py
+++ b/vcs-versioning/src/vcs_versioning/_file_finders/_git.py
@@ -4,15 +4,13 @@
import os
import subprocess
import tarfile
-
from typing import IO
from .. import _types as _t
+from .._compat import norm_real, strip_path_suffix
+from .._integration import data_from_mime
from .._run_cmd import run as _run
-from ..integration import data_from_mime
-from . import is_toplevel_acceptable
-from . import scm_find_files
-from .pathtools import norm_real
+from . import is_toplevel_acceptable, scm_find_files
log = logging.getLogger(__name__)
@@ -39,8 +37,6 @@ def _git_toplevel(path: str) -> str | None:
# ``cwd`` is absolute path to current working directory.
# the below method removes the length of ``out`` from
# ``cwd``, which gives the git toplevel
- from .._compat import strip_path_suffix
-
out = strip_path_suffix(cwd, out, f"cwd={cwd!r}\nout={out!r}")
log.debug("find files toplevel %s", out)
return norm_real(out)
@@ -97,6 +93,7 @@ def _git_ls_files_and_dirs(toplevel: str) -> tuple[set[str], set[str]]:
def git_find_files(path: _t.PathT = "") -> list[str]:
+ """Find files tracked in a Git repository"""
toplevel = _git_toplevel(os.fspath(path))
if not is_toplevel_acceptable(toplevel):
return []
@@ -108,6 +105,7 @@ def git_find_files(path: _t.PathT = "") -> list[str]:
def git_archive_find_files(path: _t.PathT = "") -> list[str]:
+ """Find files in a Git archive (all files, since archive already filtered)"""
# This function assumes that ``path`` is obtained from a git archive
# and therefore all the files that should be ignored were already removed.
archival = os.path.join(path, ".git_archival.txt")
@@ -122,3 +120,6 @@ def git_archive_find_files(path: _t.PathT = "") -> list[str]:
log.warning("git archive detected - fallback to listing all files")
return scm_find_files(path, set(), set(), force_all_files=True)
+
+
+__all__ = ["git_find_files", "git_archive_find_files"]
diff --git a/src/setuptools_scm/_file_finders/hg.py b/vcs-versioning/src/vcs_versioning/_file_finders/_hg.py
similarity index 84%
rename from src/setuptools_scm/_file_finders/hg.py
rename to vcs-versioning/src/vcs_versioning/_file_finders/_hg.py
index 182429c3..43e50261 100644
--- a/src/setuptools_scm/_file_finders/hg.py
+++ b/vcs-versioning/src/vcs_versioning/_file_finders/_hg.py
@@ -5,11 +5,10 @@
import subprocess
from .. import _types as _t
-from .._file_finders import is_toplevel_acceptable
-from .._file_finders import scm_find_files
-from ..hg import run_hg
-from ..integration import data_from_mime
-from .pathtools import norm_real
+from .._backends._hg import run_hg
+from .._compat import norm_real
+from .._integration import data_from_mime
+from . import is_toplevel_acceptable, scm_find_files
log = logging.getLogger(__name__)
@@ -47,6 +46,7 @@ def _hg_ls_files_and_dirs(toplevel: str) -> tuple[set[str], set[str]]:
def hg_find_files(path: str = "") -> list[str]:
+ """Find files tracked in a Mercurial repository"""
toplevel = _hg_toplevel(path)
if not is_toplevel_acceptable(toplevel):
return []
@@ -56,6 +56,7 @@ def hg_find_files(path: str = "") -> list[str]:
def hg_archive_find_files(path: _t.PathT = "") -> list[str]:
+ """Find files in a Mercurial archive (all files, since archive already filtered)"""
# This function assumes that ``path`` is obtained from a mercurial archive
# and therefore all the files that should be ignored were already removed.
archival = os.path.join(path, ".hg_archival.txt")
@@ -70,3 +71,6 @@ def hg_archive_find_files(path: _t.PathT = "") -> list[str]:
log.warning("hg archive detected - fallback to listing all files")
return scm_find_files(path, set(), set(), force_all_files=True)
+
+
+__all__ = ["hg_find_files", "hg_archive_find_files"]
diff --git a/src/setuptools_scm/_get_version_impl.py b/vcs-versioning/src/vcs_versioning/_get_version_impl.py
similarity index 84%
rename from src/setuptools_scm/_get_version_impl.py
rename to vcs-versioning/src/vcs_versioning/_get_version_impl.py
index 31bc9c39..44ef3582 100644
--- a/src/setuptools_scm/_get_version_impl.py
+++ b/vcs-versioning/src/vcs_versioning/_get_version_impl.py
@@ -4,27 +4,23 @@
import logging
import re
import warnings
-
from pathlib import Path
-from typing import Any
-from typing import NoReturn
-from typing import Pattern
+from re import Pattern
+from typing import Any, NoReturn
-from . import _config
-from . import _entrypoints
-from . import _run_cmd
+from . import _config, _entrypoints, _run_cmd
from . import _types as _t
from ._config import Configuration
from ._overrides import _read_pretended_version_for
+from ._scm_version import ScmVersion
from ._version_cls import _validate_version_cls
-from .version import ScmVersion
-from .version import format_version as _format_version
+from ._version_schemes import format_version as _format_version
EMPTY_TAG_REGEX_DEPRECATION = DeprecationWarning(
"empty regex for tag regex is invalid, using default"
)
-_log = logging.getLogger(__name__)
+log = logging.getLogger(__name__)
def parse_scm_version(config: Configuration) -> ScmVersion | None:
@@ -44,7 +40,7 @@ def parse_scm_version(config: Configuration) -> ScmVersion | None:
root=config.absolute_root,
)
except _run_cmd.CommandNotFoundError as e:
- _log.exception("command %s not found while parsing the scm, using fallbacks", e)
+ log.exception("command %s not found while parsing the scm, using fallbacks", e)
return None
@@ -74,7 +70,7 @@ def write_version_files(
config: Configuration, version: str, scm_version: ScmVersion
) -> None:
if config.write_to is not None:
- from ._integration.dump_version import dump_version
+ from ._dump_version import dump_version
dump_version(
root=config.root,
@@ -84,7 +80,7 @@ def write_version_files(
template=config.write_to_template,
)
if config.version_file:
- from ._integration.dump_version import write_version_to_path
+ from ._dump_version import write_version_to_path
version_file = Path(config.version_file)
assert not version_file.is_absolute(), f"{version_file=}"
@@ -112,6 +108,7 @@ def _get_version(
"force_write_version_files ought to be set,"
" presuming the legacy True value",
DeprecationWarning,
+ stacklevel=2,
)
if force_write_version_files:
@@ -130,7 +127,7 @@ def _find_scm_in_parents(config: Configuration) -> Path | None:
searching_config = dataclasses.replace(config, search_parent_directories=True)
- from .discover import iter_matching_entrypoints
+ from ._discover import iter_matching_entrypoints
for _ep in iter_matching_entrypoints(
config.absolute_root, "setuptools_scm.parse_scm", searching_config
@@ -154,18 +151,32 @@ def _version_missing(config: Configuration) -> NoReturn:
if scm_parent is not None:
# Found an SCM repository in a parent directory
+ # Get tool-specific names for error messages
+ from .overrides import get_active_overrides
+
+ overrides = get_active_overrides()
+ tool = overrides.tool
+
+ # Generate appropriate examples based on tool
+ if tool == "SETUPTOOLS_SCM":
+ api_example = "setuptools_scm.get_version(relative_to=__file__)"
+ tool_section = "[tool.setuptools_scm]"
+ else:
+ api_example = "vcs_versioning.get_version(relative_to=__file__)"
+ tool_section = "[tool.vcs-versioning]"
+
error_msg = (
base_error
+ f"However, a repository was found in a parent directory: {scm_parent}\n\n"
f"To fix this, you have a few options:\n\n"
- f"1. Use the 'relative_to' parameter to specify the file that setuptools-scm should use as reference:\n"
- f" setuptools_scm.get_version(relative_to=__file__)\n\n"
+ f"1. Use the 'relative_to' parameter to specify the file as reference:\n"
+ f" {api_example}\n\n"
f"2. Enable parent directory search in your configuration:\n"
- f" [tool.setuptools_scm]\n"
+ f" {tool_section}\n"
f" search_parent_directories = true\n\n"
f"3. Change your working directory to the repository root: {scm_parent}\n\n"
f"4. Set the root explicitly in your configuration:\n"
- f" [tool.setuptools_scm]\n"
+ f" {tool_section}\n"
f' root = "{scm_parent}"\n\n'
"For more information, see: https://setuptools-scm.readthedocs.io/en/latest/config/"
)
@@ -240,9 +251,14 @@ def get_version(
def parse_tag_regex(tag_regex: str | Pattern[str]) -> Pattern[str]:
+ """Pre-validate and convert tag_regex to Pattern before Configuration.
+
+ This ensures get_version() emits the deprecation warning for empty strings
+ before Configuration.__post_init__ runs.
+ """
if isinstance(tag_regex, str):
if tag_regex == "":
- warnings.warn(EMPTY_TAG_REGEX_DEPRECATION)
+ warnings.warn(EMPTY_TAG_REGEX_DEPRECATION, stacklevel=3)
return _config.DEFAULT_TAG_REGEX
else:
return re.compile(tag_regex)
diff --git a/src/setuptools_scm/integration.py b/vcs-versioning/src/vcs_versioning/_integration.py
similarity index 99%
rename from src/setuptools_scm/integration.py
rename to vcs-versioning/src/vcs_versioning/_integration.py
index b15d74a6..238deab2 100644
--- a/src/setuptools_scm/integration.py
+++ b/vcs-versioning/src/vcs_versioning/_integration.py
@@ -2,7 +2,6 @@
import logging
import textwrap
-
from pathlib import Path
from . import _types as _t
diff --git a/vcs-versioning/src/vcs_versioning/_integrator_helpers.py b/vcs-versioning/src/vcs_versioning/_integrator_helpers.py
new file mode 100644
index 00000000..04a1ed31
--- /dev/null
+++ b/vcs-versioning/src/vcs_versioning/_integrator_helpers.py
@@ -0,0 +1,113 @@
+"""Internal helpers for integrators to build configurations.
+
+This module provides substantial orchestration functions for building
+Configuration instances with proper override priority handling.
+
+Public API is exposed through __init__.py with restrictions.
+"""
+
+from __future__ import annotations
+
+import logging
+from typing import TYPE_CHECKING, Any
+
+if TYPE_CHECKING:
+ from ._config import Configuration
+ from ._pyproject_reading import PyProjectData
+
+log = logging.getLogger(__name__)
+
+
+def build_configuration_from_pyproject_internal(
+ pyproject_data: PyProjectData,
+ *,
+ dist_name: str | None = None,
+ **integrator_overrides: Any,
+) -> Configuration:
+ """Build Configuration with complete workflow orchestration.
+
+ This is a substantial helper that orchestrates the complete configuration
+ building workflow with proper priority handling.
+
+ Orchestration steps:
+ 1. Extract base config from pyproject_data.section
+ 2. Determine dist_name (argument > pyproject.project_name)
+ 3. Merge integrator overrides (override config file)
+ 4. Read and apply env TOML overrides (highest priority)
+ 5. Build Configuration with proper validation
+
+ Priority order (highest to lowest):
+ 1. Environment TOML overrides (TOOL_OVERRIDES_FOR_DIST, TOOL_OVERRIDES)
+ 2. Integrator **integrator_overrides arguments
+ 3. pyproject_data.section configuration
+ 4. Configuration defaults
+
+ Args:
+ pyproject_data: Parsed pyproject data from PyProjectData.from_file() or manual composition
+ dist_name: Distribution name for env var lookups (overrides pyproject_data.project_name)
+ **integrator_overrides: Integrator-provided config overrides
+ (override config file, but overridden by env)
+
+ Returns:
+ Configured Configuration instance ready for version inference
+
+ Example:
+ >>> from vcs_versioning import PyProjectData
+ >>> from vcs_versioning._integrator_helpers import build_configuration_from_pyproject_internal
+ >>>
+ >>> pyproject = PyProjectData.from_file(
+ ... "pyproject.toml",
+ ... _tool_names=["setuptools_scm", "vcs-versioning"]
+ ... )
+ >>> config = build_configuration_from_pyproject_internal(
+ ... pyproject_data=pyproject,
+ ... dist_name="my-package",
+ ... local_scheme="no-local-version", # Integrator override
+ ... )
+ """
+ # Import here to avoid circular dependencies
+ from ._config import Configuration
+ from ._overrides import read_toml_overrides
+ from ._pyproject_reading import get_args_for_pyproject
+
+ # Step 1: Get base config from pyproject section
+ # This also handles dist_name resolution
+ log.debug(
+ "Building configuration from pyproject at %s (tool: %s)",
+ pyproject_data.path,
+ pyproject_data.tool_name,
+ )
+
+ config_data = get_args_for_pyproject(
+ pyproject_data,
+ dist_name=dist_name,
+ kwargs={},
+ )
+
+ # Step 2: dist_name is now determined (from arg, config, or project.name)
+ actual_dist_name = config_data.get("dist_name")
+ log.debug("Resolved dist_name: %s", actual_dist_name)
+
+ # Step 3: Merge integrator overrides (middle priority - override config file)
+ if integrator_overrides:
+ log.debug(
+ "Applying integrator overrides: %s", list(integrator_overrides.keys())
+ )
+ config_data.update(integrator_overrides)
+
+ # Step 4: Apply environment TOML overrides (highest priority)
+ env_overrides = read_toml_overrides(actual_dist_name)
+ if env_overrides:
+ log.debug("Applying environment TOML overrides: %s", list(env_overrides.keys()))
+ config_data.update(env_overrides)
+
+ # Step 5: Build Configuration with validation
+ relative_to = pyproject_data.path
+ log.debug("Building Configuration with relative_to=%s", relative_to)
+
+ return Configuration.from_data(relative_to=relative_to, data=config_data)
+
+
+__all__ = [
+ "build_configuration_from_pyproject_internal",
+]
diff --git a/vcs-versioning/src/vcs_versioning/_log.py b/vcs-versioning/src/vcs_versioning/_log.py
new file mode 100644
index 00000000..989a50eb
--- /dev/null
+++ b/vcs-versioning/src/vcs_versioning/_log.py
@@ -0,0 +1,137 @@
+"""
+logging helpers, supports vendoring
+"""
+
+from __future__ import annotations
+
+import contextlib
+import logging
+from collections.abc import Iterator
+
+
+def make_default_handler() -> logging.Handler:
+ try:
+ from rich.console import Console
+
+ console = Console(stderr=True)
+ from rich.logging import RichHandler
+
+ return RichHandler(console=console)
+ except ImportError:
+ last_resort = logging.lastResort
+ assert last_resort is not None
+ return last_resort
+
+
+def _get_all_scm_loggers(
+ additional_loggers: list[logging.Logger] | None = None,
+) -> list[logging.Logger]:
+ """Get all SCM-related loggers that need configuration.
+
+ Always configures vcs_versioning logger.
+ If additional_loggers is provided, also configures those loggers.
+ If not provided, tries to get them from active GlobalOverrides context.
+ """
+ loggers = [logging.getLogger("vcs_versioning")]
+
+ if additional_loggers is not None:
+ loggers.extend(additional_loggers)
+ else:
+ # Try to get additional loggers from active overrides context
+ try:
+ from .overrides import _active_overrides
+
+ overrides = _active_overrides.get()
+ if overrides is not None:
+ loggers.extend(overrides.additional_loggers)
+ except ImportError:
+ # During early initialization, overrides module might not be available yet
+ pass
+
+ return loggers
+
+
+_default_handler: logging.Handler | None = None
+
+
+def _configure_loggers(
+ log_level: int, additional_loggers: list[logging.Logger] | None = None
+) -> None:
+ """Internal function to configure SCM-related loggers.
+
+ This is called automatically by GlobalOverrides.__enter__().
+ Do not call directly - use GlobalOverrides context manager instead.
+
+ Args:
+ log_level: Logging level constant from logging module
+ additional_loggers: Optional list of additional logger instances to configure
+ """
+ global _default_handler
+
+ if _default_handler is None:
+ _default_handler = make_default_handler()
+
+ for logger in _get_all_scm_loggers(additional_loggers):
+ if not logger.handlers:
+ logger.addHandler(_default_handler)
+ logger.setLevel(log_level)
+ logger.propagate = False
+
+
+# The vcs_versioning root logger
+# Note: This is created on import, but configured lazily via configure_logging()
+log = logging.getLogger("vcs_versioning")
+
+
+@contextlib.contextmanager
+def defer_to_pytest() -> Iterator[None]:
+ """Configure all SCM loggers to propagate to pytest's log capture."""
+ loggers = _get_all_scm_loggers()
+ old_states = []
+
+ for logger in loggers:
+ old_states.append((logger, logger.propagate, logger.level, logger.handlers[:]))
+ logger.propagate = True
+ logger.setLevel(logging.NOTSET)
+ # Remove all handlers
+ for handler in logger.handlers[:]:
+ logger.removeHandler(handler)
+
+ try:
+ yield
+ finally:
+ for logger, old_propagate, old_level, old_handlers in old_states:
+ for handler in old_handlers:
+ logger.addHandler(handler)
+ logger.propagate = old_propagate
+ logger.setLevel(old_level)
+
+
+@contextlib.contextmanager
+def enable_debug(handler: logging.Handler | None = None) -> Iterator[None]:
+ """Enable debug logging for all SCM loggers."""
+ global _default_handler
+ if handler is None:
+ if _default_handler is None:
+ _default_handler = make_default_handler()
+ handler = _default_handler
+
+ loggers = _get_all_scm_loggers()
+ old_states = []
+
+ for logger in loggers:
+ old_states.append((logger, logger.level))
+ logger.addHandler(handler)
+ logger.setLevel(logging.DEBUG)
+
+ old_handler_level = handler.level
+ handler.setLevel(logging.DEBUG)
+
+ try:
+ yield
+ finally:
+ handler.setLevel(old_handler_level)
+ for logger, old_level in old_states:
+ logger.setLevel(old_level)
+ if handler is not _default_handler:
+ logger.removeHandler(handler)
diff --git a/src/setuptools_scm/_modify_version.py b/vcs-versioning/src/vcs_versioning/_modify_version.py
similarity index 100%
rename from src/setuptools_scm/_modify_version.py
rename to vcs-versioning/src/vcs_versioning/_modify_version.py
diff --git a/src/setuptools_scm/_node_utils.py b/vcs-versioning/src/vcs_versioning/_node_utils.py
similarity index 100%
rename from src/setuptools_scm/_node_utils.py
rename to vcs-versioning/src/vcs_versioning/_node_utils.py
diff --git a/vcs-versioning/src/vcs_versioning/_overrides.py b/vcs-versioning/src/vcs_versioning/_overrides.py
new file mode 100644
index 00000000..be9909c8
--- /dev/null
+++ b/vcs-versioning/src/vcs_versioning/_overrides.py
@@ -0,0 +1,285 @@
+"""Internal implementation details for the overrides module.
+
+This module contains private helpers and functions used internally
+by vcs_versioning. Public API is exposed via the overrides module.
+"""
+
+from __future__ import annotations
+
+import dataclasses
+import logging
+import os
+from collections.abc import Mapping
+from datetime import date, datetime
+from difflib import get_close_matches
+from re import Pattern
+from typing import Any, TypedDict, get_type_hints
+
+from packaging.utils import canonicalize_name
+
+from . import _config
+from . import _types as _t
+from ._scm_version import ScmVersion, meta # noqa: F401 - for type checking
+from ._version_cls import Version as _Version
+
+log = logging.getLogger(__name__)
+
+
+# TypedDict schemas for TOML data validation and type hints
+
+
+class PretendMetadataDict(TypedDict, total=False):
+ """Schema for ScmVersion metadata fields that can be overridden via environment.
+
+ All fields are optional since partial overrides are allowed.
+ """
+
+ tag: str | _Version
+ distance: int
+ node: str | None
+ dirty: bool
+ preformatted: bool
+ branch: str | None
+ node_date: date | None
+ time: datetime
+
+
+class ConfigOverridesDict(TypedDict, total=False):
+ """Schema for Configuration fields that can be overridden via environment.
+
+ All fields are optional since partial overrides are allowed.
+ """
+
+ # Configuration fields
+ root: _t.PathT
+ version_scheme: _t.VERSION_SCHEME
+ local_scheme: _t.VERSION_SCHEME
+ tag_regex: str | Pattern[str]
+ parentdir_prefix_version: str | None
+ fallback_version: str | None
+ fallback_root: _t.PathT
+ write_to: _t.PathT | None
+ write_to_template: str | None
+ version_file: _t.PathT | None
+ version_file_template: str | None
+ parse: Any # ParseFunction - avoid circular import
+ git_describe_command: _t.CMD_TYPE | None # deprecated but still supported
+ dist_name: str | None
+ version_cls: Any # type[_Version] - avoid circular import
+ normalize: bool # Used in from_data
+ search_parent_directories: bool
+ parent: _t.PathT | None
+ scm: dict[str, Any] # Nested SCM configuration
+
+
+PRETEND_KEY = "SETUPTOOLS_SCM_PRETEND_VERSION"
+PRETEND_KEY_NAMED = PRETEND_KEY + "_FOR_{name}"
+PRETEND_METADATA_KEY = "SETUPTOOLS_SCM_PRETEND_METADATA"
+PRETEND_METADATA_KEY_NAMED = PRETEND_METADATA_KEY + "_FOR_{name}"
+
+
+def _search_env_vars_with_prefix(
+ prefix: str, dist_name: str, env: Mapping[str, str]
+) -> list[tuple[str, str]]:
+ """Search environment variables with a given prefix for potential dist name matches.
+
+ Args:
+ prefix: The environment variable prefix (e.g., "SETUPTOOLS_SCM_PRETEND_VERSION_FOR_")
+ dist_name: The original dist name to match against
+ env: Environment dictionary to search in
+
+ Returns:
+ List of (env_var_name, env_var_value) tuples for potential matches
+ """
+ # Get the canonical name for comparison
+ canonical_dist_name = canonicalize_name(dist_name)
+
+ matches = []
+ for env_var, value in env.items():
+ if env_var.startswith(prefix):
+ suffix = env_var[len(prefix) :]
+ # Normalize the suffix and compare to canonical dist name
+ try:
+ normalized_suffix = canonicalize_name(suffix.lower().replace("_", "-"))
+ if normalized_suffix == canonical_dist_name:
+ matches.append((env_var, value))
+ except Exception:
+ # If normalization fails for any reason, skip this env var
+ continue
+
+ return matches
+
+
+def _find_close_env_var_matches(
+ prefix: str, expected_suffix: str, env: Mapping[str, str], threshold: float = 0.6
+) -> list[str]:
+ """Find environment variables with similar suffixes that might be typos.
+
+ Args:
+ prefix: The environment variable prefix
+ expected_suffix: The expected suffix (canonicalized dist name in env var format)
+ env: Environment dictionary to search in
+ threshold: Similarity threshold for matches (0.0 to 1.0)
+
+ Returns:
+ List of environment variable names that are close matches
+ """
+ candidates = []
+ for env_var in env:
+ if env_var.startswith(prefix):
+ suffix = env_var[len(prefix) :]
+ candidates.append(suffix)
+
+ # Use difflib to find close matches
+ close_matches_list = get_close_matches(
+ expected_suffix, candidates, n=3, cutoff=threshold
+ )
+
+ return [
+ f"{prefix}{match}" for match in close_matches_list if match != expected_suffix
+ ]
+
+
+def _read_pretended_metadata_for(
+ config: _config.Configuration,
+) -> PretendMetadataDict | None:
+ """read overridden metadata from the environment
+
+ tries ``SETUPTOOLS_SCM_PRETEND_METADATA``
+ and ``SETUPTOOLS_SCM_PRETEND_METADATA_FOR_$UPPERCASE_DIST_NAME``
+
+ Returns a dictionary with metadata field overrides like:
+ {"node": "g1337beef", "distance": 4}
+ """
+ from .overrides import EnvReader
+
+ log.debug("dist name: %s", config.dist_name)
+
+ reader = EnvReader(
+ tools_names=("SETUPTOOLS_SCM", "VCS_VERSIONING"),
+ env=os.environ,
+ dist_name=config.dist_name,
+ )
+
+ try:
+ # Use schema validation during TOML parsing
+ metadata_overrides = reader.read_toml(
+ "PRETEND_METADATA", schema=PretendMetadataDict
+ )
+ return metadata_overrides or None
+ except Exception as e:
+ log.error("Failed to parse pretend metadata: %s", e)
+ return None
+
+
+def _apply_metadata_overrides(
+ scm_version: ScmVersion | None,
+ config: _config.Configuration,
+) -> ScmVersion | None:
+ """Apply metadata overrides to a ScmVersion object.
+
+ This function reads pretend metadata from environment variables and applies
+ the overrides to the given ScmVersion. TOML type coercion is used so values
+ should be provided in their correct types (int, bool, datetime, etc.).
+
+ Args:
+ scm_version: The ScmVersion to apply overrides to, or None
+ config: Configuration object
+
+ Returns:
+ Modified ScmVersion with overrides applied, or None
+ """
+ metadata_overrides = _read_pretended_metadata_for(config)
+
+ if not metadata_overrides:
+ return scm_version
+
+ if scm_version is None:
+ log.warning(
+ "PRETEND_METADATA specified but no base version found. "
+ "Metadata overrides cannot be applied without a base version."
+ )
+ return None
+
+ log.info("Applying metadata overrides: %s", metadata_overrides)
+
+ # Get type hints from PretendMetadataDict for validation
+ field_types = get_type_hints(PretendMetadataDict)
+
+ # Apply each override individually using dataclasses.replace
+ result = scm_version
+
+ for field, value in metadata_overrides.items():
+ # Validate field types using the TypedDict annotations
+ if field in field_types:
+ expected_type = field_types[field]
+ # Handle Optional/Union types (e.g., str | None)
+ if hasattr(expected_type, "__args__"):
+ # Union type - check if value is instance of any of the types
+ valid = any(
+ isinstance(value, t) if t is not type(None) else value is None
+ for t in expected_type.__args__
+ )
+ if not valid:
+ type_names = " | ".join(
+ t.__name__ if t is not type(None) else "None"
+ for t in expected_type.__args__
+ )
+ raise TypeError(
+ f"Field '{field}' must be {type_names}, "
+ f"got {type(value).__name__}: {value!r}"
+ )
+ else:
+ # Simple type
+ if not isinstance(value, expected_type):
+ raise TypeError(
+ f"Field '{field}' must be {expected_type.__name__}, "
+ f"got {type(value).__name__}: {value!r}"
+ )
+
+ result = dataclasses.replace(result, **{field: value}) # type: ignore[arg-type]
+
+ # Ensure config is preserved (should not be overridden)
+ assert result.config is config, "Config must be preserved during metadata overrides"
+
+ return result
+
+
+def _read_pretended_version_for(
+ config: _config.Configuration,
+) -> ScmVersion | None:
+ """read a a overridden version from the environment
+
+ tries ``SETUPTOOLS_SCM_PRETEND_VERSION``
+ and ``SETUPTOOLS_SCM_PRETEND_VERSION_FOR_$UPPERCASE_DIST_NAME``
+ """
+ from .overrides import EnvReader
+
+ log.debug("dist name: %s", config.dist_name)
+
+ reader = EnvReader(
+ tools_names=("SETUPTOOLS_SCM", "VCS_VERSIONING"),
+ env=os.environ,
+ dist_name=config.dist_name,
+ )
+ pretended = reader.read("PRETEND_VERSION")
+
+ if pretended:
+ return meta(tag=pretended, preformatted=True, config=config)
+ else:
+ return None
+
+
+def read_toml_overrides(dist_name: str | None) -> ConfigOverridesDict:
+ """Read TOML overrides from environment.
+
+ Validates that only known Configuration fields are provided.
+ """
+ from .overrides import EnvReader
+
+ reader = EnvReader(
+ tools_names=("SETUPTOOLS_SCM", "VCS_VERSIONING"),
+ env=os.environ,
+ dist_name=dist_name,
+ )
+ return reader.read_toml("OVERRIDES", schema=ConfigOverridesDict)
diff --git a/src/setuptools_scm/_integration/pyproject_reading.py b/vcs-versioning/src/vcs_versioning/_pyproject_reading.py
similarity index 61%
rename from src/setuptools_scm/_integration/pyproject_reading.py
rename to vcs-versioning/src/vcs_versioning/_pyproject_reading.py
index eb21dfa4..3fbabd20 100644
--- a/src/setuptools_scm/_integration/pyproject_reading.py
+++ b/vcs-versioning/src/vcs_versioning/_pyproject_reading.py
@@ -1,29 +1,36 @@
+"""Core pyproject.toml reading functionality"""
+
from __future__ import annotations
+import logging
+import os
+import sys
import warnings
-
+from collections.abc import Sequence
from dataclasses import dataclass
from pathlib import Path
-from typing import Sequence
+from typing import TypeAlias
+
+if sys.version_info >= (3, 11):
+ from typing import Self
+else:
+ from typing_extensions import Self
-from .. import _log
-from .. import _types as _t
-from .._requirement_cls import extract_package_name
-from .toml import TOML_RESULT
-from .toml import InvalidTomlError
-from .toml import read_toml_content
+from ._requirement_cls import extract_package_name
+from ._toml import TOML_RESULT, InvalidTomlError, read_toml_content
-log = _log.log.getChild("pyproject_reading")
+log = logging.getLogger(__name__)
_ROOT = "root"
DEFAULT_PYPROJECT_PATH = Path("pyproject.toml")
-DEFAULT_TOOL_NAME = "setuptools_scm"
@dataclass
class PyProjectData:
+ """Core pyproject.toml data structure"""
+
path: Path
tool_name: str
project: TOML_RESULT
@@ -32,11 +39,13 @@ class PyProjectData:
section_present: bool
project_present: bool
build_requires: list[str]
+ definition: TOML_RESULT
@classmethod
def for_testing(
cls,
*,
+ tool_name: str,
is_required: bool = False,
section_present: bool = False,
project_present: bool = False,
@@ -44,7 +53,7 @@ def for_testing(
has_dynamic_version: bool = True,
build_requires: list[str] | None = None,
local_scheme: str | None = None,
- ) -> PyProjectData:
+ ) -> Self:
"""Create a PyProjectData instance for testing purposes."""
project: TOML_RESULT
if project_name is not None:
@@ -66,19 +75,18 @@ def for_testing(
section = {}
return cls(
path=DEFAULT_PYPROJECT_PATH,
- tool_name=DEFAULT_TOOL_NAME,
+ tool_name=tool_name,
project=project,
section=section,
is_required=is_required,
section_present=section_present,
project_present=project_present,
build_requires=build_requires,
+ definition={},
)
@classmethod
- def empty(
- cls, path: Path = DEFAULT_PYPROJECT_PATH, tool_name: str = DEFAULT_TOOL_NAME
- ) -> PyProjectData:
+ def empty(cls, tool_name: str, path: Path = DEFAULT_PYPROJECT_PATH) -> Self:
return cls(
path=path,
tool_name=tool_name,
@@ -88,8 +96,52 @@ def empty(
section_present=False,
project_present=False,
build_requires=[],
+ definition={},
)
+ @classmethod
+ def from_file(
+ cls,
+ path: str | os.PathLike[str] = "pyproject.toml",
+ *,
+ _tool_names: list[str] | None = None,
+ ) -> Self:
+ """Load PyProjectData from pyproject.toml.
+
+ Public API: reads tool.vcs-versioning section.
+ Internal use: pass _tool_names for multi-tool support (e.g., setuptools_scm transition).
+
+ Args:
+ path: Path to pyproject.toml file
+ _tool_names: Internal parameter for multi-tool support.
+ If None, uses ["vcs-versioning"] (public API behavior).
+
+ Returns:
+ PyProjectData instance loaded from file
+
+ Raises:
+ FileNotFoundError: If pyproject.toml not found
+ InvalidTomlError: If pyproject.toml has invalid TOML syntax
+
+ Example:
+ >>> # Public API usage
+ >>> pyproject = PyProjectData.from_file("pyproject.toml")
+ >>>
+ >>> # Internal usage (setuptools_scm transition)
+ >>> pyproject = PyProjectData.from_file(
+ ... "pyproject.toml",
+ ... _tool_names=["setuptools_scm", "vcs-versioning"]
+ ... )
+ """
+ if _tool_names is None:
+ # Public API path - only vcs-versioning
+ _tool_names = ["vcs-versioning"]
+
+ result = read_pyproject(Path(path), tool_names=_tool_names)
+ # Type narrowing for mypy: read_pyproject returns PyProjectData,
+ # but subclasses (like setuptools_scm's extended version) need Self
+ return result # type: ignore[return-value]
+
@property
def project_name(self) -> str | None:
return self.project.get("name")
@@ -103,37 +155,17 @@ def project_version(self) -> str | None:
"""
return self.project.get("version")
- def should_infer(self) -> bool:
- """
- Determine if setuptools_scm should infer version based on configuration.
-
- Infer when:
- 1. An explicit [tool.setuptools_scm] section is present, OR
- 2. setuptools-scm[simple] is in build-system.requires AND
- version is in project.dynamic
-
- Returns:
- True if [tool.setuptools_scm] is present, otherwise False
- """
- # Original behavior: explicit tool section
- if self.section_present:
- return True
- # New behavior: simple extra + dynamic version
- if self.project_present:
- dynamic_fields = self.project.get("dynamic", [])
- if "version" in dynamic_fields:
- if has_build_package_with_extra(
- self.build_requires, "setuptools-scm", "simple"
- ):
- return True
-
- return False
+# Testing injection type for configuration reading
+GivenPyProjectResult: TypeAlias = (
+ PyProjectData | InvalidTomlError | FileNotFoundError | None
+)
def has_build_package(
requires: Sequence[str], canonical_build_package_name: str
) -> bool:
+ """Check if a package is in build requirements."""
for requirement in requires:
package_name = extract_package_name(requirement)
if package_name == canonical_build_package_name:
@@ -141,40 +173,12 @@ def has_build_package(
return False
-def has_build_package_with_extra(
- requires: Sequence[str], canonical_build_package_name: str, extra_name: str
-) -> bool:
- """Check if a build dependency has a specific extra.
-
- Args:
- requires: List of requirement strings from build-system.requires
- canonical_build_package_name: The canonical package name to look for
- extra_name: The extra name to check for (e.g., "simple")
-
- Returns:
- True if the package is found with the specified extra
- """
- from .._requirement_cls import Requirement
-
- for requirement_string in requires:
- try:
- requirement = Requirement(requirement_string)
- package_name = extract_package_name(requirement_string)
- if package_name == canonical_build_package_name:
- if extra_name in requirement.extras:
- return True
- except Exception:
- # If parsing fails, continue to next requirement
- continue
- return False
-
-
def read_pyproject(
path: Path = DEFAULT_PYPROJECT_PATH,
- tool_name: str = DEFAULT_TOOL_NAME,
canonical_build_package_name: str = "setuptools-scm",
- _given_result: _t.GivenPyProjectResult = None,
+ _given_result: GivenPyProjectResult = None,
_given_definition: TOML_RESULT | None = None,
+ tool_names: list[str] | None = None,
) -> PyProjectData:
"""Read and parse pyproject configuration.
@@ -182,7 +186,6 @@ def read_pyproject(
and ``_given_definition``.
:param path: Path to the pyproject file
- :param tool_name: The tool section name (default: ``setuptools_scm``)
:param canonical_build_package_name: Normalized build requirement name
:param _given_result: Optional testing hook. Can be:
- ``PyProjectData``: returned directly
@@ -191,12 +194,14 @@ def read_pyproject(
:param _given_definition: Optional testing hook to provide parsed TOML content.
When provided, this dictionary is used instead of reading and parsing
the file from disk. Ignored if ``_given_result`` is provided.
+ :param tool_names: List of tool section names to try in order.
+ If None, defaults to ["vcs-versioning", "setuptools_scm"]
"""
if _given_result is not None:
if isinstance(_given_result, PyProjectData):
return _given_result
- if isinstance(_given_result, (InvalidTomlError, FileNotFoundError)):
+ if isinstance(_given_result, InvalidTomlError | FileNotFoundError):
raise _given_result
if _given_definition is not None:
@@ -208,40 +213,46 @@ def read_pyproject(
is_required = has_build_package(requires, canonical_build_package_name)
tool_section = defn.get("tool", {})
- section = tool_section.get(tool_name, {})
- section_present = tool_name in tool_section
+
+ # Determine which tool names to try
+ if tool_names is None:
+ # Default: try vcs-versioning first, then setuptools_scm for backward compat
+ tool_names = ["vcs-versioning", "setuptools_scm"]
+
+ # Try each tool name in order
+ section = {}
+ section_present = False
+ actual_tool_name = tool_names[0] if tool_names else "vcs-versioning"
+
+ for name in tool_names:
+ if name in tool_section:
+ section = tool_section[name]
+ section_present = True
+ actual_tool_name = name
+ break
if not section_present:
log.warning(
- "toml section missing %r does not contain a tool.%s section",
+ "toml section missing %r does not contain any of the tool sections: %s",
path,
- tool_name,
+ tool_names,
)
project = defn.get("project", {})
project_present = "project" in defn
+
pyproject_data = PyProjectData(
path,
- tool_name,
+ actual_tool_name,
project,
section,
is_required,
section_present,
project_present,
requires,
+ defn,
)
- setuptools_dynamic_version = (
- defn.get("tool", {})
- .get("setuptools", {})
- .get("dynamic", {})
- .get("version", None)
- )
- if setuptools_dynamic_version is not None:
- from .deprecation import warn_pyproject_setuptools_dynamic_version
-
- warn_pyproject_setuptools_dynamic_version(path)
-
return pyproject_data
@@ -258,7 +269,8 @@ def get_args_for_pyproject(
warnings.warn(
f"{pyproject.path}: at [tool.{pyproject.tool_name}]\n"
f"ignoring value relative_to={relative!r}"
- " as its always relative to the config file"
+ " as its always relative to the config file",
+ stacklevel=2,
)
if "dist_name" in section:
if dist_name is None:
@@ -276,7 +288,8 @@ def get_args_for_pyproject(
if section[_ROOT] != kwargs[_ROOT]:
warnings.warn(
f"root {section[_ROOT]} is overridden"
- f" by the cli arg {kwargs[_ROOT]}"
+ f" by the cli arg {kwargs[_ROOT]}",
+ stacklevel=2,
)
section.pop(_ROOT, None)
return {"dist_name": dist_name, **section, **kwargs}
diff --git a/src/setuptools_scm/_requirement_cls.py b/vcs-versioning/src/vcs_versioning/_requirement_cls.py
similarity index 56%
rename from src/setuptools_scm/_requirement_cls.py
rename to vcs-versioning/src/vcs_versioning/_requirement_cls.py
index 9bb88462..43d1424d 100644
--- a/src/setuptools_scm/_requirement_cls.py
+++ b/vcs-versioning/src/vcs_versioning/_requirement_cls.py
@@ -1,21 +1,13 @@
from __future__ import annotations
-__all__ = ["Requirement", "extract_package_name"]
+import logging
-try:
- from packaging.requirements import Requirement
- from packaging.utils import canonicalize_name
-except ImportError:
- from setuptools.extern.packaging.requirements import ( # type: ignore[import-not-found,no-redef]
- Requirement as Requirement,
- )
- from setuptools.extern.packaging.utils import ( # type: ignore[import-not-found,no-redef]
- canonicalize_name as canonicalize_name,
- )
+__all__ = ["Requirement", "extract_package_name"]
-from . import _log
+from packaging.requirements import Requirement
+from packaging.utils import canonicalize_name
-log = _log.log.getChild("requirement_cls")
+log = logging.getLogger(__name__)
def extract_package_name(requirement_string: str) -> str:
diff --git a/src/setuptools_scm/_run_cmd.py b/vcs-versioning/src/vcs_versioning/_run_cmd.py
similarity index 87%
rename from src/setuptools_scm/_run_cmd.py
rename to vcs-versioning/src/vcs_versioning/_run_cmd.py
index 2dff6369..cbcd2856 100644
--- a/src/setuptools_scm/_run_cmd.py
+++ b/vcs-versioning/src/vcs_versioning/_run_cmd.py
@@ -1,48 +1,38 @@
from __future__ import annotations
+import logging
import os
import shlex
import subprocess
import textwrap
import warnings
+from collections.abc import Callable, Mapping, Sequence
+from typing import TypeVar, overload
-from typing import TYPE_CHECKING
-from typing import Callable
-from typing import Final
-from typing import Mapping
-from typing import Sequence
-from typing import TypeVar
-from typing import overload
-
-from . import _log
from . import _types as _t
-if TYPE_CHECKING:
- BaseCompletedProcess = subprocess.CompletedProcess[str]
-else:
- BaseCompletedProcess = subprocess.CompletedProcess
-
-# pick 40 seconds
-# unfortunately github CI for windows sometimes needs
-# up to 30 seconds to start a command
-
def _get_timeout(env: Mapping[str, str]) -> int:
- return int(env.get("SETUPTOOLS_SCM_SUBPROCESS_TIMEOUT") or 40)
+ """Get subprocess timeout from override context or environment.
+
+ This function is kept for backward compatibility but now uses the
+ global override system.
+ """
+ from .overrides import get_subprocess_timeout
+ return get_subprocess_timeout()
-BROKEN_TIMEOUT: Final[int] = _get_timeout(os.environ)
-log = _log.log.getChild("run_cmd")
+log = logging.getLogger(__name__)
PARSE_RESULT = TypeVar("PARSE_RESULT")
T = TypeVar("T")
-class CompletedProcess(BaseCompletedProcess):
+class CompletedProcess(subprocess.CompletedProcess[str]):
@classmethod
def from_raw(
- cls, input: BaseCompletedProcess, strip: bool = True
+ cls, input: subprocess.CompletedProcess[str], strip: bool = True
) -> CompletedProcess:
return cls(
args=input.args,
@@ -153,7 +143,7 @@ def run(
cmd_4_trace = " ".join(map(_unsafe_quote_for_display, cmd))
log.debug("at %s\n $ %s ", cwd, cmd_4_trace)
if timeout is None:
- timeout = BROKEN_TIMEOUT
+ timeout = _get_timeout(os.environ)
res = subprocess.run(
cmd,
capture_output=True,
@@ -208,7 +198,7 @@ def has_command(
else:
res = not p.returncode
if not res and warn:
- warnings.warn(f"{name!r} was not found", category=RuntimeWarning)
+ warnings.warn(f"{name!r} was not found", category=RuntimeWarning, stacklevel=2)
return res
diff --git a/vcs-versioning/src/vcs_versioning/_scm_version.py b/vcs-versioning/src/vcs_versioning/_scm_version.py
new file mode 100644
index 00000000..0d32b497
--- /dev/null
+++ b/vcs-versioning/src/vcs_versioning/_scm_version.py
@@ -0,0 +1,385 @@
+"""Core ScmVersion data structure and parsing utilities.
+
+This module contains the ScmVersion class which represents a parsed version
+from source control metadata, along with utilities for creating and parsing
+ScmVersion objects.
+"""
+
+from __future__ import annotations
+
+import dataclasses
+import logging
+import warnings
+from collections.abc import Callable
+from datetime import date, datetime
+from typing import TYPE_CHECKING, Any, Concatenate, ParamSpec, TypedDict
+
+from . import _config
+from . import _version_cls as _v
+from ._node_utils import _format_node_for_output
+from ._version_cls import _Version
+
+if TYPE_CHECKING:
+ import sys
+
+ if sys.version_info >= (3, 11):
+ from typing import Unpack
+ else:
+ from typing_extensions import Unpack
+
+_P = ParamSpec("_P")
+
+log = logging.getLogger(__name__)
+
+
+class _TagDict(TypedDict):
+ version: str
+ prefix: str
+ suffix: str
+
+
+class VersionExpectations(TypedDict, total=False):
+ """Expected properties for ScmVersion matching."""
+
+ tag: str | _Version
+ distance: int
+ dirty: bool
+ node_prefix: str # Prefix of the node/commit hash
+ branch: str | None
+ exact: bool
+ preformatted: bool
+ node_date: date | None
+ time: datetime | None
+
+
+@dataclasses.dataclass
+class mismatches:
+ """Represents mismatches between expected and actual ScmVersion properties."""
+
+ expected: dict[str, Any]
+ actual: dict[str, Any]
+
+ def __bool__(self) -> bool:
+ """mismatches is falsy to allow `if not version.matches(...)`."""
+ return False
+
+ def __str__(self) -> str:
+ """Format mismatches for error reporting."""
+ lines = []
+ for key, exp_val in self.expected.items():
+ if key == "node_prefix":
+ # Special handling for node prefix matching
+ actual_node = self.actual.get("node")
+ if not actual_node or not actual_node.startswith(exp_val):
+ lines.append(
+ f" node: expected prefix '{exp_val}', got '{actual_node}'"
+ )
+ else:
+ act_val = self.actual.get(key)
+ if str(exp_val) != str(act_val):
+ lines.append(f" {key}: expected {exp_val!r}, got {act_val!r}")
+ return "\n".join(lines)
+
+ def __repr__(self) -> str:
+ return f"mismatches(expected={self.expected!r}, actual={self.actual!r})"
+
+
+def _parse_version_tag(
+ tag: str | object, config: _config.Configuration
+) -> _TagDict | None:
+ match = config.tag_regex.match(str(tag))
+
+ if match:
+ key: str | int = 1 if len(match.groups()) == 1 else "version"
+ full = match.group(0)
+ log.debug("%r %r %s", tag, config.tag_regex, match)
+ log.debug(
+ "key %s data %s, %s, %r", key, match.groupdict(), match.groups(), full
+ )
+
+ if version := match.group(key):
+ result = _TagDict(
+ version=version,
+ prefix=full[: match.start(key)],
+ suffix=full[match.end(key) :],
+ )
+
+ log.debug("tag %r parsed to %r", tag, result)
+ return result
+
+ raise ValueError(
+ f'The tag_regex "{config.tag_regex.pattern}" matched tag "{tag}", '
+ "however the matched group has no value."
+ )
+ else:
+ log.debug("tag %r did not parse", tag)
+
+ return None
+
+
+def callable_or_entrypoint(group: str, callable_or_name: str | Any) -> Any:
+ log.debug("ep %r %r", group, callable_or_name)
+
+ if callable(callable_or_name):
+ return callable_or_name
+
+ from ._entrypoints import _get_ep
+
+ return _get_ep(group, callable_or_name)
+
+
+def tag_to_version(
+ tag: _Version | str, config: _config.Configuration
+) -> _Version | None:
+ """
+ take a tag that might be prefixed with a keyword and return only the version part
+ """
+ log.debug("tag %s", tag)
+
+ tag_dict = _parse_version_tag(tag, config)
+ if tag_dict is None or not tag_dict.get("version", None):
+ warnings.warn(f"tag {tag!r} no version found", stacklevel=2)
+ return None
+
+ version_str = tag_dict["version"]
+ log.debug("version pre parse %s", version_str)
+
+ # Try to create version from base version first
+ try:
+ version: _Version = config.version_cls(version_str)
+ log.debug("version=%r", version)
+ except Exception:
+ warnings.warn(
+ f"tag {tag!r} will be stripped of its suffix {tag_dict.get('suffix', '')!r}",
+ stacklevel=2,
+ )
+ # Fall back to trying without any suffix
+ version = config.version_cls(version_str)
+ log.debug("version=%r", version)
+ return version
+
+ # If base version is valid, check if we can preserve the suffix
+ if suffix := tag_dict.get("suffix", ""):
+ log.debug("tag %r includes local build data %r, preserving it", tag, suffix)
+ # Try creating version with suffix - if it fails, we'll use the base version
+ try:
+ version_with_suffix: _Version = config.version_cls(version_str + suffix)
+ log.debug("version with suffix=%r", version_with_suffix)
+ return version_with_suffix
+ except Exception:
+ warnings.warn(
+ f"tag {tag!r} will be stripped of its suffix {suffix!r}", stacklevel=2
+ )
+ # Return the base version without suffix
+ return version
+
+ return version
+
+
+def _source_epoch_or_utc_now() -> datetime:
+ """Get datetime from SOURCE_DATE_EPOCH or current UTC time.
+
+ Uses the active GlobalOverrides context if available, otherwise returns
+ current UTC time.
+ """
+ from .overrides import source_epoch_or_utc_now
+
+ return source_epoch_or_utc_now()
+
+
+@dataclasses.dataclass
+class ScmVersion:
+ """represents a parsed version from scm"""
+
+ tag: _v.Version | _v.NonNormalizedVersion
+ """the related tag or preformatted version"""
+ config: _config.Configuration
+ """the configuration used to parse the version"""
+ distance: int = 0
+ """the number of commits since the tag"""
+ node: str | None = None
+ """the shortened node id"""
+ dirty: bool = False
+ """whether the working copy had uncommitted changes"""
+ preformatted: bool = False
+ """whether the version string was preformatted"""
+ branch: str | None = None
+ """the branch name if any"""
+ node_date: date | None = None
+ """the date of the commit if available"""
+ time: datetime = dataclasses.field(default_factory=_source_epoch_or_utc_now)
+ """the current time or source epoch time
+ only set for unit-testing version schemes
+ for real usage it must be `now(utc)` or `SOURCE_EPOCH`
+ """
+
+ @property
+ def exact(self) -> bool:
+ """returns true checked out exactly on a tag and no local changes apply"""
+ return self.distance == 0 and not self.dirty
+
+ @property
+ def short_node(self) -> str | None:
+ """Return the node formatted for output."""
+ return _format_node_for_output(self.node)
+
+ def __repr__(self) -> str:
+ return (
+ f""
+ )
+
+ def format_with(self, fmt: str, **kw: object) -> str:
+ """format a given format string with attributes of this object"""
+ return fmt.format(
+ time=self.time,
+ tag=self.tag,
+ distance=self.distance,
+ node=_format_node_for_output(self.node),
+ dirty=self.dirty,
+ branch=self.branch,
+ node_date=self.node_date,
+ **kw,
+ )
+
+ def format_choice(self, clean_format: str, dirty_format: str, **kw: object) -> str:
+ """given `clean_format` and `dirty_format`
+
+ choose one based on `self.dirty` and format it using `self.format_with`"""
+
+ return self.format_with(dirty_format if self.dirty else clean_format, **kw)
+
+ def format_next_version(
+ self,
+ guess_next: Callable[Concatenate[ScmVersion, _P], str],
+ fmt: str = "{guessed}.dev{distance}",
+ *k: _P.args,
+ **kw: _P.kwargs,
+ ) -> str:
+ guessed = guess_next(self, *k, **kw)
+ return self.format_with(fmt, guessed=guessed)
+
+ def matches(self, **expectations: Unpack[VersionExpectations]) -> bool | mismatches:
+ """Check if this ScmVersion matches the given expectations.
+
+ Returns True if all specified properties match, or a mismatches
+ object (which is falsy) containing details of what didn't match.
+
+ Args:
+ **expectations: Properties to check, using VersionExpectations TypedDict
+ """
+ # Map expectation keys to ScmVersion attributes
+ attr_map: dict[str, Callable[[], Any]] = {
+ "tag": lambda: str(self.tag),
+ "node_prefix": lambda: self.node,
+ "distance": lambda: self.distance,
+ "dirty": lambda: self.dirty,
+ "branch": lambda: self.branch,
+ "exact": lambda: self.exact,
+ "preformatted": lambda: self.preformatted,
+ "node_date": lambda: self.node_date,
+ "time": lambda: self.time,
+ }
+
+ # Build actual values dict
+ actual: dict[str, Any] = {
+ key: attr_map[key]() for key in expectations if key in attr_map
+ }
+
+ # Process expectations
+ expected = {
+ "tag" if k == "tag" else k: str(v) if k == "tag" else v
+ for k, v in expectations.items()
+ }
+
+ # Check for mismatches
+ def has_mismatch() -> bool:
+ for key, exp_val in expected.items():
+ if key == "node_prefix":
+ act_val = actual.get("node_prefix")
+ if not act_val or not act_val.startswith(exp_val):
+ return True
+ else:
+ if str(exp_val) != str(actual.get(key)):
+ return True
+ return False
+
+ if has_mismatch():
+ # Rename node_prefix back to node for actual values in mismatch reporting
+ if "node_prefix" in actual:
+ actual["node"] = actual.pop("node_prefix")
+ return mismatches(expected=expected, actual=actual)
+ return True
+
+
+def _parse_tag(
+ tag: _Version | str, preformatted: bool, config: _config.Configuration
+) -> _Version:
+ if preformatted:
+ # For preformatted versions, tag should already be validated as a version object
+ # String validation is handled in meta function before calling this
+ if isinstance(tag, str):
+ # This should not happen with enhanced meta, but kept for safety
+ return _v.NonNormalizedVersion(tag)
+ else:
+ # Already a version object (including test mocks), return as-is
+ return tag
+ elif not isinstance(tag, config.version_cls):
+ version = tag_to_version(tag, config)
+ assert version is not None
+ return version
+ else:
+ return tag
+
+
+class _ScmVersionKwargs(TypedDict, total=False):
+ """TypedDict for ScmVersion constructor keyword arguments."""
+
+ distance: int
+ node: str | None
+ dirty: bool
+ preformatted: bool
+ branch: str | None
+ node_date: date | None
+ time: datetime
+
+
+def meta(
+ tag: str | _Version,
+ *,
+ distance: int = 0,
+ dirty: bool = False,
+ node: str | None = None,
+ preformatted: bool = False,
+ branch: str | None = None,
+ config: _config.Configuration,
+ node_date: date | None = None,
+ time: datetime | None = None,
+) -> ScmVersion:
+ parsed_version: _Version
+ # Enhanced string validation for preformatted versions
+ if preformatted and isinstance(tag, str):
+ # Validate PEP 440 compliance using NonNormalizedVersion
+ # Let validation errors bubble up to the caller
+ parsed_version = _v.NonNormalizedVersion(tag)
+ else:
+ # Use existing _parse_tag logic for non-preformatted or already validated inputs
+ parsed_version = _parse_tag(tag, preformatted, config)
+
+ log.info("version %s -> %s", tag, parsed_version)
+ assert parsed_version is not None, f"Can't parse version {tag}"
+
+ # Pass time explicitly to avoid triggering default_factory if provided
+ kwargs: _ScmVersionKwargs = {
+ "distance": distance,
+ "node": node,
+ "dirty": dirty,
+ "preformatted": preformatted,
+ "branch": branch,
+ "node_date": node_date,
+ }
+ if time is not None:
+ kwargs["time"] = time
+
+ scm_version = ScmVersion(parsed_version, config=config, **kwargs)
+ return scm_version
diff --git a/testing/wd_wrapper.py b/vcs-versioning/src/vcs_versioning/_test_utils.py
similarity index 85%
rename from testing/wd_wrapper.py
rename to vcs-versioning/src/vcs_versioning/_test_utils.py
index 92904dc3..d9c93a23 100644
--- a/testing/wd_wrapper.py
+++ b/vcs-versioning/src/vcs_versioning/_test_utils.py
@@ -1,26 +1,20 @@
from __future__ import annotations
import itertools
-
+from collections.abc import Callable
from pathlib import Path
-from typing import TYPE_CHECKING
-from typing import Any
-from typing import Callable
+from typing import TYPE_CHECKING, Any
import pytest
-from setuptools_scm._run_cmd import has_command
+from vcs_versioning._run_cmd import has_command
if TYPE_CHECKING:
- from setuptools_scm import Configuration
- from setuptools_scm.version import ScmVersion
- from setuptools_scm.version import VersionExpectations
-
- if itertools: # Make mypy happy about unused import
- pass
-
import sys
+ from vcs_versioning._config import Configuration
+ from vcs_versioning._scm_version import ScmVersion, VersionExpectations
+
if sys.version_info >= (3, 11):
from typing import Unpack
else:
@@ -47,7 +41,7 @@ def __call__(self, cmd: list[str] | str, *, timeout: int = 10, **kw: object) ->
if kw:
assert isinstance(cmd, str), "formatting the command requires text input"
cmd = cmd.format(**kw)
- from setuptools_scm._run_cmd import run
+ from vcs_versioning._run_cmd import run
return run(cmd, cwd=self.cwd, timeout=timeout).stdout
@@ -86,7 +80,7 @@ def commit_testfile(self, reason: str | None = None, signed: bool = False) -> No
def get_version(self, **kw: Any) -> str:
__tracebackhide__ = True
- from setuptools_scm import get_version
+ from vcs_versioning._get_version_impl import get_version
version = get_version(root=self.cwd, fallback_root=self.cwd, **kw)
print(self.cwd.name, version, sep=": ")
@@ -95,7 +89,10 @@ def get_version(self, **kw: Any) -> str:
def create_basic_setup_py(
self, name: str = "test-package", use_scm_version: str = "True"
) -> None:
- """Create a basic setup.py file with setuptools_scm configuration."""
+ """Create a basic setup.py file with version configuration.
+
+ Note: This is for setuptools_scm compatibility testing.
+ """
self.write(
"setup.py",
f"""__import__('setuptools').setup(
@@ -105,9 +102,18 @@ def create_basic_setup_py(
)
def create_basic_pyproject_toml(
- self, name: str = "test-package", dynamic_version: bool = True
+ self,
+ name: str = "test-package",
+ dynamic_version: bool = True,
+ tool_name: str = "vcs-versioning",
) -> None:
- """Create a basic pyproject.toml file with setuptools_scm configuration."""
+ """Create a basic pyproject.toml file with version configuration.
+
+ Args:
+ name: Project name
+ dynamic_version: Whether to add dynamic=['version']
+ tool_name: Tool section name (e.g., 'vcs-versioning' or 'setuptools_scm')
+ """
dynamic_section = 'dynamic = ["version"]' if dynamic_version else ""
self.write(
"pyproject.toml",
@@ -119,7 +125,7 @@ def create_basic_pyproject_toml(
name = "{name}"
{dynamic_section}
-[tool.setuptools_scm]
+[tool.{tool_name}]
""",
)
@@ -151,7 +157,7 @@ def create_tag(self, tag: str = "1.0.0") -> None:
def configure_git_commands(self) -> None:
"""Configure git commands without initializing the repository."""
- from setuptools_scm.git import parse as git_parse
+ from vcs_versioning._backends._git import parse as git_parse
self.add_command = "git add ."
self.commit_command = "git commit -m test-{reason}"
@@ -160,7 +166,7 @@ def configure_git_commands(self) -> None:
def configure_hg_commands(self) -> None:
"""Configure mercurial commands without initializing the repository."""
- from setuptools_scm.hg import parse as hg_parse
+ from vcs_versioning._backends._hg import parse as hg_parse
self.add_command = "hg add ."
self.commit_command = 'hg commit -m test-{reason} -u test -d "0 0"'
@@ -227,7 +233,7 @@ def expect_parse(
Uses the same signature as ScmVersion.matches() via TypedDict Unpack.
"""
__tracebackhide__ = True
- from setuptools_scm import Configuration
+ from vcs_versioning._config import Configuration
if self.parse is None:
raise RuntimeError(
diff --git a/vcs-versioning/src/vcs_versioning/_toml.py b/vcs-versioning/src/vcs_versioning/_toml.py
new file mode 100644
index 00000000..7b03c792
--- /dev/null
+++ b/vcs-versioning/src/vcs_versioning/_toml.py
@@ -0,0 +1,126 @@
+from __future__ import annotations
+
+import logging
+import sys
+from collections.abc import Callable
+from pathlib import Path
+from typing import Any, TypeAlias, TypedDict, TypeVar, cast, get_type_hints
+
+if sys.version_info >= (3, 11):
+ from tomllib import loads as load_toml
+else:
+ from tomli import loads as load_toml
+
+
+log = logging.getLogger(__name__)
+
+TOML_RESULT: TypeAlias = dict[str, Any]
+TOML_LOADER: TypeAlias = Callable[[str], TOML_RESULT]
+
+# TypeVar for generic TypedDict support - the schema defines the return type
+TSchema = TypeVar("TSchema", bound=TypedDict) # type: ignore[valid-type]
+
+
+class InvalidTomlError(ValueError):
+ """Raised when TOML data cannot be parsed."""
+
+
+class InvalidTomlSchemaError(ValueError):
+ """Raised when TOML data does not conform to the expected schema."""
+
+
+def read_toml_content(path: Path, default: TOML_RESULT | None = None) -> TOML_RESULT:
+ try:
+ data = path.read_text(encoding="utf-8")
+ except FileNotFoundError:
+ if default is None:
+ raise
+ else:
+ log.debug("%s missing, presuming default %r", path, default)
+ return default
+ else:
+ try:
+ return load_toml(data)
+ except Exception as e: # tomllib/tomli raise different decode errors
+ raise InvalidTomlError(f"Invalid TOML in {path}") from e
+
+
+class _CheatTomlData(TypedDict):
+ cheat: dict[str, Any]
+
+
+def _validate_against_schema(
+ data: dict[str, Any],
+ schema: type[TypedDict] | None, # type: ignore[valid-type]
+) -> dict[str, Any]:
+ """Validate parsed TOML data against a TypedDict schema.
+
+ Args:
+ data: Parsed TOML data to validate
+ schema: TypedDict class defining valid fields, or None to skip validation
+
+ Returns:
+ The validated data with invalid fields removed
+
+ Raises:
+ InvalidTomlSchemaError: If there are invalid fields (after logging warnings)
+ """
+ if schema is None:
+ return data
+
+ # Extract valid field names from the TypedDict
+ try:
+ valid_fields = frozenset(get_type_hints(schema).keys())
+ except NameError as e:
+ # If type hints can't be resolved, log warning and skip validation
+ log.warning("Could not resolve type hints for schema validation: %s", e)
+ return data
+
+ # If the schema has no fields (empty TypedDict), skip validation
+ if not valid_fields:
+ return data
+
+ invalid_fields = set(data.keys()) - valid_fields
+ if invalid_fields:
+ log.warning(
+ "Invalid fields in TOML data: %s. Valid fields are: %s",
+ sorted(invalid_fields),
+ sorted(valid_fields),
+ )
+ # Remove invalid fields
+ validated_data = {k: v for k, v in data.items() if k not in invalid_fields}
+ return validated_data
+
+ return data
+
+
+def load_toml_or_inline_map(data: str | None, *, schema: type[TSchema]) -> TSchema:
+ """Load toml data - with a special hack if only a inline map is given.
+
+ Args:
+ data: TOML string to parse, or None for empty dict
+ schema: TypedDict class for schema validation.
+ Invalid fields will be logged as warnings and removed.
+
+ Returns:
+ Parsed TOML data as a dictionary conforming to the schema type
+
+ Raises:
+ InvalidTomlError: If the TOML content is malformed
+ """
+ if not data:
+ return {} # type: ignore[return-value]
+ try:
+ if data[0] == "{":
+ data = "cheat=" + data
+ loaded: _CheatTomlData = cast(_CheatTomlData, load_toml(data))
+ result = loaded["cheat"]
+ else:
+ result = load_toml(data)
+
+ return _validate_against_schema(result, schema) # type: ignore[return-value]
+ except Exception as e: # tomllib/tomli raise different decode errors
+ # Don't re-wrap our own validation errors
+ if isinstance(e, InvalidTomlSchemaError):
+ raise
+ raise InvalidTomlError("Invalid TOML content") from e
diff --git a/vcs-versioning/src/vcs_versioning/_types.py b/vcs-versioning/src/vcs_versioning/_types.py
new file mode 100644
index 00000000..87769927
--- /dev/null
+++ b/vcs-versioning/src/vcs_versioning/_types.py
@@ -0,0 +1,28 @@
+from __future__ import annotations
+
+from collections.abc import Callable, Sequence
+from typing import TYPE_CHECKING, TypeAlias
+
+if TYPE_CHECKING:
+ from ._scm_version import ScmVersion
+
+# Re-export from _compat for backward compatibility
+from ._compat import PathT as PathT # noqa: PLC0414
+
+__all__ = [
+ "PathT",
+ "CMD_TYPE",
+ "VERSION_SCHEME",
+ "VERSION_SCHEMES",
+ "SCMVERSION",
+ "GIT_PRE_PARSE",
+]
+
+CMD_TYPE: TypeAlias = Sequence[PathT] | str
+
+VERSION_SCHEME: TypeAlias = str | Callable[["ScmVersion"], str]
+VERSION_SCHEMES: TypeAlias = list[str] | tuple[str, ...] | VERSION_SCHEME
+SCMVERSION: TypeAlias = "ScmVersion"
+
+# Git pre-parse function types
+GIT_PRE_PARSE: TypeAlias = str | None
diff --git a/src/setuptools_scm/_version_cls.py b/vcs-versioning/src/vcs_versioning/_version_cls.py
similarity index 88%
rename from src/setuptools_scm/_version_cls.py
rename to vcs-versioning/src/vcs_versioning/_version_cls.py
index e0fe387b..ff362e71 100644
--- a/src/setuptools_scm/_version_cls.py
+++ b/vcs-versioning/src/vcs_versioning/_version_cls.py
@@ -1,22 +1,21 @@
from __future__ import annotations
-from typing import Type
-from typing import Union
-from typing import cast
+import logging
+from typing import TypeAlias, cast
try:
from packaging.version import InvalidVersion
from packaging.version import Version as Version
except ImportError:
- from setuptools.extern.packaging.version import ( # type: ignore[import-not-found, no-redef]
+ from setuptools.extern.packaging.version import ( # type: ignore[import-not-found,no-redef]
InvalidVersion,
)
from setuptools.extern.packaging.version import ( # type: ignore[no-redef]
Version as Version,
)
-from . import _log
-log = _log.log.getChild("version_cls")
+
+log = logging.getLogger(__name__)
class NonNormalizedVersion(Version):
@@ -68,7 +67,7 @@ def _version_as_tuple(version_str: str) -> tuple[int | str, ...]:
return version_fields
-_VersionT = Union[Version, NonNormalizedVersion]
+_Version: TypeAlias = Version | NonNormalizedVersion
def import_name(name: str) -> object:
@@ -80,8 +79,8 @@ def import_name(name: str) -> object:
def _validate_version_cls(
- version_cls: type[_VersionT] | str | None, normalize: bool
-) -> type[_VersionT]:
+ version_cls: type[_Version] | str | None, normalize: bool
+) -> type[_Version]:
if not normalize:
if version_cls is not None:
raise ValueError(
@@ -94,7 +93,7 @@ def _validate_version_cls(
return Version
elif isinstance(version_cls, str):
try:
- return cast(Type[_VersionT], import_name(version_cls))
+ return cast(type[_Version], import_name(version_cls))
except Exception:
raise ValueError(f"Unable to import version_cls='{version_cls}'") from None
else:
diff --git a/vcs-versioning/src/vcs_versioning/_version_inference.py b/vcs-versioning/src/vcs_versioning/_version_inference.py
new file mode 100644
index 00000000..db4068a0
--- /dev/null
+++ b/vcs-versioning/src/vcs_versioning/_version_inference.py
@@ -0,0 +1,52 @@
+"""Core version inference functionality for build tool integrations."""
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING, Any
+
+if TYPE_CHECKING:
+ from ._pyproject_reading import PyProjectData
+
+
+def infer_version_string(
+ dist_name: str | None,
+ pyproject_data: PyProjectData,
+ overrides: dict[str, Any] | None = None,
+ *,
+ force_write_version_files: bool = False,
+) -> str:
+ """
+ Compute the inferred version string from the given inputs.
+
+ This is a pure helper that avoids requiring build-tool specific
+ distribution objects, making it easier to test and reuse across
+ different build systems.
+
+ Parameters:
+ dist_name: Optional distribution name (used for overrides and env scoping)
+ pyproject_data: Parsed PyProjectData (may be constructed via for_testing())
+ overrides: Optional override configuration (same keys as [tool.setuptools_scm])
+ force_write_version_files: When True, apply write_to/version_file effects
+
+ Returns:
+ The computed version string.
+
+ Raises:
+ SystemExit: If version cannot be determined (via _version_missing)
+ """
+ from ._config import Configuration
+ from ._get_version_impl import _get_version, _version_missing
+
+ config = Configuration.from_file(
+ dist_name=dist_name, pyproject_data=pyproject_data, **(overrides or {})
+ )
+
+ maybe_version = _get_version(
+ config, force_write_version_files=force_write_version_files
+ )
+ if maybe_version is None:
+ _version_missing(config)
+ return maybe_version
+
+
+__all__ = ["infer_version_string"]
diff --git a/vcs-versioning/src/vcs_versioning/_version_schemes/__init__.py b/vcs-versioning/src/vcs_versioning/_version_schemes/__init__.py
new file mode 100644
index 00000000..ccf73d8c
--- /dev/null
+++ b/vcs-versioning/src/vcs_versioning/_version_schemes/__init__.py
@@ -0,0 +1,123 @@
+"""Version schemes package for setuptools-scm.
+
+This package contains all version and local schemes that determine how
+version numbers are calculated and formatted from SCM metadata.
+"""
+
+from __future__ import annotations
+
+import logging
+
+from .. import _entrypoints
+from .._scm_version import ScmVersion, callable_or_entrypoint, meta, tag_to_version
+from ._common import (
+ SEMVER_LEN,
+ SEMVER_MINOR,
+ SEMVER_PATCH,
+ combine_version_with_local_parts,
+)
+from ._standard import (
+ calver_by_date,
+ date_ver_match,
+ get_local_dirty_tag,
+ get_local_node_and_date,
+ get_local_node_and_timestamp,
+ get_no_local_node,
+ guess_next_date_ver,
+ guess_next_dev_version,
+ guess_next_simple_semver,
+ guess_next_version,
+ no_guess_dev_version,
+ only_version,
+ postrelease_version,
+ release_branch_semver,
+ release_branch_semver_version,
+ simplified_semver_version,
+)
+from ._towncrier import version_from_fragments
+
+log = logging.getLogger(__name__)
+
+__all__ = [
+ # Constants
+ "SEMVER_LEN",
+ "SEMVER_MINOR",
+ "SEMVER_PATCH",
+ # Core types and utilities
+ "ScmVersion",
+ "meta",
+ "tag_to_version",
+ "callable_or_entrypoint",
+ "format_version",
+ # Version schemes
+ "guess_next_version",
+ "guess_next_dev_version",
+ "guess_next_simple_semver",
+ "simplified_semver_version",
+ "release_branch_semver_version",
+ "release_branch_semver", # deprecated
+ "only_version",
+ "no_guess_dev_version",
+ "calver_by_date",
+ "date_ver_match",
+ "guess_next_date_ver",
+ "postrelease_version",
+ # Local schemes
+ "get_local_node_and_date",
+ "get_local_node_and_timestamp",
+ "get_local_dirty_tag",
+ "get_no_local_node",
+ # Towncrier
+ "version_from_fragments",
+ # Utilities
+ "combine_version_with_local_parts",
+]
+
+
+def format_version(version: ScmVersion) -> str:
+ """Format a ScmVersion into a final version string.
+
+ This orchestrates calling the version scheme and local scheme,
+ then combining them with any local data from the original tag.
+
+ Args:
+ version: The ScmVersion to format
+
+ Returns:
+ A fully formatted version string
+ """
+ log.debug("scm version %s", version)
+ log.debug("config %s", version.config)
+ if version.preformatted:
+ return str(version.tag)
+
+ # Extract original tag's local data for later combination
+ original_local = ""
+ if hasattr(version.tag, "local") and version.tag.local is not None:
+ original_local = str(version.tag.local)
+
+ # Create a patched ScmVersion with only the base version (no local data) for version schemes
+ from dataclasses import replace
+
+ # Extract the base version (public part) from the tag using config's version_cls
+ base_version_str = str(version.tag.public)
+ base_tag = version.config.version_cls(base_version_str)
+ version_for_scheme = replace(version, tag=base_tag)
+
+ main_version = _entrypoints._call_version_scheme(
+ version_for_scheme,
+ "setuptools_scm.version_scheme",
+ version.config.version_scheme,
+ )
+ log.debug("version %s", main_version)
+ assert main_version is not None
+
+ local_version = _entrypoints._call_version_scheme(
+ version, "setuptools_scm.local_scheme", version.config.local_scheme, "+unknown"
+ )
+ log.debug("local_version %s", local_version)
+
+ # Combine main version with original local data and new local scheme data
+ return combine_version_with_local_parts(
+ str(main_version), original_local, local_version
+ )
diff --git a/vcs-versioning/src/vcs_versioning/_version_schemes/_common.py b/vcs-versioning/src/vcs_versioning/_version_schemes/_common.py
new file mode 100644
index 00000000..b544b5fb
--- /dev/null
+++ b/vcs-versioning/src/vcs_versioning/_version_schemes/_common.py
@@ -0,0 +1,62 @@
+"""Common utilities shared across version schemes."""
+
+from __future__ import annotations
+
+# Semantic versioning constants
+SEMVER_MINOR = 2
+SEMVER_PATCH = 3
+SEMVER_LEN = 3
+
+
+def combine_version_with_local_parts(
+ main_version: str, *local_parts: str | None
+) -> str:
+ """
+ Combine a main version with multiple local parts into a valid PEP 440 version string.
+ Handles deduplication of local parts to avoid adding the same local data twice.
+
+ Args:
+ main_version: The main version string (e.g., "1.2.0", "1.2.dev3")
+ *local_parts: Variable number of local version parts, can be None or empty
+
+ Returns:
+ A valid PEP 440 version string
+
+ Examples:
+ combine_version_with_local_parts("1.2.0", "build.123", "d20090213") -> "1.2.0+build.123.d20090213"
+ combine_version_with_local_parts("1.2.0", "build.123", None) -> "1.2.0+build.123"
+ combine_version_with_local_parts("1.2.0+build.123", "d20090213") -> "1.2.0+build.123.d20090213"
+ combine_version_with_local_parts("1.2.0+build.123", "build.123") -> "1.2.0+build.123" # no duplication
+ combine_version_with_local_parts("1.2.0", None, None) -> "1.2.0"
+ """
+ # Split main version into base and existing local parts
+ if "+" in main_version:
+ main_part, existing_local = main_version.split("+", 1)
+ all_local_parts = existing_local.split(".")
+ else:
+ main_part = main_version
+ all_local_parts = []
+
+ # Process each new local part
+ for part in local_parts:
+ if not part or not part.strip():
+ continue
+
+ # Strip any leading + and split into segments
+ clean_part = part.strip("+")
+ if not clean_part:
+ continue
+
+ # Split multi-part local identifiers (e.g., "build.123" -> ["build", "123"])
+ part_segments = clean_part.split(".")
+
+ # Add each segment if not already present
+ for segment in part_segments:
+ if segment and segment not in all_local_parts:
+ all_local_parts.append(segment)
+
+ # Return combined result
+ if all_local_parts:
+ return main_part + "+" + ".".join(all_local_parts)
+ else:
+ return main_part
diff --git a/vcs-versioning/src/vcs_versioning/_version_schemes/_standard.py b/vcs-versioning/src/vcs_versioning/_version_schemes/_standard.py
new file mode 100644
index 00000000..b9656ce9
--- /dev/null
+++ b/vcs-versioning/src/vcs_versioning/_version_schemes/_standard.py
@@ -0,0 +1,237 @@
+"""Standard version and local schemes for setuptools-scm.
+
+This module contains the built-in version schemes and local schemes that determine
+how version numbers are calculated and formatted.
+"""
+
+from __future__ import annotations
+
+import logging
+import re
+import warnings
+from datetime import date, datetime, timedelta, timezone
+from re import Match
+from typing import TYPE_CHECKING
+
+from .. import _modify_version
+from .._scm_version import ScmVersion, _parse_version_tag
+from .._version_cls import Version as PkgVersion
+from ._common import SEMVER_LEN, SEMVER_MINOR, SEMVER_PATCH
+
+if TYPE_CHECKING:
+ pass
+
+log = logging.getLogger(__name__)
+
+
+# Version Schemes
+# ----------------
+
+
+def guess_next_version(tag_version: ScmVersion) -> str:
+ version = _modify_version.strip_local(str(tag_version.tag))
+ return _modify_version._bump_dev(version) or _modify_version._bump_regex(version)
+
+
+def guess_next_dev_version(version: ScmVersion) -> str:
+ if version.exact:
+ return version.format_with("{tag}")
+ else:
+ return version.format_next_version(guess_next_version)
+
+
+def guess_next_simple_semver(
+ version: ScmVersion, retain: int, increment: bool = True
+) -> str:
+ parts = list(version.tag.release[:retain])
+ while len(parts) < retain:
+ parts.append(0)
+ if increment:
+ parts[-1] += 1
+ while len(parts) < SEMVER_LEN:
+ parts.append(0)
+ return ".".join(str(i) for i in parts)
+
+
+def simplified_semver_version(version: ScmVersion) -> str:
+ if version.exact:
+ return guess_next_simple_semver(version, retain=SEMVER_LEN, increment=False)
+ elif version.branch is not None and "feature" in version.branch:
+ return version.format_next_version(
+ guess_next_simple_semver, retain=SEMVER_MINOR
+ )
+ else:
+ return version.format_next_version(
+ guess_next_simple_semver, retain=SEMVER_PATCH
+ )
+
+
+def release_branch_semver_version(version: ScmVersion) -> str:
+ if version.exact:
+ return version.format_with("{tag}")
+ if version.branch is not None:
+ # Does the branch name (stripped of namespace) parse as a version?
+ branch_ver_data = _parse_version_tag(
+ version.branch.split("/")[-1], version.config
+ )
+ if branch_ver_data is not None:
+ branch_ver = branch_ver_data["version"]
+ if branch_ver[0] == "v":
+ # Allow branches that start with 'v', similar to Version.
+ branch_ver = branch_ver[1:]
+ # Does the branch version up to the minor part match the tag? If not it
+ # might be like, an issue number or something and not a version number, so
+ # we only want to use it if it matches.
+ tag_ver_up_to_minor = str(version.tag).split(".")[:SEMVER_MINOR]
+ branch_ver_up_to_minor = branch_ver.split(".")[:SEMVER_MINOR]
+ if branch_ver_up_to_minor == tag_ver_up_to_minor:
+ # We're in a release/maintenance branch, next is a patch/rc/beta bump:
+ return version.format_next_version(guess_next_version)
+ # We're in a development branch, next is a minor bump:
+ return version.format_next_version(guess_next_simple_semver, retain=SEMVER_MINOR)
+
+
+def release_branch_semver(version: ScmVersion) -> str:
+ warnings.warn(
+ "release_branch_semver is deprecated and will be removed in the future. "
+ "Use release_branch_semver_version instead",
+ category=DeprecationWarning,
+ stacklevel=2,
+ )
+ return release_branch_semver_version(version)
+
+
+def only_version(version: ScmVersion) -> str:
+ return version.format_with("{tag}")
+
+
+def no_guess_dev_version(version: ScmVersion) -> str:
+ if version.exact:
+ return version.format_with("{tag}")
+ else:
+ return version.format_next_version(_modify_version._dont_guess_next_version)
+
+
+_DATE_REGEX = re.compile(
+ r"""
+ ^(?P
+ (?P[vV]?)
+ (?P\d{2}|\d{4})(?:\.\d{1,2}){2})
+ (?:\.(?P\d*))?$
+ """,
+ re.VERBOSE,
+)
+
+
+def date_ver_match(ver: str) -> Match[str] | None:
+ return _DATE_REGEX.match(ver)
+
+
+def guess_next_date_ver(
+ version: ScmVersion,
+ node_date: date | None = None,
+ date_fmt: str | None = None,
+ version_cls: type | None = None,
+) -> str:
+ """
+ same-day -> patch +1
+ other-day -> today
+
+ distance is always added as .devX
+ """
+ match = date_ver_match(str(version.tag))
+ if match is None:
+ warnings.warn(
+ f"{version} does not correspond to a valid versioning date, "
+ "assuming legacy version",
+ stacklevel=2,
+ )
+ if date_fmt is None:
+ date_fmt = "%y.%m.%d"
+ else:
+ # deduct date format if not provided
+ if date_fmt is None:
+ date_fmt = "%Y.%m.%d" if len(match.group("year")) == 4 else "%y.%m.%d"
+ if prefix := match.group("prefix"):
+ if not date_fmt.startswith(prefix):
+ date_fmt = prefix + date_fmt
+
+ today = version.time.date()
+ head_date = node_date or today
+ # compute patch
+ if match is None:
+ # For legacy non-date tags, always use patch=0 (treat as "other day")
+ # Use yesterday to ensure tag_date != head_date
+ tag_date = head_date - timedelta(days=1)
+ else:
+ tag_date = (
+ datetime.strptime(match.group("date"), date_fmt)
+ .replace(tzinfo=timezone.utc)
+ .date()
+ )
+ if tag_date == head_date:
+ assert match is not None
+ # Same day as existing date tag - increment patch
+ patch = int(match.group("patch") or "0") + 1
+ else:
+ # Different day or legacy non-date tag - use patch 0
+ if tag_date > head_date and match is not None:
+ # warn on future times (only for actual date tags, not legacy)
+ warnings.warn(
+ f"your previous tag ({tag_date}) is ahead your node date ({head_date})",
+ stacklevel=2,
+ )
+ patch = 0
+ next_version = "{node_date:{date_fmt}}.{patch}".format(
+ node_date=head_date, date_fmt=date_fmt, patch=patch
+ )
+ # rely on the Version object to ensure consistency (e.g. remove leading 0s)
+ if version_cls is None:
+ version_cls = PkgVersion
+ next_version = str(version_cls(next_version))
+ return next_version
+
+
+def calver_by_date(version: ScmVersion) -> str:
+ if version.exact and not version.dirty:
+ return version.format_with("{tag}")
+ # TODO: move the release-X check to a new scheme
+ if version.branch is not None and version.branch.startswith("release-"):
+ branch_ver = _parse_version_tag(version.branch.split("-")[-1], version.config)
+ if branch_ver is not None:
+ ver = branch_ver["version"]
+ match = date_ver_match(ver)
+ if match:
+ return ver
+ return version.format_next_version(
+ guess_next_date_ver,
+ node_date=version.node_date,
+ version_cls=version.config.version_cls,
+ )
+
+
+def postrelease_version(version: ScmVersion) -> str:
+ if version.exact:
+ return version.format_with("{tag}")
+ else:
+ return version.format_with("{tag}.post{distance}")
+
+
+# Local Schemes
+# -------------
+
+
+def get_local_node_and_date(version: ScmVersion) -> str:
+ return _modify_version._format_local_with_time(version, time_format="%Y%m%d")
+
+
+def get_local_node_and_timestamp(version: ScmVersion) -> str:
+ return _modify_version._format_local_with_time(version, time_format="%Y%m%d%H%M%S")
+
+
+def get_local_dirty_tag(version: ScmVersion) -> str:
+ return version.format_choice("", "+dirty")
+
+
+def get_no_local_node(version: ScmVersion) -> str:
+ return ""
diff --git a/vcs-versioning/src/vcs_versioning/_version_schemes/_towncrier.py b/vcs-versioning/src/vcs_versioning/_version_schemes/_towncrier.py
new file mode 100644
index 00000000..f15a5e05
--- /dev/null
+++ b/vcs-versioning/src/vcs_versioning/_version_schemes/_towncrier.py
@@ -0,0 +1,165 @@
+"""Version scheme based on towncrier changelog fragments.
+
+This version scheme analyzes changelog fragments in the changelog.d/ directory
+to determine the appropriate version bump:
+- Major bump: if 'removal' fragments are present
+- Minor bump: if 'feature' or 'deprecation' fragments are present
+- Patch bump: if only 'bugfix', 'doc', or 'misc' fragments are present
+
+Falls back to guess-next-dev if no fragments are found.
+"""
+
+from __future__ import annotations
+
+import logging
+from pathlib import Path
+
+from .._scm_version import ScmVersion
+from ._common import SEMVER_MINOR, SEMVER_PATCH
+from ._standard import guess_next_dev_version, guess_next_simple_semver
+
+log = logging.getLogger(__name__)
+
+# Fragment types that indicate different version bumps
+MAJOR_FRAGMENT_TYPES = {"removal"}
+MINOR_FRAGMENT_TYPES = {"feature", "deprecation"}
+PATCH_FRAGMENT_TYPES = {"bugfix", "doc", "misc"}
+
+ALL_FRAGMENT_TYPES = MAJOR_FRAGMENT_TYPES | MINOR_FRAGMENT_TYPES | PATCH_FRAGMENT_TYPES
+
+
+def _find_fragments(
+ root: Path, changelog_dir: str = "changelog.d"
+) -> dict[str, list[str]]:
+ """Find and categorize changelog fragments.
+
+ Args:
+ root: Root directory to search from
+ changelog_dir: Name of the changelog directory
+
+ Returns:
+ Dictionary mapping fragment types to lists of fragment filenames
+ """
+ fragments: dict[str, list[str]] = {ftype: [] for ftype in ALL_FRAGMENT_TYPES}
+
+ changelog_path = root / changelog_dir
+ if not changelog_path.exists():
+ log.debug("No changelog directory found at %s", changelog_path)
+ return fragments
+
+ for entry in changelog_path.iterdir():
+ if not entry.is_file():
+ continue
+
+ # Skip template, README, and .gitkeep files
+ if entry.name in ("template.md", "README.md", ".gitkeep"):
+ continue
+
+ # Fragment naming: {number}.{type}.md
+ parts = entry.name.split(".")
+ if len(parts) >= 2:
+ fragment_type = parts[1]
+ if fragment_type in ALL_FRAGMENT_TYPES:
+ fragments[fragment_type].append(entry.name)
+ log.debug("Found %s fragment: %s", fragment_type, entry.name)
+
+ return fragments
+
+
+def _determine_bump_type(fragments: dict[str, list[str]]) -> str | None:
+ """Determine version bump type from fragments.
+
+ Returns:
+ 'major', 'minor', 'patch', or None if no fragments found
+ """
+ # Check for any fragments at all
+ total_fragments = sum(len(files) for files in fragments.values())
+ if total_fragments == 0:
+ return None
+
+ # Major bump if any removal fragments
+ if any(fragments[ftype] for ftype in MAJOR_FRAGMENT_TYPES):
+ return "major"
+
+ # Minor bump if any feature/deprecation fragments
+ if any(fragments[ftype] for ftype in MINOR_FRAGMENT_TYPES):
+ return "minor"
+
+ # Patch bump for other fragments
+ if any(fragments[ftype] for ftype in PATCH_FRAGMENT_TYPES):
+ return "patch"
+
+ return None
+
+
+def version_from_fragments(version: ScmVersion) -> str:
+ """Version scheme that determines version from towncrier fragments.
+
+ This is the main entry point registered as a setuptools_scm version scheme.
+
+ Args:
+ version: ScmVersion object from VCS
+
+ Returns:
+ Formatted version string
+ """
+ # If we're exactly on a tag, return it
+ if version.exact:
+ return version.format_with("{tag}")
+
+ # Find where to look for changelog.d/ directory
+ # Prefer relative_to (location of config file) for monorepo support
+ # This allows changelog.d/ to be in the project dir rather than repo root
+ if version.config.relative_to:
+ # relative_to is typically the pyproject.toml file path
+ # changelog.d/ should be in the same directory
+ import os
+
+ if os.path.isfile(version.config.relative_to):
+ root = Path(os.path.dirname(version.config.relative_to))
+ else:
+ root = Path(version.config.relative_to)
+ else:
+ # When no relative_to is set, use absolute_root (the VCS root)
+ root = Path(version.config.absolute_root)
+
+ log.debug("Analyzing fragments in %s", root)
+
+ # Find and analyze fragments
+ fragments = _find_fragments(root)
+ bump_type = _determine_bump_type(fragments)
+
+ if bump_type is None:
+ log.debug("No fragments found, falling back to guess-next-dev")
+ return guess_next_dev_version(version)
+
+ log.info("Determined version bump type from fragments: %s", bump_type)
+
+ # Determine the next version based on bump type
+ if bump_type == "major":
+ # Major bump: increment major version, reset minor and patch to 0
+ from .. import _modify_version
+
+ def guess_next_major(v: ScmVersion) -> str:
+ tag_version = _modify_version.strip_local(str(v.tag))
+ parts = tag_version.split(".")
+ if len(parts) >= 1:
+ major = int(parts[0].lstrip("v")) # Handle 'v' prefix
+ return f"{major + 1}.0.0"
+ # Fallback to bump_dev
+ bumped = _modify_version._bump_dev(tag_version)
+ return bumped if bumped is not None else f"{tag_version}.dev0"
+
+ return version.format_next_version(guess_next_major)
+
+ elif bump_type == "minor":
+ # Minor bump: use simplified semver with MINOR retention
+ return version.format_next_version(
+ guess_next_simple_semver, retain=SEMVER_MINOR
+ )
+
+ else: # patch
+ # Patch bump: use simplified semver with PATCH retention
+ return version.format_next_version(
+ guess_next_simple_semver, retain=SEMVER_PATCH
+ )
diff --git a/vcs-versioning/src/vcs_versioning/overrides.py b/vcs-versioning/src/vcs_versioning/overrides.py
new file mode 100644
index 00000000..9b1e55e5
--- /dev/null
+++ b/vcs-versioning/src/vcs_versioning/overrides.py
@@ -0,0 +1,719 @@
+"""
+Environment variable overrides API for VCS versioning.
+
+This module provides tools for managing environment variable overrides
+in a structured way, with support for custom tool prefixes and fallback
+to VCS_VERSIONING_* variables.
+
+Example usage:
+ >>> from vcs_versioning.overrides import GlobalOverrides
+ >>>
+ >>> # Apply overrides for the entire execution scope
+ >>> with GlobalOverrides.from_env("HATCH_VCS"):
+ >>> version = get_version(...)
+
+See the integrators documentation for more details.
+"""
+
+from __future__ import annotations
+
+import logging
+import os
+import warnings
+from collections.abc import Mapping, MutableMapping
+from contextlib import ContextDecorator
+from contextvars import ContextVar
+from dataclasses import dataclass
+from datetime import datetime
+from typing import TYPE_CHECKING, Any, Literal, TypedDict, TypeVar, overload
+
+from packaging.utils import canonicalize_name
+
+from ._overrides import (
+ _find_close_env_var_matches,
+ _search_env_vars_with_prefix,
+)
+from ._toml import load_toml_or_inline_map
+
+# TypeVar for generic TypedDict support
+TSchema = TypeVar("TSchema", bound=TypedDict) # type: ignore[valid-type]
+
+if TYPE_CHECKING:
+ from pytest import MonkeyPatch
+
+log = logging.getLogger(__name__)
+
+
+class EnvReader:
+ """Helper class to read environment variables with tool prefix fallback.
+
+ This class provides a structured way to read environment variables by trying
+ multiple tool prefixes in order, with support for distribution-specific variants.
+
+ Attributes:
+ tools_names: Tuple of tool prefixes to try in order (e.g., ("HATCH_VCS", "VCS_VERSIONING"))
+ env: Environment mapping to read from
+ dist_name: Optional distribution name for dist-specific env vars
+
+ Example:
+ >>> reader = EnvReader(
+ ... tools_names=("HATCH_VCS", "VCS_VERSIONING"),
+ ... env=os.environ,
+ ... dist_name="my-package"
+ ... )
+ >>> debug_val = reader.read("DEBUG") # tries HATCH_VCS_DEBUG, then VCS_VERSIONING_DEBUG
+ >>> pretend = reader.read("PRETEND_VERSION") # tries dist-specific first, then generic
+ """
+
+ tools_names: tuple[str, ...]
+ env: Mapping[str, str]
+ dist_name: str | None
+
+ def __init__(
+ self,
+ tools_names: tuple[str, ...],
+ env: Mapping[str, str],
+ dist_name: str | None = None,
+ ):
+ """Initialize the EnvReader.
+
+ Args:
+ tools_names: Tuple of tool prefixes to try in order (e.g., ("HATCH_VCS", "VCS_VERSIONING"))
+ env: Environment mapping to read from
+ dist_name: Optional distribution name for dist-specific variables
+ """
+ if not tools_names:
+ raise TypeError("tools_names must be a non-empty tuple")
+ self.tools_names = tools_names
+ self.env = env
+ self.dist_name = dist_name
+
+ @overload
+ def read(self, name: str, *, split: str) -> list[str]: ...
+
+ @overload
+ def read(self, name: str, *, split: str, default: list[str]) -> list[str]: ...
+
+ @overload
+ def read(self, name: str, *, default: str) -> str: ...
+
+ @overload
+ def read(self, name: str) -> str | None: ...
+
+ def read(
+ self, name: str, *, split: str | None = None, default: Any = None
+ ) -> str | list[str] | None:
+ """Read a named environment variable, trying each tool in tools_names order.
+
+ If dist_name is provided, tries distribution-specific variants first
+ (e.g., TOOL_NAME_FOR_DIST), then falls back to generic variants (e.g., TOOL_NAME).
+
+ Also provides helpful diagnostics when similar environment variables are found
+ but don't match exactly (e.g., typos or incorrect normalizations in distribution names).
+
+ Args:
+ name: The environment variable name component (e.g., "DEBUG", "PRETEND_VERSION")
+ split: Optional separator to split the value by (e.g., os.pathsep for path lists)
+ default: Default value to return if not found (defaults to None)
+
+ Returns:
+ - If split is provided and value found: list[str] of split values
+ - If split is provided and not found: default value
+ - If split is None and value found: str value
+ - If split is None and not found: default value
+ """
+ # If dist_name is provided, try dist-specific variants first
+ found_value: str | None = None
+ if self.dist_name is not None:
+ canonical_dist_name = canonicalize_name(self.dist_name)
+ env_var_dist_name = canonical_dist_name.replace("-", "_").upper()
+
+ # Try each tool's dist-specific variant
+ for tool in self.tools_names:
+ expected_env_var = f"{tool}_{name}_FOR_{env_var_dist_name}"
+ val = self.env.get(expected_env_var)
+ if val is not None:
+ found_value = val
+ break
+
+ # Try generic versions for each tool
+ if found_value is None:
+ for tool in self.tools_names:
+ val = self.env.get(f"{tool}_{name}")
+ if val is not None:
+ found_value = val
+ break
+
+ # Not found - if dist_name is provided, check for common mistakes
+ if found_value is None and self.dist_name is not None:
+ canonical_dist_name = canonicalize_name(self.dist_name)
+ env_var_dist_name = canonical_dist_name.replace("-", "_").upper()
+
+ # Try each tool prefix for fuzzy matching
+ for tool in self.tools_names:
+ expected_env_var = f"{tool}_{name}_FOR_{env_var_dist_name}"
+ prefix = f"{tool}_{name}_FOR_"
+
+ # Search for alternative normalizations
+ matches = _search_env_vars_with_prefix(prefix, self.dist_name, self.env)
+ if matches:
+ env_var_name, value = matches[0]
+ log.warning(
+ "Found environment variable '%s' for dist name '%s', "
+ "but expected '%s'. Consider using the standard normalized name.",
+ env_var_name,
+ self.dist_name,
+ expected_env_var,
+ )
+ if len(matches) > 1:
+ other_vars = [var for var, _ in matches[1:]]
+ log.warning(
+ "Multiple alternative environment variables found: %s. Using '%s'.",
+ other_vars,
+ env_var_name,
+ )
+ found_value = value
+ break
+
+ # Search for close matches (potential typos)
+ close_matches = _find_close_env_var_matches(
+ prefix, env_var_dist_name, self.env
+ )
+ if close_matches:
+ log.warning(
+ "Environment variable '%s' not found for dist name '%s' "
+ "(canonicalized as '%s'). Did you mean one of these? %s",
+ expected_env_var,
+ self.dist_name,
+ canonical_dist_name,
+ close_matches,
+ )
+
+ # Process the found value or return default
+ if found_value is not None:
+ if split is not None:
+ # Split the value by the provided separator, filtering out empty strings
+ return [part for part in found_value.split(split) if part]
+ return found_value
+ # Return default, honoring the type based on split parameter
+ if split is not None:
+ # When split is provided, default should be a list
+ return default if default is not None else []
+ # For non-split case, default can be None or str
+ return default # type: ignore[no-any-return]
+
+ def read_toml(self, name: str, *, schema: type[TSchema]) -> TSchema:
+ """Read and parse a TOML-formatted environment variable.
+
+ This method is useful for reading structured configuration like:
+ - Config overrides (e.g., TOOL_OVERRIDES_FOR_DIST)
+ - ScmVersion metadata (e.g., TOOL_PRETEND_METADATA_FOR_DIST)
+
+ Supports both full TOML documents and inline TOML maps (starting with '{').
+
+ Args:
+ name: The environment variable name component (e.g., "OVERRIDES", "PRETEND_METADATA")
+ schema: TypedDict class for schema validation.
+ Invalid fields will be logged as warnings and removed.
+
+ Returns:
+ Parsed TOML data conforming to the schema type, or an empty dict if not found.
+ Raises InvalidTomlError if the TOML content is malformed.
+
+ Example:
+ >>> from typing import TypedDict
+ >>> class MySchema(TypedDict, total=False):
+ ... local_scheme: str
+ >>> reader = EnvReader(tools_names=("TOOL",), env={
+ ... "TOOL_OVERRIDES": '{"local_scheme": "no-local-version"}',
+ ... })
+ >>> result: MySchema = reader.read_toml("OVERRIDES", schema=MySchema)
+ >>> result["local_scheme"]
+ 'no-local-version'
+ """
+ data = self.read(name)
+ return load_toml_or_inline_map(data, schema=schema)
+
+
+@dataclass(frozen=True)
+class GlobalOverrides:
+ """Global environment variable overrides for VCS versioning.
+
+ Use as a context manager to apply overrides for the execution scope.
+ Logging is automatically configured when entering the context.
+
+ Attributes:
+ debug: Debug logging level (int from logging module) or False to disable
+ subprocess_timeout: Timeout for subprocess commands in seconds
+ hg_command: Command to use for Mercurial operations
+ source_date_epoch: Unix timestamp for reproducible builds (None if not set)
+ ignore_vcs_roots: List of VCS root paths to ignore for file finding
+ tool: Tool prefix used to read these overrides
+ dist_name: Optional distribution name for dist-specific env var lookups
+ additional_loggers: List of logger instances to configure alongside vcs_versioning
+
+ Usage:
+ with GlobalOverrides.from_env("HATCH_VCS", dist_name="my-package") as overrides:
+ # All modules now have access to these overrides
+ # Logging is automatically configured based on HATCH_VCS_DEBUG
+
+ # Read custom environment variables
+ custom_val = overrides.env_reader.read("MY_CUSTOM_VAR")
+
+ version = get_version(...)
+ """
+
+ debug: int | Literal[False]
+ subprocess_timeout: int
+ hg_command: str
+ source_date_epoch: int | None
+ ignore_vcs_roots: list[str]
+ tool: str
+ env_reader: EnvReader
+ dist_name: str | None = None
+ additional_loggers: tuple[logging.Logger, ...] = ()
+
+ def __post_init__(self) -> None:
+ """Validate that env_reader configuration matches GlobalOverrides settings."""
+ # Verify that the env_reader's dist_name matches
+ if self.env_reader.dist_name != self.dist_name:
+ raise ValueError(
+ f"EnvReader dist_name mismatch: "
+ f"GlobalOverrides has {self.dist_name!r}, "
+ f"but EnvReader has {self.env_reader.dist_name!r}"
+ )
+
+ # Verify that the env_reader has the correct tool prefix
+ expected_tools = (self.tool, "VCS_VERSIONING")
+ if self.env_reader.tools_names != expected_tools:
+ raise ValueError(
+ f"EnvReader tools_names mismatch: "
+ f"expected {expected_tools}, "
+ f"but got {self.env_reader.tools_names}"
+ )
+
+ @classmethod
+ def from_env(
+ cls,
+ tool: str,
+ env: Mapping[str, str] = os.environ,
+ dist_name: str | None = None,
+ additional_loggers: logging.Logger | list[logging.Logger] | tuple[()] = (),
+ ) -> GlobalOverrides:
+ """Read all global overrides from environment variables.
+
+ Checks both tool-specific prefix and VCS_VERSIONING prefix as fallback.
+
+ Args:
+ tool: Tool prefix (e.g., "HATCH_VCS", "SETUPTOOLS_SCM")
+ env: Environment dict to read from (defaults to os.environ)
+ dist_name: Optional distribution name for dist-specific env var lookups
+ additional_loggers: Logger instance(s) to configure alongside vcs_versioning.
+ Can be a single logger, a list of loggers, or empty tuple.
+
+ Returns:
+ GlobalOverrides instance ready to use as context manager
+ """
+
+ # Create EnvReader for reading environment variables with fallback
+ reader = EnvReader(
+ tools_names=(tool, "VCS_VERSIONING"), env=env, dist_name=dist_name
+ )
+
+ # Convert additional_loggers to a tuple of logger instances
+ logger_tuple: tuple[logging.Logger, ...]
+ if isinstance(additional_loggers, logging.Logger):
+ logger_tuple = (additional_loggers,)
+ elif isinstance(additional_loggers, list):
+ logger_tuple = tuple(additional_loggers)
+ else:
+ logger_tuple = ()
+
+ # Read debug flag - support multiple formats
+ debug_val = reader.read("DEBUG")
+ if debug_val is None:
+ debug: int | Literal[False] = False
+ else:
+ # Try to parse as integer log level
+ try:
+ parsed_int = int(debug_val)
+ # If it's a small integer (0, 1), treat as boolean flag
+ # Otherwise treat as explicit log level (10, 20, 30, etc.)
+ if parsed_int in (0, 1):
+ debug = logging.DEBUG if parsed_int else False
+ else:
+ debug = parsed_int
+ except ValueError:
+ # Not an integer - check if it's a level name (DEBUG, INFO, WARNING, etc.)
+ level_name = debug_val.upper()
+ level_value = getattr(logging, level_name, None)
+ if isinstance(level_value, int):
+ # Valid level name found
+ debug = level_value
+ else:
+ # Unknown value - treat as boolean flag (any non-empty value means DEBUG)
+ debug = logging.DEBUG
+
+ # Read subprocess timeout
+ timeout_val = reader.read("SUBPROCESS_TIMEOUT")
+ subprocess_timeout = 40 # default
+ if timeout_val is not None:
+ try:
+ subprocess_timeout = int(timeout_val)
+ except ValueError:
+ log.warning(
+ "Invalid SUBPROCESS_TIMEOUT value '%s', using default %d",
+ timeout_val,
+ subprocess_timeout,
+ )
+
+ # Read hg command
+ hg_command = reader.read("HG_COMMAND") or "hg"
+
+ # Read SOURCE_DATE_EPOCH (standard env var, no prefix)
+ source_date_epoch_val = env.get("SOURCE_DATE_EPOCH")
+ source_date_epoch: int | None = None
+ if source_date_epoch_val is not None:
+ try:
+ source_date_epoch = int(source_date_epoch_val)
+ except ValueError:
+ log.warning(
+ "Invalid SOURCE_DATE_EPOCH value '%s', ignoring",
+ source_date_epoch_val,
+ )
+
+ # Read ignore_vcs_roots - paths separated by os.pathsep
+ ignore_vcs_roots_raw = reader.read(
+ "IGNORE_VCS_ROOTS", split=os.pathsep, default=[]
+ )
+ ignore_vcs_roots = [os.path.normcase(p) for p in ignore_vcs_roots_raw]
+
+ return cls(
+ debug=debug,
+ subprocess_timeout=subprocess_timeout,
+ hg_command=hg_command,
+ source_date_epoch=source_date_epoch,
+ ignore_vcs_roots=ignore_vcs_roots,
+ tool=tool,
+ env_reader=reader,
+ dist_name=dist_name,
+ additional_loggers=logger_tuple,
+ )
+
+ def __enter__(self) -> GlobalOverrides:
+ """Enter context: set this as the active override and configure logging."""
+ token = _active_overrides.set(self)
+ # Store the token so we can restore in __exit__
+ object.__setattr__(self, "_token", token)
+
+ # Automatically configure logging using the log_level property
+ from ._log import _configure_loggers
+
+ _configure_loggers(
+ log_level=self.log_level(), additional_loggers=list(self.additional_loggers)
+ )
+
+ return self
+
+ def __exit__(self, *exc_info: Any) -> None:
+ """Exit context: restore previous override state."""
+ token = getattr(self, "_token", None)
+ if token is not None:
+ _active_overrides.reset(token)
+ object.__delattr__(self, "_token")
+
+ def log_level(self) -> int:
+ """Get the appropriate logging level from the debug setting.
+
+ Returns:
+ logging level constant (DEBUG, WARNING, etc.)
+ """
+ if self.debug is False:
+ return logging.WARNING
+ return self.debug
+
+ def source_epoch_or_utc_now(self) -> datetime:
+ """Get datetime from SOURCE_DATE_EPOCH or current UTC time.
+
+ Returns:
+ datetime object in UTC timezone
+ """
+ from datetime import datetime, timezone
+
+ if self.source_date_epoch is not None:
+ return datetime.fromtimestamp(self.source_date_epoch, timezone.utc)
+ else:
+ return datetime.now(timezone.utc)
+
+ @classmethod
+ def from_active(cls, **changes: Any) -> GlobalOverrides:
+ """Create a new GlobalOverrides instance based on the currently active one.
+
+ Uses dataclasses.replace() to create a modified copy of the active overrides.
+ If no overrides are currently active, raises a RuntimeError.
+
+ Args:
+ **changes: Fields to update in the new instance
+
+ Returns:
+ New GlobalOverrides instance with the specified changes
+
+ Raises:
+ RuntimeError: If no GlobalOverrides context is currently active
+
+ Example:
+ >>> with GlobalOverrides.from_env("TEST"):
+ ... # Create a modified version with different debug level
+ ... with GlobalOverrides.from_active(debug=logging.INFO):
+ ... # This context has INFO level instead
+ ... pass
+ """
+ from dataclasses import replace
+
+ active = _active_overrides.get()
+ if active is None:
+ raise RuntimeError(
+ "Cannot call from_active() without an active GlobalOverrides context. "
+ "Use from_env() to create the initial context."
+ )
+
+ # If dist_name or tool is being changed, create a new EnvReader with the updated settings
+ new_dist_name = changes.get("dist_name", active.dist_name)
+ new_tool = changes.get("tool", active.tool)
+
+ if ("dist_name" in changes and changes["dist_name"] != active.dist_name) or (
+ "tool" in changes and changes["tool"] != active.tool
+ ):
+ changes["env_reader"] = EnvReader(
+ tools_names=(new_tool, "VCS_VERSIONING"),
+ env=active.env_reader.env,
+ dist_name=new_dist_name,
+ )
+
+ return replace(active, **changes)
+
+ def export(self, target: MutableMapping[str, str] | MonkeyPatch) -> None:
+ """Export overrides to environment variables.
+
+ Can export to either a dict-like environment or a pytest monkeypatch fixture.
+ This is useful for tests that need to propagate overrides to subprocesses.
+
+ Args:
+ target: Either a MutableMapping (e.g., dict, os.environ) or a pytest
+ MonkeyPatch instance (or any object with a setenv method)
+
+ Example:
+ >>> # Export to environment dict
+ >>> overrides = GlobalOverrides.from_env("TEST", env={"TEST_DEBUG": "1"})
+ >>> env = {}
+ >>> overrides.export(env)
+ >>> print(env["TEST_DEBUG"])
+ 1
+
+ >>> # Export via pytest monkeypatch
+ >>> def test_something(monkeypatch):
+ ... overrides = GlobalOverrides.from_env("TEST")
+ ... overrides.export(monkeypatch)
+ ... # Environment is now set
+ """
+
+ # Helper to set variable based on target type
+ def set_var(key: str, value: str) -> None:
+ if isinstance(target, MutableMapping):
+ target[key] = value
+ else:
+ target.setenv(key, value)
+
+ # Export SOURCE_DATE_EPOCH
+ if self.source_date_epoch is not None:
+ set_var("SOURCE_DATE_EPOCH", str(self.source_date_epoch))
+
+ # Export tool-prefixed variables
+ prefix = self.tool
+
+ # Export debug
+ if self.debug is False:
+ set_var(f"{prefix}_DEBUG", "0")
+ else:
+ set_var(f"{prefix}_DEBUG", str(self.debug))
+
+ # Export subprocess timeout
+ set_var(f"{prefix}_SUBPROCESS_TIMEOUT", str(self.subprocess_timeout))
+
+ # Export hg command
+ set_var(f"{prefix}_HG_COMMAND", self.hg_command)
+
+
+# Thread-local storage for active global overrides
+_active_overrides: ContextVar[GlobalOverrides | None] = ContextVar(
+ "vcs_versioning_overrides", default=None
+)
+
+# Flag to track if we've already warned about auto-creating context
+_auto_create_warning_issued = False
+
+
+# Accessor functions for getting current override values
+
+
+def get_active_overrides() -> GlobalOverrides:
+ """Get the currently active GlobalOverrides instance.
+
+ If no context is active, creates one from the current environment
+ using SETUPTOOLS_SCM prefix for legacy compatibility.
+
+ Note: The auto-created instance reads from os.environ at call time,
+ so it will pick up environment changes (e.g., from pytest monkeypatch).
+
+ Returns:
+ GlobalOverrides instance
+ """
+ global _auto_create_warning_issued
+
+ overrides = _active_overrides.get()
+ if overrides is None:
+ # Auto-create context from environment for backwards compatibility
+ # Note: We create a fresh instance each time to pick up env changes
+ if not _auto_create_warning_issued:
+ warnings.warn(
+ "No GlobalOverrides context is active. "
+ "Auto-creating one with SETUPTOOLS_SCM prefix for backwards compatibility. "
+ "Consider using 'with GlobalOverrides.from_env(\"YOUR_TOOL\"):' explicitly.",
+ UserWarning,
+ stacklevel=2,
+ )
+ _auto_create_warning_issued = True
+ overrides = GlobalOverrides.from_env(
+ "SETUPTOOLS_SCM",
+ env=os.environ,
+ additional_loggers=logging.getLogger("setuptools_scm"),
+ )
+ return overrides
+
+
+def get_debug_level() -> int | Literal[False]:
+ """Get current debug level from active override context.
+
+ Returns:
+ logging level constant (DEBUG, INFO, WARNING, etc.) or False
+ """
+ return get_active_overrides().debug
+
+
+def get_subprocess_timeout() -> int:
+ """Get current subprocess timeout from active override context.
+
+ Returns:
+ Subprocess timeout in seconds
+ """
+ return get_active_overrides().subprocess_timeout
+
+
+def get_hg_command() -> str:
+ """Get current Mercurial command from active override context.
+
+ Returns:
+ Mercurial command string
+ """
+ return get_active_overrides().hg_command
+
+
+def get_source_date_epoch() -> int | None:
+ """Get SOURCE_DATE_EPOCH from active override context.
+
+ Returns:
+ Unix timestamp or None
+ """
+ return get_active_overrides().source_date_epoch
+
+
+def source_epoch_or_utc_now() -> datetime:
+ """Get datetime from SOURCE_DATE_EPOCH or current UTC time.
+
+ Uses the active GlobalOverrides context. If no SOURCE_DATE_EPOCH is set,
+ returns the current UTC time.
+
+ Returns:
+ datetime object in UTC timezone
+ """
+ return get_active_overrides().source_epoch_or_utc_now()
+
+
+class ensure_context(ContextDecorator):
+ """Context manager/decorator that ensures a GlobalOverrides context is active.
+
+ If no context is active, creates one using from_env() with the specified tool.
+ Can be used as a decorator or context manager.
+
+ Example as decorator:
+ @ensure_context("SETUPTOOLS_SCM", additional_loggers=logging.getLogger("setuptools_scm"))
+ def my_entry_point():
+ # Will automatically have context
+ pass
+
+ Example as context manager:
+ with ensure_context("SETUPTOOLS_SCM", additional_loggers=logging.getLogger("setuptools_scm")):
+ # Will have context here
+ pass
+ """
+
+ def __init__(
+ self,
+ tool: str,
+ *,
+ env: Mapping[str, str] | None = None,
+ dist_name: str | None = None,
+ additional_loggers: logging.Logger | list[logging.Logger] | tuple[()] = (),
+ ):
+ """Initialize the context ensurer.
+
+ Args:
+ tool: Tool name (e.g., "SETUPTOOLS_SCM", "vcs-versioning")
+ env: Environment variables to read from (defaults to os.environ)
+ dist_name: Optional distribution name
+ additional_loggers: Logger instance(s) to configure
+ """
+ self.tool = tool
+ self.env = env if env is not None else os.environ
+ self.dist_name = dist_name
+ self.additional_loggers = additional_loggers
+ self._context: GlobalOverrides | None = None
+ self._created_context = False
+
+ def __enter__(self) -> GlobalOverrides:
+ """Enter context: create GlobalOverrides if none is active."""
+ # Check if there's already an active context
+ existing = _active_overrides.get()
+
+ if existing is not None:
+ # Already have a context, just return it
+ self._created_context = False
+ return existing
+
+ # No context active, create one
+ self._created_context = True
+ self._context = GlobalOverrides.from_env(
+ self.tool,
+ env=self.env,
+ dist_name=self.dist_name,
+ additional_loggers=self.additional_loggers,
+ )
+ return self._context.__enter__()
+
+ def __exit__(self, *exc_info: Any) -> None:
+ """Exit context: only exit if we created the context."""
+ if self._created_context and self._context is not None:
+ self._context.__exit__(*exc_info)
+
+
+__all__ = [
+ "EnvReader",
+ "GlobalOverrides",
+ "ensure_context",
+ "get_active_overrides",
+ "get_debug_level",
+ "get_hg_command",
+ "get_source_date_epoch",
+ "get_subprocess_timeout",
+ "source_epoch_or_utc_now",
+]
diff --git a/testing/conftest.py b/vcs-versioning/src/vcs_versioning/test_api.py
similarity index 62%
rename from testing/conftest.py
rename to vcs-versioning/src/vcs_versioning/test_api.py
index f0223c77..0b772f4f 100644
--- a/testing/conftest.py
+++ b/vcs-versioning/src/vcs_versioning/test_api.py
@@ -1,27 +1,45 @@
+"""
+Pytest plugin and test API for vcs_versioning.
+
+This module can be used as a pytest plugin by adding to conftest.py:
+ pytest_plugins = ["vcs_versioning.test_api"]
+"""
+
from __future__ import annotations
import contextlib
import os
import shutil
import sys
-
-from datetime import datetime
-from datetime import timezone
+from collections.abc import Iterator
+from datetime import datetime, timezone
from pathlib import Path
from types import TracebackType
-from typing import Any
-from typing import Iterator
import pytest
-from setuptools_scm._run_cmd import run
+from ._run_cmd import run
if sys.version_info >= (3, 11):
from typing import Self
else:
from typing_extensions import Self
-from .wd_wrapper import WorkDir
+# Re-export WorkDir from _test_utils module
+from ._test_utils import WorkDir
+
+__all__ = [
+ "TEST_SOURCE_DATE",
+ "TEST_SOURCE_DATE_EPOCH",
+ "TEST_SOURCE_DATE_FORMATTED",
+ "TEST_SOURCE_DATE_TIMESTAMP",
+ "DebugMode",
+ "WorkDir",
+ "debug_mode",
+ "hg_exe",
+ "repositories_hg_git",
+ "wd",
+]
# Test time constants: 2009-02-13T23:31:30+00:00
TEST_SOURCE_DATE = datetime(2009, 2, 13, 23, 31, 30, tzinfo=timezone.utc)
@@ -33,42 +51,31 @@
def pytest_configure(config: pytest.Config) -> None:
+ """Configure pytest for vcs_versioning tests."""
# 2009-02-13T23:31:30+00:00
os.environ["SOURCE_DATE_EPOCH"] = str(TEST_SOURCE_DATE_EPOCH)
- os.environ["SETUPTOOLS_SCM_DEBUG"] = "1"
-
+ os.environ["VCS_VERSIONING_DEBUG"] = "1"
-VERSION_PKGS = ["setuptools", "setuptools_scm", "packaging", "build", "wheel"]
+@pytest.fixture(scope="session", autouse=True)
+def _global_overrides_context() -> Iterator[None]:
+ """Automatically apply GlobalOverrides context for all tests.
-def pytest_report_header() -> list[str]:
- from importlib.metadata import version
-
- res = []
- for pkg in VERSION_PKGS:
- pkg_version = version(pkg)
- path = __import__(pkg).__file__
- if path and "site-packages" in path:
- # Replace everything up to and including site-packages with site::
- parts = path.split("site-packages", 1)
- if len(parts) > 1:
- path = "site::" + parts[1]
- elif path and str(Path.cwd()) in path:
- # Replace current working directory with CWD::
- path = path.replace(str(Path.cwd()), "CWD::")
- res.append(f"{pkg} version {pkg_version} from {path}")
- return res
-
+ This ensures that SOURCE_DATE_EPOCH and debug settings from pytest_configure
+ are properly picked up by the override system.
+ """
+ from .overrides import GlobalOverrides
-def pytest_addoption(parser: Any) -> None:
- group = parser.getgroup("setuptools_scm")
- group.addoption(
- "--test-legacy", dest="scm_test_virtualenv", default=False, action="store_true"
- )
+ # Use SETUPTOOLS_SCM prefix for backwards compatibility.
+ # EnvReader will also check VCS_VERSIONING as a fallback.
+ with GlobalOverrides.from_env("SETUPTOOLS_SCM"):
+ yield
class DebugMode(contextlib.AbstractContextManager): # type: ignore[type-arg]
- from setuptools_scm import _log as __module
+ """Context manager to enable debug logging for tests."""
+
+ from . import _log as __module
def __init__(self) -> None:
self.__stack = contextlib.ExitStack()
@@ -94,6 +101,7 @@ def disable(self) -> None:
@pytest.fixture(autouse=True)
def debug_mode() -> Iterator[DebugMode]:
+ """Fixture to enable debug mode for all tests."""
with DebugMode() as debug_mode:
yield debug_mode
@@ -111,6 +119,7 @@ def wd(tmp_path: Path) -> WorkDir:
@pytest.fixture(scope="session")
def hg_exe() -> str:
+ """Fixture to get the hg executable path, skipping if not found."""
hg = shutil.which("hg")
if hg is None:
pytest.skip("hg executable not found")
@@ -119,6 +128,7 @@ def hg_exe() -> str:
@pytest.fixture
def repositories_hg_git(tmp_path: Path) -> tuple[WorkDir, WorkDir]:
+ """Fixture to create paired git and hg repositories for hg-git tests."""
tmp_path = tmp_path.resolve()
path_git = tmp_path / "repo_git"
path_git.mkdir()
diff --git a/vcs-versioning/testing_vcs/__init__.py b/vcs-versioning/testing_vcs/__init__.py
new file mode 100644
index 00000000..66e3cb07
--- /dev/null
+++ b/vcs-versioning/testing_vcs/__init__.py
@@ -0,0 +1 @@
+"""Tests for vcs-versioning."""
diff --git a/vcs-versioning/testing_vcs/conftest.py b/vcs-versioning/testing_vcs/conftest.py
new file mode 100644
index 00000000..bc8b6a12
--- /dev/null
+++ b/vcs-versioning/testing_vcs/conftest.py
@@ -0,0 +1,10 @@
+"""Pytest configuration for vcs-versioning tests.
+
+Uses vcs_versioning.test_api as a pytest plugin.
+"""
+
+from __future__ import annotations
+
+# Use our own test_api module as a pytest plugin
+# Moved to pyproject.toml addopts to avoid non-top-level conftest issues
+# pytest_plugins = ["vcs_versioning.test_api"]
diff --git a/testing/test_better_root_errors.py b/vcs-versioning/testing_vcs/test_better_root_errors.py
similarity index 95%
rename from testing/test_better_root_errors.py
rename to vcs-versioning/testing_vcs/test_better_root_errors.py
index a0c19949..b8b809e3 100644
--- a/testing/test_better_root_errors.py
+++ b/vcs-versioning/testing_vcs/test_better_root_errors.py
@@ -9,12 +9,13 @@
from __future__ import annotations
import pytest
-
-from setuptools_scm import Configuration
-from setuptools_scm import get_version
-from setuptools_scm._get_version_impl import _find_scm_in_parents
-from setuptools_scm._get_version_impl import _version_missing
-from testing.wd_wrapper import WorkDir
+from vcs_versioning import Configuration
+from vcs_versioning._get_version_impl import (
+ _find_scm_in_parents,
+ _version_missing,
+ get_version,
+)
+from vcs_versioning.test_api import WorkDir
# No longer need to import setup functions - using WorkDir methods directly
diff --git a/testing/test_compat.py b/vcs-versioning/testing_vcs/test_compat.py
similarity index 95%
rename from testing/test_compat.py
rename to vcs-versioning/testing_vcs/test_compat.py
index 3cd52771..85c8b4ba 100644
--- a/testing/test_compat.py
+++ b/vcs-versioning/testing_vcs/test_compat.py
@@ -3,9 +3,7 @@
from __future__ import annotations
import pytest
-
-from setuptools_scm._compat import normalize_path_for_assertion
-from setuptools_scm._compat import strip_path_suffix
+from vcs_versioning._compat import normalize_path_for_assertion, strip_path_suffix
def test_normalize_path_for_assertion() -> None:
diff --git a/vcs-versioning/testing_vcs/test_config.py b/vcs-versioning/testing_vcs/test_config.py
new file mode 100644
index 00000000..464c7c3f
--- /dev/null
+++ b/vcs-versioning/testing_vcs/test_config.py
@@ -0,0 +1,56 @@
+"""Tests for core Configuration functionality."""
+
+from __future__ import annotations
+
+import re
+
+import pytest
+from vcs_versioning import Configuration
+
+
+@pytest.mark.parametrize(
+ ("tag", "expected_version"),
+ [
+ ("apache-arrow-0.9.0", "0.9.0"),
+ ("arrow-0.9.0", "0.9.0"),
+ ("arrow-0.9.0-rc", "0.9.0-rc"),
+ ("arrow-1", "1"),
+ ("arrow-1+", "1"),
+ ("arrow-1+foo", "1"),
+ ("arrow-1.1+foo", "1.1"),
+ ("v1.1", "v1.1"),
+ ("V1.1", "V1.1"),
+ ],
+)
+def test_tag_regex(tag: str, expected_version: str) -> None:
+ config = Configuration()
+ match = config.tag_regex.match(tag)
+ assert match
+ version = match.group("version")
+ assert version == expected_version
+
+
+def test_config_regex_init() -> None:
+ tag_regex = re.compile(r"v(\d+)")
+ conf = Configuration(tag_regex=tag_regex)
+ assert conf.tag_regex is tag_regex
+
+
+@pytest.mark.parametrize(
+ "tag_regex",
+ [
+ r".*",
+ r"(.+)(.+)",
+ r"((.*))",
+ ],
+)
+def test_config_bad_regex(tag_regex: str) -> None:
+ with pytest.raises(
+ ValueError,
+ match=(
+ f"Expected tag_regex '{re.escape(tag_regex)}' to contain a single match"
+ " group or a group named 'version' to identify the version part of any"
+ " tag."
+ ),
+ ):
+ Configuration(tag_regex=re.compile(tag_regex))
diff --git a/testing/test_expect_parse.py b/vcs-versioning/testing_vcs/test_expect_parse.py
similarity index 93%
rename from testing/test_expect_parse.py
rename to vcs-versioning/testing_vcs/test_expect_parse.py
index 88d19303..b95acb90 100644
--- a/testing/test_expect_parse.py
+++ b/vcs-versioning/testing_vcs/test_expect_parse.py
@@ -2,20 +2,13 @@
from __future__ import annotations
-from datetime import date
-from datetime import datetime
-from datetime import timezone
+from datetime import date, datetime, timezone
from pathlib import Path
import pytest
-
-from setuptools_scm import Configuration
-from setuptools_scm.version import ScmVersion
-from setuptools_scm.version import meta
-from setuptools_scm.version import mismatches
-
-from .conftest import TEST_SOURCE_DATE
-from .wd_wrapper import WorkDir
+from vcs_versioning import Configuration
+from vcs_versioning._scm_version import ScmVersion, meta, mismatches
+from vcs_versioning.test_api import TEST_SOURCE_DATE, WorkDir
def test_scm_version_matches_basic() -> None:
diff --git a/testing/test_file_finder.py b/vcs-versioning/testing_vcs/test_file_finders.py
similarity index 96%
rename from testing/test_file_finder.py
rename to vcs-versioning/testing_vcs/test_file_finders.py
index 22a10f81..7b51e76c 100644
--- a/testing/test_file_finder.py
+++ b/vcs-versioning/testing_vcs/test_file_finders.py
@@ -2,14 +2,11 @@
import os
import sys
-
-from typing import Iterable
+from collections.abc import Iterable
import pytest
-
-from setuptools_scm._file_finders import find_files
-
-from .wd_wrapper import WorkDir
+from vcs_versioning._file_finders import find_files
+from vcs_versioning.test_api import WorkDir
@pytest.fixture(params=["git", "hg"])
@@ -255,8 +252,8 @@ def test_hg_command_from_env(
request: pytest.FixtureRequest,
hg_exe: str,
) -> None:
- with monkeypatch.context() as m:
- m.setenv("SETUPTOOLS_SCM_HG_COMMAND", hg_exe)
- m.setenv("PATH", str(hg_wd.cwd / "not-existing"))
- # No module reloading needed - runtime configuration works immediately
+ from vcs_versioning.overrides import GlobalOverrides
+
+ monkeypatch.setenv("PATH", str(hg_wd.cwd / "not-existing"))
+ with GlobalOverrides.from_active(hg_command=hg_exe):
assert set(find_files()) == {"file"}
diff --git a/testing/test_git.py b/vcs-versioning/testing_vcs/test_git.py
similarity index 93%
rename from testing/test_git.py
rename to vcs-versioning/testing_vcs/test_git.py
index 642cadcd..6e50b7f3 100644
--- a/testing/test_git.py
+++ b/vcs-versioning/testing_vcs/test_git.py
@@ -5,34 +5,33 @@
import shutil
import subprocess
import sys
-
-from datetime import date
-from datetime import datetime
-from datetime import timezone
+from collections.abc import Generator
+from datetime import date, datetime, timezone
from os.path import join as opj
from pathlib import Path
from textwrap import dedent
-from typing import Generator
-from unittest.mock import Mock
-from unittest.mock import patch
+from unittest.mock import Mock, patch
import pytest
-import setuptools_scm._file_finders
-
-from setuptools_scm import Configuration
-from setuptools_scm import NonNormalizedVersion
-from setuptools_scm import git
-from setuptools_scm._file_finders.git import git_find_files
-from setuptools_scm._run_cmd import CommandNotFoundError
-from setuptools_scm._run_cmd import CompletedProcess
-from setuptools_scm._run_cmd import has_command
-from setuptools_scm._run_cmd import run
-from setuptools_scm.git import archival_to_version
-from setuptools_scm.version import format_version
+# File finder imports (now in vcs_versioning)
+import vcs_versioning._file_finders # noqa: F401
+from vcs_versioning import Configuration
+from vcs_versioning._backends import _git
+from vcs_versioning._file_finders._git import git_find_files
+from vcs_versioning._run_cmd import (
+ CommandNotFoundError,
+ CompletedProcess,
+ has_command,
+ run,
+)
+from vcs_versioning._version_cls import NonNormalizedVersion
+from vcs_versioning._version_schemes import format_version
+from vcs_versioning.test_api import DebugMode, WorkDir
-from .conftest import DebugMode
-from .wd_wrapper import WorkDir
+# Use vcs_versioning git backend directly
+git = _git
+archival_to_version = _git.archival_to_version
# Note: Git availability is now checked in WorkDir.setup_git() method
@@ -56,12 +55,12 @@ def wd(wd: WorkDir, monkeypatch: pytest.MonkeyPatch, debug_mode: DebugMode) -> W
def test_parse_describe_output(
given: str, tag: str, number: int, node: str, dirty: bool
) -> None:
- parsed = git._git_parse_describe(given)
+ parsed = _git._git_parse_describe(given)
assert parsed == (tag, number, node, dirty)
def test_root_relative_to(wd: WorkDir, monkeypatch: pytest.MonkeyPatch) -> None:
- monkeypatch.delenv("SETUPTOOLS_SCM_DEBUG")
+ monkeypatch.delenv("SETUPTOOLS_SCM_DEBUG", raising=False)
p = wd.cwd.joinpath("sub/package")
p.mkdir(parents=True)
p.joinpath("setup.py").write_text(
@@ -78,7 +77,7 @@ def test_root_relative_to(wd: WorkDir, monkeypatch: pytest.MonkeyPatch) -> None:
def test_root_search_parent_directories(
wd: WorkDir, monkeypatch: pytest.MonkeyPatch
) -> None:
- monkeypatch.delenv("SETUPTOOLS_SCM_DEBUG")
+ monkeypatch.delenv("SETUPTOOLS_SCM_DEBUG", raising=False)
p = wd.cwd.joinpath("sub/package")
p.mkdir(parents=True)
p.joinpath("setup.py").write_text(
@@ -94,7 +93,7 @@ def test_root_search_parent_directories(
def test_git_gone(wd: WorkDir, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("PATH", str(wd.cwd / "not-existing"))
- wd.write("pyproject.toml", "[tool.setuptools_scm]")
+ wd.write("pyproject.toml", "[tool.vcs-versioning]")
with pytest.raises(CommandNotFoundError, match=r"git"):
git.parse(wd.cwd, Configuration(), git.DEFAULT_DESCRIBE)
@@ -284,18 +283,23 @@ def test_git_worktree(wd: WorkDir) -> None:
def test_git_dirty_notag(
today: bool, wd: WorkDir, monkeypatch: pytest.MonkeyPatch
) -> None:
- if today:
- monkeypatch.delenv("SOURCE_DATE_EPOCH", raising=False)
+ from vcs_versioning.overrides import GlobalOverrides
+
wd.commit_testfile()
wd.write("test.txt", "test2")
wd("git add test.txt")
- version = wd.get_version()
if today:
- # the date on the tag is in UTC
- tag = datetime.now(timezone.utc).date().strftime(".d%Y%m%d")
+ # Use from_active() to create overrides without SOURCE_DATE_EPOCH
+ with GlobalOverrides.from_active(source_date_epoch=None):
+ version = wd.get_version()
+ # the date on the tag is in UTC
+ tag = datetime.now(timezone.utc).date().strftime(".d%Y%m%d")
else:
+ # Use the existing context with SOURCE_DATE_EPOCH set
+ version = wd.get_version()
tag = ".d20090213"
+
assert version.startswith("0.1.dev1+g")
assert version.endswith(tag)
@@ -350,7 +354,7 @@ def test_find_files_stop_at_root_git(wd: WorkDir) -> None:
project = wd.cwd / "project"
project.mkdir()
project.joinpath("setup.cfg").touch()
- assert setuptools_scm._file_finders.find_files(str(project)) == []
+ assert vcs_versioning._file_finders.find_files(str(project)) == []
@pytest.mark.issue(128)
@@ -380,7 +384,7 @@ def test_git_archive_export_ignore(
wd("git add test1.txt test2.txt")
wd.commit()
monkeypatch.chdir(wd.cwd)
- assert setuptools_scm._file_finders.find_files(".") == [opj(".", "test1.txt")]
+ assert vcs_versioning._file_finders.find_files(".") == [opj(".", "test1.txt")]
@pytest.mark.issue(228)
@@ -390,7 +394,7 @@ def test_git_archive_subdirectory(wd: WorkDir, monkeypatch: pytest.MonkeyPatch)
wd("git add foobar")
wd.commit()
monkeypatch.chdir(wd.cwd)
- assert setuptools_scm._file_finders.find_files(".") == [
+ assert vcs_versioning._file_finders.find_files(".") == [
opj(".", "foobar", "test1.txt")
]
@@ -404,7 +408,7 @@ def test_git_archive_run_from_subdirectory(
wd("git add foobar")
wd.commit()
monkeypatch.chdir(wd.cwd / "foobar")
- assert setuptools_scm._file_finders.find_files(".") == [opj(".", "test1.txt")]
+ assert vcs_versioning._file_finders.find_files(".") == [opj(".", "test1.txt")]
@pytest.mark.issue("https://github.com/pypa/setuptools-scm/issues/728")
@@ -506,7 +510,7 @@ def test_git_getdate_badgit(
git_wd = git.GitWorkdir(wd.cwd)
fake_date_result = CompletedProcess(args=[], stdout="%cI", stderr="", returncode=0)
with patch.object(
- git,
+ _git,
"run_git",
Mock(return_value=fake_date_result),
):
@@ -522,7 +526,7 @@ def test_git_getdate_git_2_45_0_plus(
args=[], stdout="2024-04-30T22:33:10Z", stderr="", returncode=0
)
with patch.object(
- git,
+ _git,
"run_git",
Mock(return_value=fake_date_result),
):
@@ -705,8 +709,7 @@ def test_git_pre_parse_config_integration(wd: WorkDir) -> None:
assert result is not None
# Test with explicit configuration
- from setuptools_scm._config import GitConfiguration
- from setuptools_scm._config import ScmConfiguration
+ from vcs_versioning._config import GitConfiguration, ScmConfiguration
config_with_pre_parse = Configuration(
scm=ScmConfiguration(
@@ -812,8 +815,7 @@ def test_git_describe_command_init_argument_deprecation() -> None:
def test_git_describe_command_init_conflict() -> None:
"""Test that specifying both old and new configuration raises ValueError."""
- from setuptools_scm._config import GitConfiguration
- from setuptools_scm._config import ScmConfiguration
+ from vcs_versioning._config import GitConfiguration, ScmConfiguration
# Both old init arg and new configuration specified - should raise ValueError
with pytest.warns(DeprecationWarning, match=r"git_describe_command.*deprecated"):
diff --git a/testing/test_hg_git.py b/vcs-versioning/testing_vcs/test_hg_git.py
similarity index 87%
rename from testing/test_hg_git.py
rename to vcs-versioning/testing_vcs/test_hg_git.py
index 1f6d2ec5..57864ed5 100644
--- a/testing/test_hg_git.py
+++ b/vcs-versioning/testing_vcs/test_hg_git.py
@@ -1,13 +1,10 @@
from __future__ import annotations
import pytest
-
-from setuptools_scm import Configuration
-from setuptools_scm._run_cmd import CommandNotFoundError
-from setuptools_scm._run_cmd import has_command
-from setuptools_scm._run_cmd import run
-from setuptools_scm.hg import parse
-from testing.wd_wrapper import WorkDir
+from vcs_versioning import Configuration
+from vcs_versioning._backends._hg import parse
+from vcs_versioning._run_cmd import CommandNotFoundError, has_command, run
+from vcs_versioning.test_api import WorkDir
@pytest.fixture(scope="module", autouse=True)
@@ -115,10 +112,11 @@ def test_hg_command_from_env(
request: pytest.FixtureRequest,
hg_exe: str,
) -> None:
+ from vcs_versioning.overrides import GlobalOverrides
+
wd = repositories_hg_git[0]
- with monkeypatch.context() as m:
- m.setenv("SETUPTOOLS_SCM_HG_COMMAND", hg_exe)
- m.setenv("PATH", str(wd.cwd / "not-existing"))
- # No module reloading needed - runtime configuration works immediately
- wd.write("pyproject.toml", "[tool.setuptools_scm]")
+ wd.write("pyproject.toml", "[tool.vcs-versioning]")
+
+ monkeypatch.setenv("PATH", str(wd.cwd / "not-existing"))
+ with GlobalOverrides.from_active(hg_command=hg_exe):
assert wd.get_version().startswith("0.1.dev0+")
diff --git a/vcs-versioning/testing_vcs/test_integrator_helpers.py b/vcs-versioning/testing_vcs/test_integrator_helpers.py
new file mode 100644
index 00000000..b7da315c
--- /dev/null
+++ b/vcs-versioning/testing_vcs/test_integrator_helpers.py
@@ -0,0 +1,552 @@
+"""Tests for integrator helper API."""
+
+from __future__ import annotations
+
+from pathlib import Path
+
+import pytest
+from vcs_versioning import PyProjectData, build_configuration_from_pyproject
+from vcs_versioning._integrator_helpers import (
+ build_configuration_from_pyproject_internal,
+)
+
+
+class TestPyProjectDataFromFile:
+ """Test PyProjectData.from_file() public API."""
+
+ def test_from_file_reads_vcs_versioning(self, tmp_path: Path) -> None:
+ """Public API reads vcs-versioning section by default."""
+ pyproject = tmp_path / "pyproject.toml"
+ pyproject.write_text(
+ """
+[tool.vcs-versioning]
+version_scheme = "guess-next-dev"
+local_scheme = "no-local-version"
+"""
+ )
+
+ data = PyProjectData.from_file(pyproject)
+
+ assert data.tool_name == "vcs-versioning"
+ assert data.section_present is True
+ assert data.section["version_scheme"] == "guess-next-dev"
+ assert data.section["local_scheme"] == "no-local-version"
+
+ def test_from_file_ignores_setuptools_scm_by_default(self, tmp_path: Path) -> None:
+ """Public API ignores setuptools_scm section without internal parameter."""
+ pyproject = tmp_path / "pyproject.toml"
+ pyproject.write_text(
+ """
+[tool.setuptools_scm]
+version_scheme = "guess-next-dev"
+"""
+ )
+
+ # Public API doesn't read setuptools_scm section
+ data = PyProjectData.from_file(pyproject)
+ assert data.section_present is False
+
+ def test_from_file_internal_multi_tool_support(self, tmp_path: Path) -> None:
+ """Internal _tool_names parameter supports multiple tools."""
+ pyproject = tmp_path / "pyproject.toml"
+ pyproject.write_text(
+ """
+[tool.setuptools_scm]
+version_scheme = "guess-next-dev"
+"""
+ )
+
+ # Internal API can use _tool_names
+ data = PyProjectData.from_file(
+ pyproject,
+ _tool_names=["setuptools_scm", "vcs-versioning"],
+ )
+
+ assert data.tool_name == "setuptools_scm"
+ assert data.section_present is True
+ assert data.section["version_scheme"] == "guess-next-dev"
+
+ def test_from_file_internal_tries_in_order(self, tmp_path: Path) -> None:
+ """Internal API tries tool names in order."""
+ pyproject = tmp_path / "pyproject.toml"
+ pyproject.write_text(
+ """
+[tool.vcs-versioning]
+local_scheme = "no-local-version"
+
+[tool.setuptools_scm]
+local_scheme = "node-and-date"
+"""
+ )
+
+ # First tool name wins
+ data = PyProjectData.from_file(
+ pyproject,
+ _tool_names=["setuptools_scm", "vcs-versioning"],
+ )
+ assert data.tool_name == "setuptools_scm"
+ assert data.section["local_scheme"] == "node-and-date"
+
+ # Order matters
+ data2 = PyProjectData.from_file(
+ pyproject,
+ _tool_names=["vcs-versioning", "setuptools_scm"],
+ )
+ assert data2.tool_name == "vcs-versioning"
+ assert data2.section["local_scheme"] == "no-local-version"
+
+
+class TestManualPyProjectComposition:
+ """Test manual PyProjectData composition by integrators."""
+
+ def test_manual_composition_basic(self) -> None:
+ """Integrators can manually compose PyProjectData."""
+ pyproject = PyProjectData(
+ path=Path("pyproject.toml"),
+ tool_name="vcs-versioning",
+ project={"name": "my-pkg"},
+ section={"local_scheme": "no-local-version"},
+ is_required=True,
+ section_present=True,
+ project_present=True,
+ build_requires=["vcs-versioning"],
+ definition={},
+ )
+
+ assert pyproject.tool_name == "vcs-versioning"
+ assert pyproject.project_name == "my-pkg"
+ assert pyproject.section["local_scheme"] == "no-local-version"
+
+ def test_manual_composition_with_config_builder(self) -> None:
+ """Manual composition works with config builder."""
+ pyproject = PyProjectData(
+ path=Path("pyproject.toml"),
+ tool_name="vcs-versioning",
+ project={"name": "test-pkg"},
+ section={"version_scheme": "guess-next-dev"},
+ is_required=False,
+ section_present=True,
+ project_present=True,
+ build_requires=[],
+ definition={},
+ )
+
+ config = build_configuration_from_pyproject(
+ pyproject_data=pyproject,
+ dist_name="test-pkg",
+ )
+
+ assert config.dist_name == "test-pkg"
+ assert config.version_scheme == "guess-next-dev"
+
+
+class TestBuildConfigurationFromPyProject:
+ """Test build_configuration_from_pyproject() function."""
+
+ def test_build_configuration_basic(self, tmp_path: Path) -> None:
+ """Basic configuration building from pyproject data."""
+ pyproject_file = tmp_path / "pyproject.toml"
+ pyproject_file.write_text(
+ """
+[project]
+name = "test-package"
+
+[tool.vcs-versioning]
+version_scheme = "guess-next-dev"
+local_scheme = "no-local-version"
+"""
+ )
+
+ pyproject = PyProjectData.from_file(pyproject_file)
+ config = build_configuration_from_pyproject(
+ pyproject_data=pyproject,
+ )
+
+ assert config.dist_name == "test-package"
+ assert config.version_scheme == "guess-next-dev"
+ assert config.local_scheme == "no-local-version"
+
+ def test_build_configuration_with_dist_name_override(self, tmp_path: Path) -> None:
+ """dist_name argument overrides project name."""
+ pyproject_file = tmp_path / "pyproject.toml"
+ pyproject_file.write_text(
+ """
+[project]
+name = "wrong-name"
+
+[tool.vcs-versioning]
+version_scheme = "guess-next-dev"
+"""
+ )
+
+ pyproject = PyProjectData.from_file(pyproject_file)
+ config = build_configuration_from_pyproject(
+ pyproject_data=pyproject,
+ dist_name="correct-name",
+ )
+
+ assert config.dist_name == "correct-name"
+
+ def test_build_configuration_with_integrator_overrides(
+ self, tmp_path: Path
+ ) -> None:
+ """Integrator overrides override config file."""
+ pyproject_file = tmp_path / "pyproject.toml"
+ pyproject_file.write_text(
+ """
+[project]
+name = "test-pkg"
+
+[tool.vcs-versioning]
+version_scheme = "guess-next-dev"
+local_scheme = "node-and-date"
+"""
+ )
+
+ pyproject = PyProjectData.from_file(pyproject_file)
+ config = build_configuration_from_pyproject(
+ pyproject_data=pyproject,
+ # Integrator overrides
+ local_scheme="no-local-version",
+ version_scheme="release-branch-semver",
+ )
+
+ # Integrator overrides win over config file
+ assert config.local_scheme == "no-local-version"
+ assert config.version_scheme == "release-branch-semver"
+
+ def test_build_configuration_with_env_overrides(
+ self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
+ ) -> None:
+ """Env overrides win over integrator overrides."""
+ pyproject_file = tmp_path / "pyproject.toml"
+ pyproject_file.write_text(
+ """
+[project]
+name = "test-pkg"
+
+[tool.vcs-versioning]
+local_scheme = "node-and-date"
+"""
+ )
+
+ # Set environment TOML override
+ monkeypatch.setenv(
+ "VCS_VERSIONING_OVERRIDES",
+ '{local_scheme = "no-local-version"}',
+ )
+
+ pyproject = PyProjectData.from_file(pyproject_file)
+ config = build_configuration_from_pyproject(
+ pyproject_data=pyproject,
+ # Integrator tries to override, but env wins
+ local_scheme="dirty-tag",
+ )
+
+ # Env override wins
+ assert config.local_scheme == "no-local-version"
+
+
+class TestOverridePriorityOrder:
+ """Test complete priority order: env > integrator > config > defaults."""
+
+ def test_priority_defaults_only(self, tmp_path: Path) -> None:
+ """When nothing is set, use defaults."""
+ pyproject_file = tmp_path / "pyproject.toml"
+ pyproject_file.write_text(
+ """
+[project]
+name = "test-pkg"
+
+[tool.vcs-versioning]
+"""
+ )
+
+ pyproject = PyProjectData.from_file(pyproject_file)
+ config = build_configuration_from_pyproject(pyproject_data=pyproject)
+
+ # Default values
+ from vcs_versioning import DEFAULT_LOCAL_SCHEME, DEFAULT_VERSION_SCHEME
+
+ assert config.local_scheme == DEFAULT_LOCAL_SCHEME
+ assert config.version_scheme == DEFAULT_VERSION_SCHEME
+
+ def test_priority_config_over_defaults(self, tmp_path: Path) -> None:
+ """Config file overrides defaults."""
+ pyproject_file = tmp_path / "pyproject.toml"
+ pyproject_file.write_text(
+ """
+[project]
+name = "test-pkg"
+
+[tool.vcs-versioning]
+local_scheme = "node-and-date"
+"""
+ )
+
+ pyproject = PyProjectData.from_file(pyproject_file)
+ config = build_configuration_from_pyproject(pyproject_data=pyproject)
+
+ assert config.local_scheme == "node-and-date"
+
+ def test_priority_integrator_over_config(self, tmp_path: Path) -> None:
+ """Integrator overrides override config file."""
+ pyproject_file = tmp_path / "pyproject.toml"
+ pyproject_file.write_text(
+ """
+[project]
+name = "test-pkg"
+
+[tool.vcs-versioning]
+local_scheme = "node-and-date"
+"""
+ )
+
+ pyproject = PyProjectData.from_file(pyproject_file)
+ config = build_configuration_from_pyproject(
+ pyproject_data=pyproject,
+ local_scheme="no-local-version",
+ )
+
+ assert config.local_scheme == "no-local-version"
+
+ def test_priority_env_over_integrator(
+ self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
+ ) -> None:
+ """Environment overrides win over integrator overrides."""
+ pyproject_file = tmp_path / "pyproject.toml"
+ pyproject_file.write_text(
+ """
+[project]
+name = "test-pkg"
+
+[tool.vcs-versioning]
+local_scheme = "node-and-date"
+"""
+ )
+
+ monkeypatch.setenv(
+ "VCS_VERSIONING_OVERRIDES",
+ '{local_scheme = "dirty-tag"}',
+ )
+
+ pyproject = PyProjectData.from_file(pyproject_file)
+ config = build_configuration_from_pyproject(
+ pyproject_data=pyproject,
+ local_scheme="no-local-version",
+ )
+
+ # Env wins over everything
+ assert config.local_scheme == "dirty-tag"
+
+ def test_priority_complete_chain(
+ self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
+ ) -> None:
+ """Test complete priority chain with all levels."""
+ pyproject_file = tmp_path / "pyproject.toml"
+ pyproject_file.write_text(
+ """
+[project]
+name = "test-pkg"
+
+[tool.vcs-versioning]
+local_scheme = "node-and-date"
+version_scheme = "guess-next-dev"
+"""
+ )
+
+ # Env only overrides local_scheme
+ monkeypatch.setenv(
+ "VCS_VERSIONING_OVERRIDES",
+ '{local_scheme = "dirty-tag"}',
+ )
+
+ pyproject = PyProjectData.from_file(pyproject_file)
+ config = build_configuration_from_pyproject(
+ pyproject_data=pyproject,
+ # Integrator overrides both
+ local_scheme="no-local-version",
+ version_scheme="release-branch-semver",
+ )
+
+ # local_scheme: env wins (dirty-tag)
+ # version_scheme: integrator wins (no env override)
+ assert config.local_scheme == "dirty-tag"
+ assert config.version_scheme == "release-branch-semver"
+
+
+class TestInternalAPIMultiTool:
+ """Test internal API for setuptools_scm transition."""
+
+ def test_internal_build_configuration_multi_tool(self, tmp_path: Path) -> None:
+ """Internal API supports multiple tool names."""
+ pyproject_file = tmp_path / "pyproject.toml"
+ pyproject_file.write_text(
+ """
+[project]
+name = "test-pkg"
+
+[tool.setuptools_scm]
+local_scheme = "no-local-version"
+"""
+ )
+
+ # Internal API can load setuptools_scm section
+ pyproject = PyProjectData.from_file(
+ pyproject_file,
+ _tool_names=["setuptools_scm", "vcs-versioning"],
+ )
+
+ # Internal helper can build configuration from it
+ config = build_configuration_from_pyproject_internal(
+ pyproject_data=pyproject,
+ dist_name="test-pkg",
+ )
+
+ assert config.local_scheme == "no-local-version"
+ assert config.dist_name == "test-pkg"
+
+ def test_internal_prefers_first_tool_name(self, tmp_path: Path) -> None:
+ """Internal API uses first available tool name."""
+ pyproject_file = tmp_path / "pyproject.toml"
+ pyproject_file.write_text(
+ """
+[tool.setuptools_scm]
+local_scheme = "setuptools-value"
+
+[tool.vcs-versioning]
+local_scheme = "vcs-value"
+"""
+ )
+
+ # setuptools_scm first
+ pyproject1 = PyProjectData.from_file(
+ pyproject_file,
+ _tool_names=["setuptools_scm", "vcs-versioning"],
+ )
+ config1 = build_configuration_from_pyproject_internal(pyproject_data=pyproject1)
+ assert config1.local_scheme == "setuptools-value"
+
+ # vcs-versioning first
+ pyproject2 = PyProjectData.from_file(
+ pyproject_file,
+ _tool_names=["vcs-versioning", "setuptools_scm"],
+ )
+ config2 = build_configuration_from_pyproject_internal(pyproject_data=pyproject2)
+ assert config2.local_scheme == "vcs-value"
+
+
+class TestDistNameResolution:
+ """Test dist_name resolution in different scenarios."""
+
+ def test_dist_name_from_argument(self, tmp_path: Path) -> None:
+ """Explicit dist_name argument has highest priority."""
+ pyproject_file = tmp_path / "pyproject.toml"
+ pyproject_file.write_text(
+ """
+[project]
+name = "project-name"
+
+[tool.vcs-versioning]
+"""
+ )
+
+ pyproject = PyProjectData.from_file(pyproject_file)
+ config = build_configuration_from_pyproject(
+ pyproject_data=pyproject,
+ dist_name="argument-name",
+ )
+
+ # Argument wins
+ assert config.dist_name == "argument-name"
+
+ def test_dist_name_from_config(self, tmp_path: Path) -> None:
+ """dist_name from config if no argument."""
+ pyproject_file = tmp_path / "pyproject.toml"
+ pyproject_file.write_text(
+ """
+[project]
+name = "project-name"
+
+[tool.vcs-versioning]
+dist_name = "config-name"
+"""
+ )
+
+ pyproject = PyProjectData.from_file(pyproject_file)
+ config = build_configuration_from_pyproject(pyproject_data=pyproject)
+
+ assert config.dist_name == "config-name"
+
+ def test_dist_name_from_project(self, tmp_path: Path) -> None:
+ """dist_name from project.name if not in config."""
+ pyproject_file = tmp_path / "pyproject.toml"
+ pyproject_file.write_text(
+ """
+[project]
+name = "project-name"
+
+[tool.vcs-versioning]
+"""
+ )
+
+ pyproject = PyProjectData.from_file(pyproject_file)
+ config = build_configuration_from_pyproject(pyproject_data=pyproject)
+
+ assert config.dist_name == "project-name"
+
+ def test_dist_name_none_when_missing(self, tmp_path: Path) -> None:
+ """dist_name is None when not specified anywhere."""
+ pyproject_file = tmp_path / "pyproject.toml"
+ pyproject_file.write_text(
+ """
+[tool.vcs-versioning]
+"""
+ )
+
+ pyproject = PyProjectData.from_file(pyproject_file)
+ config = build_configuration_from_pyproject(pyproject_data=pyproject)
+
+ assert config.dist_name is None
+
+
+class TestEmptyPyProjectData:
+ """Test with empty or minimal PyProjectData."""
+
+ def test_empty_pyproject_section(self, tmp_path: Path) -> None:
+ """Empty vcs-versioning section uses defaults."""
+ pyproject_file = tmp_path / "pyproject.toml"
+ pyproject_file.write_text(
+ """
+[tool.vcs-versioning]
+"""
+ )
+
+ pyproject = PyProjectData.from_file(pyproject_file)
+ config = build_configuration_from_pyproject(pyproject_data=pyproject)
+
+ # Should use defaults
+ from vcs_versioning import DEFAULT_LOCAL_SCHEME, DEFAULT_VERSION_SCHEME
+
+ assert config.local_scheme == DEFAULT_LOCAL_SCHEME
+ assert config.version_scheme == DEFAULT_VERSION_SCHEME
+
+ def test_section_not_present(self, tmp_path: Path) -> None:
+ """Missing section still creates configuration."""
+ pyproject_file = tmp_path / "pyproject.toml"
+ pyproject_file.write_text(
+ """
+[project]
+name = "test-pkg"
+"""
+ )
+
+ # Note: This will log a warning but should not fail
+ pyproject = PyProjectData.from_file(pyproject_file)
+ config = build_configuration_from_pyproject(
+ pyproject_data=pyproject,
+ dist_name="test-pkg",
+ )
+
+ # Should still create config with defaults
+ assert config.dist_name == "test-pkg"
diff --git a/vcs-versioning/testing_vcs/test_internal_log_level.py b/vcs-versioning/testing_vcs/test_internal_log_level.py
new file mode 100644
index 00000000..f14eb273
--- /dev/null
+++ b/vcs-versioning/testing_vcs/test_internal_log_level.py
@@ -0,0 +1,47 @@
+from __future__ import annotations
+
+import logging
+
+from vcs_versioning.overrides import GlobalOverrides
+
+
+def test_log_levels_when_set() -> None:
+ # Empty string or "1" should map to DEBUG (10)
+ with GlobalOverrides.from_env("TEST", env={"TEST_DEBUG": ""}) as overrides:
+ assert overrides.log_level() == logging.DEBUG
+
+ with GlobalOverrides.from_env("TEST", env={"TEST_DEBUG": "1"}) as overrides:
+ assert overrides.log_level() == logging.DEBUG
+
+ # Level names should be recognized
+ with GlobalOverrides.from_env("TEST", env={"TEST_DEBUG": "INFO"}) as overrides:
+ assert overrides.log_level() == logging.INFO
+
+ with GlobalOverrides.from_env("TEST", env={"TEST_DEBUG": "info"}) as overrides:
+ assert overrides.log_level() == logging.INFO
+
+ with GlobalOverrides.from_env("TEST", env={"TEST_DEBUG": "WARNING"}) as overrides:
+ assert overrides.log_level() == logging.WARNING
+
+ with GlobalOverrides.from_env("TEST", env={"TEST_DEBUG": "ERROR"}) as overrides:
+ assert overrides.log_level() == logging.ERROR
+
+ with GlobalOverrides.from_env("TEST", env={"TEST_DEBUG": "CRITICAL"}) as overrides:
+ assert overrides.log_level() == logging.CRITICAL
+
+ with GlobalOverrides.from_env("TEST", env={"TEST_DEBUG": "DEBUG"}) as overrides:
+ assert overrides.log_level() == logging.DEBUG
+
+ # Unknown string should default to DEBUG
+ with GlobalOverrides.from_env("TEST", env={"TEST_DEBUG": "yes"}) as overrides:
+ assert overrides.log_level() == logging.DEBUG
+
+ # Explicit log level (>=2) should be used as-is
+ with GlobalOverrides.from_env("TEST", env={"TEST_DEBUG": "10"}) as overrides:
+ assert overrides.log_level() == logging.DEBUG
+
+ with GlobalOverrides.from_env("TEST", env={"TEST_DEBUG": "20"}) as overrides:
+ assert overrides.log_level() == logging.INFO
+
+ with GlobalOverrides.from_env("TEST", env={"TEST_DEBUG": "30"}) as overrides:
+ assert overrides.log_level() == logging.WARNING
diff --git a/testing/test_mercurial.py b/vcs-versioning/testing_vcs/test_mercurial.py
similarity index 85%
rename from testing/test_mercurial.py
rename to vcs-versioning/testing_vcs/test_mercurial.py
index 6e1b4890..082e18eb 100644
--- a/testing/test_mercurial.py
+++ b/vcs-versioning/testing_vcs/test_mercurial.py
@@ -1,19 +1,15 @@
from __future__ import annotations
import os
-
from pathlib import Path
import pytest
-
-import setuptools_scm._file_finders
-
-from setuptools_scm import Configuration
-from setuptools_scm._run_cmd import CommandNotFoundError
-from setuptools_scm.hg import archival_to_version
-from setuptools_scm.hg import parse
-from setuptools_scm.version import format_version
-from testing.wd_wrapper import WorkDir
+import vcs_versioning._file_finders # noqa: F401
+from vcs_versioning import Configuration
+from vcs_versioning._backends._hg import archival_to_version, parse
+from vcs_versioning._run_cmd import CommandNotFoundError
+from vcs_versioning._version_schemes import format_version
+from vcs_versioning.test_api import WorkDir
# Note: Mercurial availability is now checked in WorkDir.setup_hg() method
@@ -55,7 +51,7 @@ def test_archival_to_version(expected: str, data: dict[str, str]) -> None:
def test_hg_gone(wd: WorkDir, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("PATH", str(wd.cwd / "not-existing"))
config = Configuration()
- wd.write("pyproject.toml", "[tool.setuptools_scm]")
+ wd.write("pyproject.toml", "[tool.vcs-versioning]")
with pytest.raises(CommandNotFoundError, match=r"hg"):
parse(wd.cwd, config=config)
@@ -68,24 +64,28 @@ def test_hg_command_from_env(
request: pytest.FixtureRequest,
hg_exe: str,
) -> None:
- wd.write("pyproject.toml", "[tool.setuptools_scm]")
+ from vcs_versioning.overrides import GlobalOverrides
+
+ wd.write("pyproject.toml", "[tool.vcs-versioning]")
# Need to commit something first for versioning to work
wd.commit_testfile()
- monkeypatch.setenv("SETUPTOOLS_SCM_HG_COMMAND", hg_exe)
monkeypatch.setenv("PATH", str(wd.cwd / "not-existing"))
- version = wd.get_version()
- assert version.startswith("0.1.dev1+")
+ with GlobalOverrides.from_active(hg_command=hg_exe):
+ version = wd.get_version()
+ assert version.startswith("0.1.dev1+")
def test_hg_command_from_env_is_invalid(
wd: WorkDir, monkeypatch: pytest.MonkeyPatch, request: pytest.FixtureRequest
) -> None:
- with monkeypatch.context() as m:
- m.setenv("SETUPTOOLS_SCM_HG_COMMAND", str(wd.cwd / "not-existing"))
- # No module reloading needed - runtime configuration works immediately
- config = Configuration()
- wd.write("pyproject.toml", "[tool.setuptools_scm]")
+ from vcs_versioning.overrides import GlobalOverrides
+
+ config = Configuration()
+ wd.write("pyproject.toml", "[tool.vcs-versioning]")
+
+ # Use from_active() to create overrides with invalid hg command
+ with GlobalOverrides.from_active(hg_command=str(wd.cwd / "not-existing")):
with pytest.raises(CommandNotFoundError, match=r"test.*hg.*not-existing"):
parse(wd.cwd, config=config)
@@ -100,11 +100,11 @@ def test_find_files_stop_at_root_hg(
project.mkdir()
project.joinpath("setup.cfg").touch()
# setup.cfg has not been committed
- assert setuptools_scm._file_finders.find_files(str(project)) == []
+ assert vcs_versioning._file_finders.find_files(str(project)) == []
# issue 251
wd.add_and_commit()
monkeypatch.chdir(project)
- assert setuptools_scm._file_finders.find_files() == ["setup.cfg"]
+ assert vcs_versioning._file_finders.find_files() == ["setup.cfg"]
# XXX: better tests for tag prefixes
diff --git a/vcs-versioning/testing_vcs/test_overrides_api.py b/vcs-versioning/testing_vcs/test_overrides_api.py
new file mode 100644
index 00000000..06822e13
--- /dev/null
+++ b/vcs-versioning/testing_vcs/test_overrides_api.py
@@ -0,0 +1,709 @@
+"""Tests for GlobalOverrides API methods."""
+
+from __future__ import annotations
+
+import logging
+
+import pytest
+from vcs_versioning.overrides import GlobalOverrides
+
+
+def test_from_active_modifies_field() -> None:
+ """Test that from_active() creates a modified copy."""
+ with GlobalOverrides.from_env("TEST", env={"TEST_DEBUG": "1"}):
+ # Original has DEBUG level
+ assert GlobalOverrides.from_active().debug == logging.DEBUG
+
+ # Create modified version with INFO level
+ with GlobalOverrides.from_active(debug=logging.INFO):
+ from vcs_versioning.overrides import get_active_overrides
+
+ active = get_active_overrides()
+ assert active.debug == logging.INFO
+
+
+def test_from_active_preserves_other_fields() -> None:
+ """Test that from_active() preserves fields not explicitly changed."""
+ env = {
+ "TEST_DEBUG": "20", # INFO
+ "TEST_SUBPROCESS_TIMEOUT": "100",
+ "TEST_HG_COMMAND": "custom_hg",
+ "SOURCE_DATE_EPOCH": "1234567890",
+ }
+
+ with GlobalOverrides.from_env("TEST", env=env):
+ # Modify only debug level
+ with GlobalOverrides.from_active(debug=logging.WARNING):
+ from vcs_versioning.overrides import get_active_overrides
+
+ active = get_active_overrides()
+ assert active.debug == logging.WARNING
+ # Other fields preserved
+ assert active.subprocess_timeout == 100
+ assert active.hg_command == "custom_hg"
+ assert active.source_date_epoch == 1234567890
+ assert active.tool == "TEST"
+
+
+def test_from_active_without_context_raises() -> None:
+ """Test that from_active() raises when no context is active."""
+ from vcs_versioning import overrides
+
+ # Temporarily clear any active context
+ token = overrides._active_overrides.set(None)
+ try:
+ with pytest.raises(
+ RuntimeError,
+ match="Cannot call from_active\\(\\) without an active GlobalOverrides context",
+ ):
+ GlobalOverrides.from_active(debug=logging.INFO)
+ finally:
+ overrides._active_overrides.reset(token)
+
+
+def test_export_to_dict() -> None:
+ """Test exporting overrides to a dictionary."""
+ env_source = {
+ "TEST_DEBUG": "INFO",
+ "TEST_SUBPROCESS_TIMEOUT": "99",
+ "TEST_HG_COMMAND": "/usr/bin/hg",
+ "SOURCE_DATE_EPOCH": "1672531200",
+ }
+
+ overrides = GlobalOverrides.from_env("TEST", env=env_source)
+
+ target_env: dict[str, str] = {}
+ overrides.export(target_env)
+
+ assert target_env["TEST_DEBUG"] == "20" # INFO level
+ assert target_env["TEST_SUBPROCESS_TIMEOUT"] == "99"
+ assert target_env["TEST_HG_COMMAND"] == "/usr/bin/hg"
+ assert target_env["SOURCE_DATE_EPOCH"] == "1672531200"
+
+
+def test_export_to_monkeypatch(monkeypatch: pytest.MonkeyPatch) -> None:
+ """Test exporting overrides via monkeypatch."""
+ import os
+
+ overrides = GlobalOverrides.from_env(
+ "TEST",
+ env={
+ "TEST_DEBUG": "DEBUG",
+ "TEST_SUBPROCESS_TIMEOUT": "77",
+ "SOURCE_DATE_EPOCH": "1000000000",
+ },
+ )
+
+ overrides.export(monkeypatch)
+
+ # Check that environment was set
+ assert os.environ["TEST_DEBUG"] == "10" # DEBUG level
+ assert os.environ["TEST_SUBPROCESS_TIMEOUT"] == "77"
+ assert os.environ["SOURCE_DATE_EPOCH"] == "1000000000"
+
+
+def test_export_debug_false() -> None:
+ """Test that debug=False exports as '0'."""
+ overrides = GlobalOverrides.from_env("TEST", env={})
+
+ target_env: dict[str, str] = {}
+ overrides.export(target_env)
+
+ assert target_env["TEST_DEBUG"] == "0"
+
+
+def test_from_active_and_export_together(monkeypatch: pytest.MonkeyPatch) -> None:
+ """Test using from_active() and export() together."""
+ import os
+
+ # Start with one context
+ with GlobalOverrides.from_env("TOOL", env={"TOOL_DEBUG": "1"}):
+ # Create a modified version
+ modified = GlobalOverrides.from_active(
+ debug=logging.WARNING, subprocess_timeout=200
+ )
+
+ # Export it
+ modified.export(monkeypatch)
+
+ # Verify it was exported correctly
+ assert os.environ["TOOL_DEBUG"] == "30" # WARNING
+ assert os.environ["TOOL_SUBPROCESS_TIMEOUT"] == "200"
+
+
+def test_nested_from_active_contexts() -> None:
+ """Test nested contexts using from_active()."""
+ with GlobalOverrides.from_env("TEST", env={"TEST_DEBUG": "DEBUG"}):
+ from vcs_versioning.overrides import get_active_overrides
+
+ # Original: DEBUG level
+ assert get_active_overrides().debug == logging.DEBUG
+
+ with GlobalOverrides.from_active(debug=logging.INFO):
+ # Modified: INFO level
+ assert get_active_overrides().debug == logging.INFO
+
+ with GlobalOverrides.from_active(debug=logging.WARNING):
+ # Further modified: WARNING level
+ assert get_active_overrides().debug == logging.WARNING
+
+ # Back to INFO
+ assert get_active_overrides().debug == logging.INFO
+
+ # Back to DEBUG
+ assert get_active_overrides().debug == logging.DEBUG
+
+
+def test_export_without_source_date_epoch() -> None:
+ """Test that export() handles None source_date_epoch correctly."""
+ overrides = GlobalOverrides.from_env("TEST", env={})
+
+ target_env: dict[str, str] = {}
+ overrides.export(target_env)
+
+ # SOURCE_DATE_EPOCH should not be in the exported env
+ assert "SOURCE_DATE_EPOCH" not in target_env
+ assert "TEST_DEBUG" in target_env
+ assert "TEST_SUBPROCESS_TIMEOUT" in target_env
+ assert "TEST_HG_COMMAND" in target_env
+
+
+def test_from_active_multiple_fields() -> None:
+ """Test changing multiple fields at once with from_active()."""
+ env = {
+ "TEST_DEBUG": "DEBUG",
+ "TEST_SUBPROCESS_TIMEOUT": "50",
+ "TEST_HG_COMMAND": "hg",
+ "SOURCE_DATE_EPOCH": "1000000000",
+ }
+
+ with GlobalOverrides.from_env("TEST", env=env):
+ # Change multiple fields
+ with GlobalOverrides.from_active(
+ debug=logging.ERROR,
+ subprocess_timeout=999,
+ hg_command="/custom/hg",
+ source_date_epoch=2000000000,
+ ):
+ from vcs_versioning.overrides import get_active_overrides
+
+ active = get_active_overrides()
+ assert active.debug == logging.ERROR
+ assert active.subprocess_timeout == 999
+ assert active.hg_command == "/custom/hg"
+ assert active.source_date_epoch == 2000000000
+ # Tool should be preserved
+ assert active.tool == "TEST"
+
+
+def test_export_roundtrip() -> None:
+ """Test that export -> from_env produces equivalent overrides."""
+ original = GlobalOverrides.from_env(
+ "TEST",
+ env={
+ "TEST_DEBUG": "WARNING",
+ "TEST_SUBPROCESS_TIMEOUT": "123",
+ "TEST_HG_COMMAND": "/my/hg",
+ "SOURCE_DATE_EPOCH": "1234567890",
+ },
+ )
+
+ # Export to dict
+ exported_env: dict[str, str] = {}
+ original.export(exported_env)
+
+ # Create new overrides from exported env
+ recreated = GlobalOverrides.from_env("TEST", env=exported_env)
+
+ # Should be equivalent
+ assert recreated.debug == original.debug
+ assert recreated.subprocess_timeout == original.subprocess_timeout
+ assert recreated.hg_command == original.hg_command
+ assert recreated.source_date_epoch == original.source_date_epoch
+ assert recreated.tool == original.tool
+
+
+def test_from_active_preserves_tool() -> None:
+ """Test that from_active() preserves the tool prefix."""
+ with GlobalOverrides.from_env("CUSTOM_TOOL", env={"CUSTOM_TOOL_DEBUG": "1"}):
+ with GlobalOverrides.from_active(subprocess_timeout=999):
+ from vcs_versioning.overrides import get_active_overrides
+
+ active = get_active_overrides()
+ assert active.tool == "CUSTOM_TOOL"
+
+
+def test_export_with_different_debug_levels() -> None:
+ """Test that export() correctly formats different debug levels."""
+ test_cases = [
+ (False, "0"),
+ (logging.DEBUG, "10"),
+ (logging.INFO, "20"),
+ (logging.WARNING, "30"),
+ (logging.ERROR, "40"),
+ (logging.CRITICAL, "50"),
+ ]
+
+ for debug_val, expected_str in test_cases:
+ # Need an active context to use from_active()
+ with GlobalOverrides.from_env("TEST", env={}):
+ modified = GlobalOverrides.from_active(debug=debug_val)
+
+ target_env: dict[str, str] = {}
+ modified.export(target_env)
+
+ assert target_env["TEST_DEBUG"] == expected_str, (
+ f"Expected {expected_str} for debug={debug_val}, got {target_env['TEST_DEBUG']}"
+ )
+
+
+def test_from_active_with_source_date_epoch_none() -> None:
+ """Test that from_active() can clear source_date_epoch."""
+ with GlobalOverrides.from_env("TEST", env={"SOURCE_DATE_EPOCH": "1234567890"}):
+ from vcs_versioning.overrides import get_active_overrides
+
+ # Original has epoch set
+ assert get_active_overrides().source_date_epoch == 1234567890
+
+ # Clear it with from_active
+ with GlobalOverrides.from_active(source_date_epoch=None):
+ assert get_active_overrides().source_date_epoch is None
+
+
+def test_export_integration_with_subprocess_pattern() -> None:
+ """Test the common pattern of exporting for subprocess calls."""
+
+ # Simulate the pattern used in tests
+ with GlobalOverrides.from_env("TOOL", env={"TOOL_DEBUG": "INFO"}):
+ modified = GlobalOverrides.from_active(
+ subprocess_timeout=5, debug=logging.DEBUG
+ )
+
+ # Export to a clean environment
+ subprocess_env: dict[str, str] = {}
+ modified.export(subprocess_env)
+
+ # Verify subprocess would get the right values
+ assert subprocess_env["TOOL_DEBUG"] == "10" # DEBUG
+ assert subprocess_env["TOOL_SUBPROCESS_TIMEOUT"] == "5"
+
+ # Can be used with subprocess.run
+ # subprocess.run(["cmd"], env=subprocess_env)
+
+
+def test_env_reader_property() -> None:
+ """Test that GlobalOverrides provides a configured EnvReader."""
+ env = {
+ "TOOL_CUSTOM_VAR": "value1",
+ "VCS_VERSIONING_FALLBACK_VAR": "value2",
+ "TOOL_VAR_FOR_MY_PKG": "dist_specific",
+ }
+
+ # Without dist_name
+ with GlobalOverrides.from_env("TOOL", env=env) as overrides:
+ reader = overrides.env_reader
+ assert reader.read("CUSTOM_VAR") == "value1"
+ assert reader.read("FALLBACK_VAR") == "value2" # Uses VCS_VERSIONING fallback
+ assert reader.read("NONEXISTENT") is None
+
+ # With dist_name
+ with GlobalOverrides.from_env("TOOL", env=env, dist_name="my-pkg") as overrides:
+ reader = overrides.env_reader
+ assert reader.read("VAR") == "dist_specific" # Dist-specific takes precedence
+
+
+def test_env_reader_property_with_dist_name() -> None:
+ """Test EnvReader property with distribution-specific variables."""
+ env = {
+ "TOOL_CONFIG_FOR_MY_PACKAGE": '{local_scheme = "no-local"}',
+ "TOOL_CONFIG": '{version_scheme = "guess-next-dev"}',
+ }
+
+ from typing import TypedDict
+
+ class TestSchema(TypedDict, total=False):
+ local_scheme: str
+ version_scheme: str
+
+ with GlobalOverrides.from_env("TOOL", env=env, dist_name="my-package") as overrides:
+ # Should read dist-specific TOML
+ config = overrides.env_reader.read_toml("CONFIG", schema=TestSchema)
+ assert config == {"local_scheme": "no-local"}
+
+ # Without dist_name, gets generic
+ with GlobalOverrides.from_env("TOOL", env=env) as overrides:
+ config = overrides.env_reader.read_toml("CONFIG", schema=TestSchema)
+ assert config == {"version_scheme": "guess-next-dev"}
+
+
+class TestEnvReader:
+ """Tests for the EnvReader class."""
+
+ def test_requires_tools_names(self) -> None:
+ """Test that EnvReader requires tools_names to be provided."""
+ from vcs_versioning.overrides import EnvReader
+
+ with pytest.raises(TypeError, match="tools_names must be a non-empty tuple"):
+ EnvReader(tools_names=(), env={})
+
+ def test_empty_tools_names_raises(self) -> None:
+ """Test that empty tools_names raises an error."""
+ from vcs_versioning.overrides import EnvReader
+
+ with pytest.raises(TypeError, match="tools_names must be a non-empty tuple"):
+ EnvReader(tools_names=(), env={})
+
+ def test_read_generic_first_tool(self) -> None:
+ """Test reading generic env var from first tool."""
+ from vcs_versioning.overrides import EnvReader
+
+ env = {"TOOL_A_DEBUG": "1"}
+ reader = EnvReader(tools_names=("TOOL_A", "TOOL_B"), env=env)
+ assert reader.read("DEBUG") == "1"
+
+ def test_read_generic_fallback_to_second_tool(self) -> None:
+ """Test falling back to second tool when first not found."""
+ from vcs_versioning.overrides import EnvReader
+
+ env = {"TOOL_B_DEBUG": "2"}
+ reader = EnvReader(tools_names=("TOOL_A", "TOOL_B"), env=env)
+ assert reader.read("DEBUG") == "2"
+
+ def test_read_generic_first_tool_wins(self) -> None:
+ """Test that first tool takes precedence."""
+ from vcs_versioning.overrides import EnvReader
+
+ env = {"TOOL_A_DEBUG": "1", "TOOL_B_DEBUG": "2"}
+ reader = EnvReader(tools_names=("TOOL_A", "TOOL_B"), env=env)
+ assert reader.read("DEBUG") == "1"
+
+ def test_read_not_found(self) -> None:
+ """Test that None is returned when env var not found."""
+ from vcs_versioning.overrides import EnvReader
+
+ reader = EnvReader(tools_names=("TOOL_A", "TOOL_B"), env={})
+ assert reader.read("DEBUG") is None
+
+ def test_read_dist_specific_first_tool(self) -> None:
+ """Test reading dist-specific env var from first tool."""
+ from vcs_versioning.overrides import EnvReader
+
+ env = {"TOOL_A_PRETEND_VERSION_FOR_MY_PACKAGE": "1.0.0"}
+ reader = EnvReader(
+ tools_names=("TOOL_A", "TOOL_B"), env=env, dist_name="my-package"
+ )
+ assert reader.read("PRETEND_VERSION") == "1.0.0"
+
+ def test_read_dist_specific_fallback_to_second_tool(self) -> None:
+ """Test falling back to second tool for dist-specific."""
+ from vcs_versioning.overrides import EnvReader
+
+ env = {"TOOL_B_PRETEND_VERSION_FOR_MY_PACKAGE": "2.0.0"}
+ reader = EnvReader(
+ tools_names=("TOOL_A", "TOOL_B"), env=env, dist_name="my-package"
+ )
+ assert reader.read("PRETEND_VERSION") == "2.0.0"
+
+ def test_read_dist_specific_takes_precedence_over_generic(self) -> None:
+ """Test that dist-specific takes precedence over generic."""
+ from vcs_versioning.overrides import EnvReader
+
+ env = {
+ "TOOL_A_PRETEND_VERSION_FOR_MY_PACKAGE": "1.0.0",
+ "TOOL_A_PRETEND_VERSION": "2.0.0",
+ }
+ reader = EnvReader(
+ tools_names=("TOOL_A", "TOOL_B"), env=env, dist_name="my-package"
+ )
+ assert reader.read("PRETEND_VERSION") == "1.0.0"
+
+ def test_read_dist_specific_second_tool_over_generic_first_tool(self) -> None:
+ """Test that dist-specific from second tool beats generic from first tool."""
+ from vcs_versioning.overrides import EnvReader
+
+ env = {
+ "TOOL_B_PRETEND_VERSION_FOR_MY_PACKAGE": "2.0.0",
+ "TOOL_A_PRETEND_VERSION": "1.0.0",
+ }
+ reader = EnvReader(
+ tools_names=("TOOL_A", "TOOL_B"), env=env, dist_name="my-package"
+ )
+ # Dist-specific from TOOL_B should win
+ assert reader.read("PRETEND_VERSION") == "2.0.0"
+
+ def test_read_falls_back_to_generic_when_no_dist_specific(self) -> None:
+ """Test falling back to generic when dist-specific not found."""
+ from vcs_versioning.overrides import EnvReader
+
+ env = {"TOOL_B_PRETEND_VERSION": "2.0.0"}
+ reader = EnvReader(
+ tools_names=("TOOL_A", "TOOL_B"), env=env, dist_name="my-package"
+ )
+ assert reader.read("PRETEND_VERSION") == "2.0.0"
+
+ def test_read_normalizes_dist_name(self) -> None:
+ """Test that distribution names are normalized correctly."""
+ from vcs_versioning.overrides import EnvReader
+
+ env = {"TOOL_A_PRETEND_VERSION_FOR_MY_PACKAGE": "1.0.0"}
+ # Try various equivalent dist names
+ for dist_name in ["my-package", "My.Package", "my_package", "MY-PACKAGE"]:
+ reader = EnvReader(tools_names=("TOOL_A",), env=env, dist_name=dist_name)
+ assert reader.read("PRETEND_VERSION") == "1.0.0"
+
+ def test_read_finds_alternative_normalization(
+ self, caplog: pytest.LogCaptureFixture
+ ) -> None:
+ """Test that read warns about alternative normalizations."""
+ from vcs_versioning.overrides import EnvReader
+
+ # Use a non-standard normalization
+ env = {"TOOL_A_PRETEND_VERSION_FOR_MY-PACKAGE": "1.0.0"}
+ reader = EnvReader(tools_names=("TOOL_A",), env=env, dist_name="my-package")
+
+ with caplog.at_level(logging.WARNING):
+ result = reader.read("PRETEND_VERSION")
+
+ assert result == "1.0.0"
+ assert "Found environment variable" in caplog.text
+ assert "but expected" in caplog.text
+ assert "TOOL_A_PRETEND_VERSION_FOR_MY_PACKAGE" in caplog.text
+
+ def test_read_suggests_close_matches(
+ self, caplog: pytest.LogCaptureFixture
+ ) -> None:
+ """Test that read suggests close matches for typos."""
+ from vcs_versioning.overrides import EnvReader
+
+ # Use a typo in dist name
+ env = {"TOOL_A_PRETEND_VERSION_FOR_MY_PACKGE": "1.0.0"}
+ reader = EnvReader(tools_names=("TOOL_A",), env=env, dist_name="my-package")
+
+ with caplog.at_level(logging.WARNING):
+ result = reader.read("PRETEND_VERSION")
+
+ assert result is None
+ assert "Did you mean" in caplog.text
+
+ def test_read_returns_exact_match_without_warning(
+ self, caplog: pytest.LogCaptureFixture
+ ) -> None:
+ """Test that exact matches don't trigger diagnostics."""
+ from vcs_versioning.overrides import EnvReader
+
+ env = {"TOOL_A_PRETEND_VERSION_FOR_MY_PACKAGE": "1.0.0"}
+ reader = EnvReader(tools_names=("TOOL_A",), env=env, dist_name="my-package")
+
+ with caplog.at_level(logging.WARNING):
+ result = reader.read("PRETEND_VERSION")
+
+ assert result == "1.0.0"
+ # No warnings should be logged for exact matches
+ assert not caplog.records
+
+ def test_read_toml_inline_map(self) -> None:
+ """Test reading an inline TOML map."""
+ from vcs_versioning._overrides import ConfigOverridesDict
+ from vcs_versioning.overrides import EnvReader
+
+ env = {
+ "TOOL_A_OVERRIDES": '{local_scheme = "no-local-version", version_scheme = "release-branch-semver"}'
+ }
+ reader = EnvReader(tools_names=("TOOL_A",), env=env)
+
+ result = reader.read_toml("OVERRIDES", schema=ConfigOverridesDict)
+ assert result == {
+ "local_scheme": "no-local-version",
+ "version_scheme": "release-branch-semver",
+ }
+
+ def test_read_toml_full_document(self) -> None:
+ """Test reading a full TOML document."""
+ from vcs_versioning._overrides import PretendMetadataDict
+ from vcs_versioning.overrides import EnvReader
+
+ env = {
+ "TOOL_A_PRETEND_METADATA": 'tag = "v1.0.0"\ndistance = 4\nnode = "g123abc"'
+ }
+ reader = EnvReader(tools_names=("TOOL_A",), env=env)
+
+ result = reader.read_toml("PRETEND_METADATA", schema=PretendMetadataDict)
+ assert result == {"tag": "v1.0.0", "distance": 4, "node": "g123abc"}
+
+ def test_read_toml_not_found_returns_empty_dict(self) -> None:
+ """Test that read_toml returns empty dict when not found."""
+ from vcs_versioning._overrides import ConfigOverridesDict
+ from vcs_versioning.overrides import EnvReader
+
+ reader = EnvReader(tools_names=("TOOL_A",), env={})
+
+ result = reader.read_toml("OVERRIDES", schema=ConfigOverridesDict)
+ assert result == {}
+
+ def test_read_toml_empty_string_returns_empty_dict(self) -> None:
+ """Test that empty string returns empty dict."""
+ from vcs_versioning._overrides import ConfigOverridesDict
+ from vcs_versioning.overrides import EnvReader
+
+ env = {"TOOL_A_OVERRIDES": ""}
+ reader = EnvReader(tools_names=("TOOL_A",), env=env)
+
+ result = reader.read_toml("OVERRIDES", schema=ConfigOverridesDict)
+ assert result == {}
+
+ def test_read_toml_with_tool_fallback(self) -> None:
+ """Test that read_toml respects tool fallback order."""
+ from typing import TypedDict
+
+ from vcs_versioning.overrides import EnvReader
+
+ class _TestSchema(TypedDict, total=False):
+ """Schema for this test without validation."""
+
+ debug: bool
+
+ env = {"TOOL_B_OVERRIDES": "{debug = true}"}
+ reader = EnvReader(tools_names=("TOOL_A", "TOOL_B"), env=env)
+
+ result = reader.read_toml("OVERRIDES", schema=_TestSchema)
+ assert result == {"debug": True}
+
+ def test_read_toml_with_dist_specific(self) -> None:
+ """Test reading dist-specific TOML data."""
+ from vcs_versioning._overrides import ConfigOverridesDict
+ from vcs_versioning.overrides import EnvReader
+
+ env = {
+ "TOOL_A_OVERRIDES_FOR_MY_PACKAGE": '{local_scheme = "no-local-version"}',
+ "TOOL_A_OVERRIDES": '{version_scheme = "guess-next-dev"}',
+ }
+ reader = EnvReader(tools_names=("TOOL_A",), env=env, dist_name="my-package")
+
+ # Should get dist-specific version
+ result = reader.read_toml("OVERRIDES", schema=ConfigOverridesDict)
+ assert result == {"local_scheme": "no-local-version"}
+
+ def test_read_toml_dist_specific_fallback_to_generic(self) -> None:
+ """Test falling back to generic when dist-specific not found."""
+ from vcs_versioning._overrides import ConfigOverridesDict
+ from vcs_versioning.overrides import EnvReader
+
+ env = {"TOOL_A_OVERRIDES": '{version_scheme = "guess-next-dev"}'}
+ reader = EnvReader(tools_names=("TOOL_A",), env=env, dist_name="my-package")
+
+ result = reader.read_toml("OVERRIDES", schema=ConfigOverridesDict)
+ assert result == {"version_scheme": "guess-next-dev"}
+
+ def test_read_toml_invalid_raises(self) -> None:
+ """Test that invalid TOML raises InvalidTomlError."""
+ from vcs_versioning._overrides import ConfigOverridesDict
+ from vcs_versioning._toml import InvalidTomlError
+ from vcs_versioning.overrides import EnvReader
+
+ env = {"TOOL_A_OVERRIDES": "this is not valid toml {{{"}
+ reader = EnvReader(tools_names=("TOOL_A",), env=env)
+
+ with pytest.raises(InvalidTomlError, match="Invalid TOML content"):
+ reader.read_toml("OVERRIDES", schema=ConfigOverridesDict)
+
+ def test_read_toml_with_alternative_normalization(
+ self, caplog: pytest.LogCaptureFixture
+ ) -> None:
+ """Test that read_toml works with diagnostic warnings."""
+ from typing import TypedDict
+
+ from vcs_versioning.overrides import EnvReader
+
+ class _TestSchema(TypedDict, total=False):
+ """Schema for this test without validation."""
+
+ key: str
+
+ # Use a non-standard normalization
+ env = {"TOOL_A_OVERRIDES_FOR_MY-PACKAGE": '{key = "value"}'}
+ reader = EnvReader(tools_names=("TOOL_A",), env=env, dist_name="my-package")
+
+ with caplog.at_level(logging.WARNING):
+ result = reader.read_toml("OVERRIDES", schema=_TestSchema)
+
+ assert result == {"key": "value"}
+ assert "Found environment variable" in caplog.text
+ assert "but expected" in caplog.text
+
+ def test_read_toml_complex_metadata(self) -> None:
+ """Test reading complex ScmVersion metadata."""
+ from vcs_versioning._overrides import PretendMetadataDict
+ from vcs_versioning.overrides import EnvReader
+
+ env = {
+ "TOOL_A_PRETEND_METADATA": '{tag = "v2.0.0", distance = 10, node = "gabcdef123", dirty = true, branch = "main"}'
+ }
+ reader = EnvReader(tools_names=("TOOL_A",), env=env)
+
+ result = reader.read_toml("PRETEND_METADATA", schema=PretendMetadataDict)
+ assert result["tag"] == "v2.0.0"
+ assert result["distance"] == 10
+ assert result["node"] == "gabcdef123"
+ assert result["dirty"] is True
+ assert result["branch"] == "main"
+
+ def test_read_toml_with_schema_validation(
+ self, caplog: pytest.LogCaptureFixture
+ ) -> None:
+ """Test that schema validation filters invalid fields."""
+ from typing import TypedDict
+
+ from vcs_versioning.overrides import EnvReader
+
+ # Define a test schema
+ class TestSchema(TypedDict, total=False):
+ valid_field: str
+ another_valid: str
+
+ env = {
+ "TOOL_A_DATA": '{valid_field = "ok", invalid_field = "bad", another_valid = "also ok"}'
+ }
+ reader = EnvReader(tools_names=("TOOL_A",), env=env)
+
+ with caplog.at_level(logging.WARNING):
+ result = reader.read_toml("DATA", schema=TestSchema)
+
+ # Invalid field should be removed
+ assert result == {"valid_field": "ok", "another_valid": "also ok"}
+ assert "invalid_field" not in result
+
+ # Should have logged a warning about invalid fields
+ assert "Invalid fields in TOML data" in caplog.text
+ assert "invalid_field" in caplog.text
+
+
+def test_read_toml_overrides_with_schema(
+ caplog: pytest.LogCaptureFixture,
+) -> None:
+ """Test that read_toml_overrides validates against CONFIG_OVERRIDES_SCHEMA."""
+ import os
+ from unittest.mock import patch
+
+ from vcs_versioning._overrides import read_toml_overrides
+
+ # Mock the environment with valid and invalid fields
+ mock_env = {
+ "SETUPTOOLS_SCM_OVERRIDES": '{version_scheme = "guess-next-dev", local_scheme = "no-local-version", invalid_field = "bad"}'
+ }
+
+ with (
+ patch.dict(os.environ, mock_env, clear=True),
+ caplog.at_level(logging.WARNING),
+ ):
+ result = read_toml_overrides(dist_name=None)
+
+ # Valid fields should be present
+ assert result["version_scheme"] == "guess-next-dev"
+ assert result["local_scheme"] == "no-local-version"
+
+ # Invalid field should be removed
+ assert "invalid_field" not in result
+
+ # Should have logged a warning
+ assert "Invalid fields in TOML data" in caplog.text
+ assert "invalid_field" in caplog.text
diff --git a/testing/test_overrides.py b/vcs-versioning/testing_vcs/test_overrides_env_reader.py
similarity index 93%
rename from testing/test_overrides.py
rename to vcs-versioning/testing_vcs/test_overrides_env_reader.py
index afba5339..5ad042b4 100644
--- a/testing/test_overrides.py
+++ b/vcs-versioning/testing_vcs/test_overrides_env_reader.py
@@ -3,10 +3,28 @@
import logging
import pytest
-
-from setuptools_scm._overrides import _find_close_env_var_matches
-from setuptools_scm._overrides import _search_env_vars_with_prefix
-from setuptools_scm._overrides import read_named_env
+from vcs_versioning._overrides import (
+ _find_close_env_var_matches,
+ _search_env_vars_with_prefix,
+)
+from vcs_versioning.overrides import EnvReader
+
+
+# Helper function that matches the old read_named_env signature for tests
+def read_named_env(
+ *,
+ name: str,
+ dist_name: str | None,
+ env: dict[str, str],
+ tool: str = "SETUPTOOLS_SCM",
+) -> str | None:
+ """Test helper that wraps EnvReader to match old read_named_env signature."""
+ reader = EnvReader(
+ tools_names=(tool, "VCS_VERSIONING"),
+ env=env,
+ dist_name=dist_name,
+ )
+ return reader.read(name)
class TestSearchEnvVarsWithPrefix:
diff --git a/vcs-versioning/testing_vcs/test_regressions.py b/vcs-versioning/testing_vcs/test_regressions.py
new file mode 100644
index 00000000..ae032543
--- /dev/null
+++ b/vcs-versioning/testing_vcs/test_regressions.py
@@ -0,0 +1,104 @@
+"""Core VCS regression tests."""
+
+from __future__ import annotations
+
+import sys
+from collections.abc import Sequence
+from dataclasses import replace
+from pathlib import Path
+
+import pytest
+from vcs_versioning import Configuration
+from vcs_versioning._backends._git import parse
+from vcs_versioning._run_cmd import run
+from vcs_versioning._scm_version import meta
+from vcs_versioning.test_api import WorkDir
+
+
+@pytest.mark.skipif(sys.platform != "win32", reason="this bug is only valid on windows")
+def test_case_mismatch_on_windows_git(tmp_path: Path) -> None:
+ """Case insensitive path checks on Windows"""
+ camel_case_path = tmp_path / "CapitalizedDir"
+ camel_case_path.mkdir()
+ run("git init", camel_case_path)
+ res = parse(str(camel_case_path).lower(), Configuration())
+ assert res is not None
+
+
+@pytest.mark.skipif(sys.platform != "win32", reason="this bug is only valid on windows")
+def test_case_mismatch_nested_dir_windows_git(tmp_path: Path) -> None:
+ """Test case where we have a nested directory with different casing"""
+ # Create git repo in my_repo
+ repo_path = tmp_path / "my_repo"
+ repo_path.mkdir()
+ wd = WorkDir(repo_path).setup_git()
+
+ # Create a nested directory with specific casing
+ nested_dir = repo_path / "CasedDir"
+ nested_dir.mkdir()
+
+ # Create a pyproject.toml in the nested directory
+ wd.write(
+ "CasedDir/pyproject.toml",
+ """
+[build-system]
+requires = ["setuptools>=64", "setuptools-scm"]
+build-backend = "setuptools.build_meta"
+
+[project]
+name = "test-project"
+dynamic = ["version"]
+
+[tool.setuptools_scm]
+""",
+ )
+
+ # Add and commit the file
+ wd.add_and_commit("Initial commit")
+
+ # Now try to parse from the nested directory with lowercase path
+ # This simulates: cd my_repo/caseddir (lowercase) when actual dir is CasedDir
+ lowercase_nested_path = str(nested_dir).replace("CasedDir", "caseddir")
+
+ # This should trigger the assertion error in _git_toplevel
+ try:
+ res = parse(lowercase_nested_path, Configuration())
+ # If we get here without assertion error, the bug is already fixed or not triggered
+ print(f"Parse succeeded with result: {res}")
+ except AssertionError as e:
+ print(f"AssertionError caught as expected: {e}")
+ # Re-raise so the test fails, showing we reproduced the bug
+ raise
+
+
+def test_write_to_absolute_path_passes_when_subdir_of_root(tmp_path: Path) -> None:
+ c = Configuration(root=tmp_path, write_to=tmp_path / "VERSION.py")
+ v = meta("1.0", config=c)
+ from vcs_versioning._get_version_impl import write_version_files
+
+ with pytest.warns(DeprecationWarning, match=".*write_to=.* is a absolute.*"):
+ write_version_files(c, "1.0", v)
+ write_version_files(replace(c, write_to="VERSION.py"), "1.0", v)
+ subdir = tmp_path / "subdir"
+ subdir.mkdir()
+ with pytest.raises(
+ # todo: python version specific error list
+ ValueError,
+ match=r".*VERSION.py' .* .*subdir.*",
+ ):
+ write_version_files(replace(c, root=subdir), "1.0", v)
+
+
+@pytest.mark.parametrize(
+ ("input", "expected"),
+ [
+ ("1.0", (1, 0)),
+ ("1.0a2", (1, 0, "a2")),
+ ("1.0.b2dev1", (1, 0, "b2", "dev1")),
+ ("1.0.dev1", (1, 0, "dev1")),
+ ],
+)
+def test_version_as_tuple(input: str, expected: Sequence[int | str]) -> None:
+ from vcs_versioning._version_cls import _version_as_tuple
+
+ assert _version_as_tuple(input) == expected
diff --git a/testing/test_version.py b/vcs-versioning/testing_vcs/test_version.py
similarity index 90%
rename from testing/test_version.py
rename to vcs-versioning/testing_vcs/test_version.py
index a87f49d7..a0a35bd6 100644
--- a/testing/test_version.py
+++ b/vcs-versioning/testing_vcs/test_version.py
@@ -1,28 +1,23 @@
from __future__ import annotations
import re
-
from dataclasses import replace
-from datetime import date
-from datetime import datetime
-from datetime import timedelta
-from datetime import timezone
+from datetime import date, datetime, timedelta, timezone
from typing import Any
import pytest
-
-from setuptools_scm import Configuration
-from setuptools_scm import NonNormalizedVersion
-from setuptools_scm.version import ScmVersion
-from setuptools_scm.version import calver_by_date
-from setuptools_scm.version import format_version
-from setuptools_scm.version import guess_next_date_ver
-from setuptools_scm.version import guess_next_version
-from setuptools_scm.version import meta
-from setuptools_scm.version import no_guess_dev_version
-from setuptools_scm.version import only_version
-from setuptools_scm.version import release_branch_semver_version
-from setuptools_scm.version import simplified_semver_version
+from vcs_versioning import Configuration, NonNormalizedVersion
+from vcs_versioning._scm_version import ScmVersion, meta
+from vcs_versioning._version_schemes import (
+ calver_by_date,
+ format_version,
+ guess_next_date_ver,
+ guess_next_version,
+ no_guess_dev_version,
+ only_version,
+ release_branch_semver_version,
+ simplified_semver_version,
+)
c = Configuration()
c_non_normalize = Configuration(version_cls=NonNormalizedVersion)
@@ -70,34 +65,6 @@ def test_next_semver(version: ScmVersion, expected_next: str) -> None:
assert computed == expected_next
-def test_next_semver_bad_tag() -> None:
- # Create a mock version class that represents an invalid version for testing error handling
- from typing import cast
-
- from setuptools_scm._version_cls import _VersionT
-
- class BrokenVersionForTest:
- """A mock version that behaves like a string but passes type checking."""
-
- def __init__(self, version_str: str):
- self._version_str = version_str
-
- def __str__(self) -> str:
- return self._version_str
-
- def __repr__(self) -> str:
- return f"BrokenVersionForTest({self._version_str!r})"
-
- # Cast to the expected type to avoid type checking issues
- broken_tag = cast(_VersionT, BrokenVersionForTest("1.0.0-foo"))
- version = meta(broken_tag, preformatted=True, config=c)
-
- with pytest.raises(
- ValueError, match=r"1\.0\.0-foo.* can't be parsed as numeric version"
- ):
- simplified_semver_version(version)
-
-
@pytest.mark.parametrize(
("version", "expected_next"),
[
@@ -241,7 +208,6 @@ def test_tag_regex1(tag: str, expected: str) -> None:
result = meta(tag, config=c)
else:
result = meta(tag, config=c)
- assert not isinstance(result.tag, str)
assert result.tag.public == expected
@@ -297,7 +263,7 @@ def test_custom_version_schemes() -> None:
config=replace(
c,
local_scheme="no-local-version",
- version_scheme="setuptools_scm.version:no_guess_dev_version",
+ version_scheme="vcs_versioning._version_schemes:no_guess_dev_version",
),
)
custom_computed = format_version(version)
diff --git a/vcs-versioning/testing_vcs/test_version_scheme_towncrier.py b/vcs-versioning/testing_vcs/test_version_scheme_towncrier.py
new file mode 100644
index 00000000..f7987653
--- /dev/null
+++ b/vcs-versioning/testing_vcs/test_version_scheme_towncrier.py
@@ -0,0 +1,484 @@
+"""Tests for the towncrier-fragments version scheme."""
+
+from __future__ import annotations
+
+from pathlib import Path
+
+import pytest
+from vcs_versioning import _config
+from vcs_versioning._scm_version import ScmVersion
+from vcs_versioning._version_cls import Version
+from vcs_versioning._version_schemes._towncrier import (
+ _determine_bump_type,
+ _find_fragments,
+ version_from_fragments,
+)
+
+
+@pytest.fixture
+def changelog_dir(tmp_path: Path) -> Path:
+ """Create a temporary changelog.d directory."""
+ changelog_d = tmp_path / "changelog.d"
+ changelog_d.mkdir()
+ return changelog_d
+
+
+@pytest.fixture
+def config(tmp_path: Path) -> _config.Configuration:
+ """Create a minimal configuration object."""
+ return _config.Configuration(root=tmp_path)
+
+
+def test_find_fragments_empty(changelog_dir: Path) -> None:
+ """Test finding fragments in an empty directory."""
+ fragments = _find_fragments(changelog_dir.parent)
+ assert all(len(frags) == 0 for frags in fragments.values())
+
+
+def test_find_fragments_feature(changelog_dir: Path) -> None:
+ """Test finding feature fragments."""
+ (changelog_dir / "123.feature.md").write_text("Add new feature")
+ (changelog_dir / "456.feature.md").write_text("Another feature")
+
+ fragments = _find_fragments(changelog_dir.parent)
+ assert len(fragments["feature"]) == 2
+ assert "123.feature.md" in fragments["feature"]
+ assert "456.feature.md" in fragments["feature"]
+
+
+def test_find_fragments_bugfix(changelog_dir: Path) -> None:
+ """Test finding bugfix fragments."""
+ (changelog_dir / "789.bugfix.md").write_text("Fix bug")
+
+ fragments = _find_fragments(changelog_dir.parent)
+ assert len(fragments["bugfix"]) == 1
+ assert "789.bugfix.md" in fragments["bugfix"]
+
+
+def test_find_fragments_removal(changelog_dir: Path) -> None:
+ """Test finding removal fragments."""
+ (changelog_dir / "321.removal.md").write_text("Remove deprecated API")
+
+ fragments = _find_fragments(changelog_dir.parent)
+ assert len(fragments["removal"]) == 1
+ assert "321.removal.md" in fragments["removal"]
+
+
+def test_find_fragments_deprecation(changelog_dir: Path) -> None:
+ """Test finding deprecation fragments."""
+ (changelog_dir / "654.deprecation.md").write_text("Deprecate old method")
+
+ fragments = _find_fragments(changelog_dir.parent)
+ assert len(fragments["deprecation"]) == 1
+ assert "654.deprecation.md" in fragments["deprecation"]
+
+
+def test_find_fragments_doc(changelog_dir: Path) -> None:
+ """Test finding doc fragments."""
+ (changelog_dir / "111.doc.md").write_text("Update documentation")
+
+ fragments = _find_fragments(changelog_dir.parent)
+ assert len(fragments["doc"]) == 1
+ assert "111.doc.md" in fragments["doc"]
+
+
+def test_find_fragments_misc(changelog_dir: Path) -> None:
+ """Test finding misc fragments."""
+ (changelog_dir / "222.misc.md").write_text("Refactor internal code")
+
+ fragments = _find_fragments(changelog_dir.parent)
+ assert len(fragments["misc"]) == 1
+ assert "222.misc.md" in fragments["misc"]
+
+
+def test_find_fragments_ignores_template(changelog_dir: Path) -> None:
+ """Test that template files are ignored."""
+ (changelog_dir / "template.md").write_text("Template content")
+ (changelog_dir / "README.md").write_text("README content")
+ (changelog_dir / ".gitkeep").write_text("")
+
+ fragments = _find_fragments(changelog_dir.parent)
+ assert all(len(frags) == 0 for frags in fragments.values())
+
+
+def test_find_fragments_mixed_types(changelog_dir: Path) -> None:
+ """Test finding multiple fragment types."""
+ (changelog_dir / "1.feature.md").write_text("Feature")
+ (changelog_dir / "2.bugfix.md").write_text("Bugfix")
+ (changelog_dir / "3.doc.md").write_text("Doc")
+
+ fragments = _find_fragments(changelog_dir.parent)
+ assert len(fragments["feature"]) == 1
+ assert len(fragments["bugfix"]) == 1
+ assert len(fragments["doc"]) == 1
+
+
+def test_determine_bump_type_none() -> None:
+ """Test bump type with no fragments."""
+ fragments: dict[str, list[str]] = {
+ "removal": [],
+ "feature": [],
+ "deprecation": [],
+ "bugfix": [],
+ "doc": [],
+ "misc": [],
+ }
+ assert _determine_bump_type(fragments) is None
+
+
+def test_determine_bump_type_major() -> None:
+ """Test major bump with removal fragments."""
+ fragments: dict[str, list[str]] = {
+ "removal": ["1.removal.md"],
+ "feature": [],
+ "deprecation": [],
+ "bugfix": [],
+ "doc": [],
+ "misc": [],
+ }
+ assert _determine_bump_type(fragments) == "major"
+
+
+def test_determine_bump_type_major_with_others() -> None:
+ """Test major bump takes precedence over other types."""
+ fragments: dict[str, list[str]] = {
+ "removal": ["1.removal.md"],
+ "feature": ["2.feature.md"],
+ "bugfix": ["3.bugfix.md"],
+ "deprecation": [],
+ "doc": [],
+ "misc": [],
+ }
+ assert _determine_bump_type(fragments) == "major"
+
+
+def test_determine_bump_type_minor_feature() -> None:
+ """Test minor bump with feature fragments."""
+ fragments: dict[str, list[str]] = {
+ "removal": [],
+ "feature": ["1.feature.md"],
+ "deprecation": [],
+ "bugfix": [],
+ "doc": [],
+ "misc": [],
+ }
+ assert _determine_bump_type(fragments) == "minor"
+
+
+def test_determine_bump_type_minor_deprecation() -> None:
+ """Test minor bump with deprecation fragments."""
+ fragments: dict[str, list[str]] = {
+ "removal": [],
+ "feature": [],
+ "deprecation": ["1.deprecation.md"],
+ "bugfix": [],
+ "doc": [],
+ "misc": [],
+ }
+ assert _determine_bump_type(fragments) == "minor"
+
+
+def test_determine_bump_type_minor_with_patch() -> None:
+ """Test minor bump takes precedence over patch types."""
+ fragments: dict[str, list[str]] = {
+ "removal": [],
+ "feature": ["1.feature.md"],
+ "deprecation": [],
+ "bugfix": ["2.bugfix.md"],
+ "doc": ["3.doc.md"],
+ "misc": [],
+ }
+ assert _determine_bump_type(fragments) == "minor"
+
+
+def test_determine_bump_type_patch_bugfix() -> None:
+ """Test patch bump with bugfix fragments."""
+ fragments: dict[str, list[str]] = {
+ "removal": [],
+ "feature": [],
+ "deprecation": [],
+ "bugfix": ["1.bugfix.md"],
+ "doc": [],
+ "misc": [],
+ }
+ assert _determine_bump_type(fragments) == "patch"
+
+
+def test_determine_bump_type_patch_doc() -> None:
+ """Test patch bump with doc fragments."""
+ fragments: dict[str, list[str]] = {
+ "removal": [],
+ "feature": [],
+ "deprecation": [],
+ "bugfix": [],
+ "doc": ["1.doc.md"],
+ "misc": [],
+ }
+ assert _determine_bump_type(fragments) == "patch"
+
+
+def test_determine_bump_type_patch_misc() -> None:
+ """Test patch bump with misc fragments."""
+ fragments: dict[str, list[str]] = {
+ "removal": [],
+ "feature": [],
+ "deprecation": [],
+ "bugfix": [],
+ "doc": [],
+ "misc": ["1.misc.md"],
+ }
+ assert _determine_bump_type(fragments) == "patch"
+
+
+def test_determine_bump_type_patch_mixed() -> None:
+ """Test patch bump with multiple patch-level fragment types."""
+ fragments: dict[str, list[str]] = {
+ "removal": [],
+ "feature": [],
+ "deprecation": [],
+ "bugfix": ["1.bugfix.md"],
+ "doc": ["2.doc.md"],
+ "misc": ["3.misc.md"],
+ }
+ assert _determine_bump_type(fragments) == "patch"
+
+
+def test_version_from_fragments_exact(
+ changelog_dir: Path, config: _config.Configuration
+) -> None:
+ """Test version scheme when exactly on a tag."""
+ version = ScmVersion(
+ tag=Version("1.2.3"),
+ distance=0,
+ node="abc123",
+ dirty=False,
+ config=config,
+ )
+ result = version_from_fragments(version)
+ assert result == "1.2.3"
+
+
+def test_version_from_fragments_no_fragments(
+ changelog_dir: Path, config: _config.Configuration
+) -> None:
+ """Test version scheme with no fragments falls back to guess-next-dev."""
+ version = ScmVersion(
+ tag=Version("1.2.3"),
+ distance=5,
+ node="abc123",
+ dirty=False,
+ config=config,
+ )
+ result = version_from_fragments(version)
+ # Should fall back to guess_next_dev_version behavior
+ assert result.startswith("1.2.4.dev5")
+
+
+def test_version_from_fragments_major_bump(
+ changelog_dir: Path, config: _config.Configuration
+) -> None:
+ """Test version scheme with removal fragments (major bump)."""
+ (changelog_dir / "1.removal.md").write_text("Remove old API")
+
+ version = ScmVersion(
+ tag=Version("1.2.3"),
+ distance=5,
+ node="abc123",
+ dirty=False,
+ config=config,
+ )
+ result = version_from_fragments(version)
+ assert result.startswith("2.0.0.dev5")
+
+
+def test_version_from_fragments_minor_bump(
+ changelog_dir: Path, config: _config.Configuration
+) -> None:
+ """Test version scheme with feature fragments (minor bump)."""
+ (changelog_dir / "1.feature.md").write_text("Add new feature")
+
+ version = ScmVersion(
+ tag=Version("1.2.3"),
+ distance=5,
+ node="abc123",
+ dirty=False,
+ config=config,
+ )
+ result = version_from_fragments(version)
+ assert result.startswith("1.3.0.dev5")
+
+
+def test_version_from_fragments_patch_bump(
+ changelog_dir: Path, config: _config.Configuration
+) -> None:
+ """Test version scheme with bugfix fragments (patch bump)."""
+ (changelog_dir / "1.bugfix.md").write_text("Fix bug")
+
+ version = ScmVersion(
+ tag=Version("1.2.3"),
+ distance=5,
+ node="abc123",
+ dirty=False,
+ config=config,
+ )
+ result = version_from_fragments(version)
+ assert result.startswith("1.2.4.dev5")
+
+
+def test_version_from_fragments_precedence(
+ changelog_dir: Path, config: _config.Configuration
+) -> None:
+ """Test that removal > feature > bugfix precedence works."""
+ # Add all three types - removal should win
+ (changelog_dir / "1.removal.md").write_text("Remove API")
+ (changelog_dir / "2.feature.md").write_text("Add feature")
+ (changelog_dir / "3.bugfix.md").write_text("Fix bug")
+
+ version = ScmVersion(
+ tag=Version("1.2.3"),
+ distance=5,
+ node="abc123",
+ dirty=False,
+ config=config,
+ )
+ result = version_from_fragments(version)
+ # Should use major bump
+ assert result.startswith("2.0.0.dev5")
+
+
+def test_version_from_fragments_minor_over_patch(
+ changelog_dir: Path, config: _config.Configuration
+) -> None:
+ """Test that feature takes precedence over bugfix."""
+ (changelog_dir / "1.feature.md").write_text("Add feature")
+ (changelog_dir / "2.bugfix.md").write_text("Fix bug")
+
+ version = ScmVersion(
+ tag=Version("1.2.3"),
+ distance=5,
+ node="abc123",
+ dirty=False,
+ config=config,
+ )
+ result = version_from_fragments(version)
+ # Should use minor bump
+ assert result.startswith("1.3.0.dev5")
+
+
+def test_version_from_fragments_deprecation_is_minor(
+ changelog_dir: Path, config: _config.Configuration
+) -> None:
+ """Test that deprecation triggers a minor bump."""
+ (changelog_dir / "1.deprecation.md").write_text("Deprecate method")
+
+ version = ScmVersion(
+ tag=Version("1.2.3"),
+ distance=5,
+ node="abc123",
+ dirty=False,
+ config=config,
+ )
+ result = version_from_fragments(version)
+ assert result.startswith("1.3.0.dev5")
+
+
+def test_version_from_fragments_doc_is_patch(
+ changelog_dir: Path, config: _config.Configuration
+) -> None:
+ """Test that doc changes trigger a patch bump."""
+ (changelog_dir / "1.doc.md").write_text("Update docs")
+
+ version = ScmVersion(
+ tag=Version("1.2.3"),
+ distance=5,
+ node="abc123",
+ dirty=False,
+ config=config,
+ )
+ result = version_from_fragments(version)
+ assert result.startswith("1.2.4.dev5")
+
+
+def test_version_from_fragments_misc_is_patch(
+ changelog_dir: Path, config: _config.Configuration
+) -> None:
+ """Test that misc changes trigger a patch bump."""
+ (changelog_dir / "1.misc.md").write_text("Refactor")
+
+ version = ScmVersion(
+ tag=Version("1.2.3"),
+ distance=5,
+ node="abc123",
+ dirty=False,
+ config=config,
+ )
+ result = version_from_fragments(version)
+ assert result.startswith("1.2.4.dev5")
+
+
+def test_version_from_fragments_major_from_0_x(
+ changelog_dir: Path, config: _config.Configuration
+) -> None:
+ """Test major bump from 0.x version."""
+ (changelog_dir / "1.removal.md").write_text("Remove API")
+
+ version = ScmVersion(
+ tag=Version("0.5.3"),
+ distance=5,
+ node="abc123",
+ dirty=False,
+ config=config,
+ )
+ result = version_from_fragments(version)
+ assert result.startswith("1.0.0.dev5")
+
+
+def test_version_from_fragments_minor_from_0_x(
+ changelog_dir: Path, config: _config.Configuration
+) -> None:
+ """Test minor bump from 0.x version."""
+ (changelog_dir / "1.feature.md").write_text("Add feature")
+
+ version = ScmVersion(
+ tag=Version("0.5.3"),
+ distance=5,
+ node="abc123",
+ dirty=False,
+ config=config,
+ )
+ result = version_from_fragments(version)
+ assert result.startswith("0.6.0.dev5")
+
+
+def test_version_from_fragments_missing_changelog_dir(
+ config: _config.Configuration,
+) -> None:
+ """Test version scheme when changelog.d directory doesn't exist."""
+ version = ScmVersion(
+ tag=Version("1.2.3"),
+ distance=5,
+ node="abc123",
+ dirty=False,
+ config=config,
+ )
+ # Should fall back to guess-next-dev when directory is missing
+ result = version_from_fragments(version)
+ assert result.startswith("1.2.4.dev5")
+
+
+def test_version_from_fragments_dirty(
+ changelog_dir: Path, config: _config.Configuration
+) -> None:
+ """Test version scheme with dirty working directory."""
+ (changelog_dir / "1.feature.md").write_text("Add feature")
+
+ version = ScmVersion(
+ tag=Version("1.2.3"),
+ distance=5,
+ node="abc123",
+ dirty=True,
+ config=config,
+ )
+ result = version_from_fragments(version)
+ # Should still bump correctly, dirty flag affects local version
+ assert result.startswith("1.3.0.dev5")
diff --git a/testing/test_functions.py b/vcs-versioning/testing_vcs/test_version_schemes.py
similarity index 56%
rename from testing/test_functions.py
rename to vcs-versioning/testing_vcs/test_version_schemes.py
index b6b8a59e..6c03a8f6 100644
--- a/testing/test_functions.py
+++ b/vcs-versioning/testing_vcs/test_version_schemes.py
@@ -1,21 +1,12 @@
-from __future__ import annotations
-
-import shutil
-import subprocess
+"""Tests for core version scheme and formatting functionality."""
-from pathlib import Path
+from __future__ import annotations
import pytest
-
-from setuptools_scm import Configuration
-from setuptools_scm import dump_version
-from setuptools_scm import get_version
-from setuptools_scm._overrides import PRETEND_KEY
-from setuptools_scm._run_cmd import has_command
-from setuptools_scm.version import format_version
-from setuptools_scm.version import guess_next_version
-from setuptools_scm.version import meta
-from setuptools_scm.version import tag_to_version
+from vcs_versioning import Configuration
+from vcs_versioning._run_cmd import has_command
+from vcs_versioning._scm_version import meta, tag_to_version
+from vcs_versioning._version_schemes import format_version, guess_next_version
c = Configuration()
@@ -179,88 +170,17 @@ def test_format_version_with_build_metadata(
assert result == expected, f"Expected {expected}, got {result}"
-def test_dump_version_doesnt_bail_on_value_error(tmp_path: Path) -> None:
- write_to = "VERSION"
- version = str(VERSIONS["exact"].tag)
- scm_version = meta(VERSIONS["exact"].tag, config=c)
- with pytest.raises(ValueError, match=r"^bad file format:"):
- dump_version(tmp_path, version, write_to, scm_version=scm_version)
-
-
@pytest.mark.parametrize(
- "version", ["1.0", "1.2.3.dev1+ge871260", "1.2.3.dev15+ge871260.d20180625"]
+ ("tag", "expected_version"),
+ [
+ ("1.1", "1.1"),
+ ("release-1.1", "1.1"),
+ pytest.param("3.3.1-rc26", "3.3.1rc26", marks=pytest.mark.issue(266)),
+ ],
)
-def test_dump_version_works_with_pretend(
- version: str, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
-) -> None:
- monkeypatch.setenv(PRETEND_KEY, version)
- name = "VERSION.txt"
- target = tmp_path.joinpath(name)
- get_version(root=tmp_path, write_to=name)
- assert target.read_text(encoding="utf-8") == version
-
-
-def test_dump_version_modern(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
- version = "1.2.3"
- monkeypatch.setenv(PRETEND_KEY, version)
- name = "VERSION.txt"
-
- project = tmp_path.joinpath("project")
- target = project.joinpath(name)
- project.mkdir()
-
- get_version(root="..", relative_to=target, version_file=name)
- assert target.read_text(encoding="utf-8") == version
-
-
-def dump_a_version(tmp_path: Path) -> None:
- from setuptools_scm._integration.dump_version import write_version_to_path
-
- version = "1.2.3"
- scm_version = meta(version, config=c)
- write_version_to_path(
- tmp_path / "VERSION.py", template=None, version=version, scm_version=scm_version
- )
-
-
-def test_dump_version_on_old_python(tmp_path: Path) -> None:
- python37 = shutil.which("python3.7")
- if python37 is None:
- pytest.skip("python3.7 not found")
- dump_a_version(tmp_path)
- subprocess.run(
- [python37, "-c", "import VERSION;print(VERSION.version)"],
- cwd=tmp_path,
- check=True,
- )
-
-
-def test_dump_version_mypy(tmp_path: Path) -> None:
- mypy = shutil.which("mypy")
- if mypy is None:
- pytest.skip("mypy not found")
- dump_a_version(tmp_path)
- subprocess.run(
- [mypy, "--python-version=3.8", "--strict", "VERSION.py"],
- cwd=tmp_path,
- check=True,
- )
-
-
-def test_dump_version_flake8(tmp_path: Path) -> None:
- flake8 = shutil.which("flake8")
- if flake8 is None:
- pytest.skip("flake8 not found")
- dump_a_version(tmp_path)
- subprocess.run([flake8, "VERSION.py"], cwd=tmp_path, check=True)
-
-
-def test_dump_version_ruff(tmp_path: Path) -> None:
- ruff = shutil.which("ruff")
- if ruff is None:
- pytest.skip("ruff not found")
- dump_a_version(tmp_path)
- subprocess.run([ruff, "check", "--no-fix", "VERSION.py"], cwd=tmp_path, check=True)
+def test_tag_to_version(tag: str, expected_version: str) -> None:
+ version = str(tag_to_version(tag, c))
+ assert version == expected_version
def test_has_command() -> None:
@@ -280,74 +200,3 @@ def test_has_command_logs_stderr(caplog: pytest.LogCaptureFixture) -> None:
if "returned non-zero. This is stderr" in record.message:
found_it = True
assert found_it, "Did not find expected log record for "
-
-
-@pytest.mark.parametrize(
- ("tag", "expected_version"),
- [
- ("1.1", "1.1"),
- ("release-1.1", "1.1"),
- pytest.param("3.3.1-rc26", "3.3.1rc26", marks=pytest.mark.issue(266)),
- ],
-)
-def test_tag_to_version(tag: str, expected_version: str) -> None:
- version = str(tag_to_version(tag, c))
- assert version == expected_version
-
-
-def test_write_version_to_path_deprecation_warning_none(tmp_path: Path) -> None:
- """Test that write_version_to_path warns when scm_version=None is passed."""
- from setuptools_scm._integration.dump_version import write_version_to_path
-
- target_file = tmp_path / "version.py"
-
- # This should raise a deprecation warning when scm_version=None is explicitly passed
- with pytest.warns(
- DeprecationWarning, match="write_version_to_path called without scm_version"
- ):
- write_version_to_path(
- target=target_file,
- template=None, # Use default template
- version="1.2.3",
- scm_version=None, # Explicitly passing None should warn
- )
-
- # Verify the file was created and contains the expected content
- assert target_file.exists()
- content = target_file.read_text(encoding="utf-8")
-
- # Check that the version is correctly formatted
- assert "__version__ = version = '1.2.3'" in content
- assert "__version_tuple__ = version_tuple = (1, 2, 3)" in content
-
- # Check that commit_id is set to None when scm_version is None
- assert "__commit_id__ = commit_id = None" in content
-
-
-def test_write_version_to_path_deprecation_warning_missing(tmp_path: Path) -> None:
- """Test that write_version_to_path warns when scm_version parameter is not provided."""
- from setuptools_scm._integration.dump_version import write_version_to_path
-
- target_file = tmp_path / "version.py"
-
- # This should raise a deprecation warning when scm_version is not provided
- with pytest.warns(
- DeprecationWarning, match="write_version_to_path called without scm_version"
- ):
- write_version_to_path(
- target=target_file,
- template=None, # Use default template
- version="1.2.3",
- # scm_version not provided - should warn
- )
-
- # Verify the file was created and contains the expected content
- assert target_file.exists()
- content = target_file.read_text(encoding="utf-8")
-
- # Check that the version is correctly formatted
- assert "__version__ = version = '1.2.3'" in content
- assert "__version_tuple__ = version_tuple = (1, 2, 3)" in content
-
- # Check that commit_id is set to None when scm_version is None
- assert "__commit_id__ = commit_id = None" in content