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 -[![github ci](https://github.com/pypa/setuptools-scm/actions/workflows/python-tests.yml/badge.svg)](https://github.com/pypa/setuptools-scm/actions/workflows/python-tests.yml) -[![Documentation Status](https://readthedocs.org/projects/setuptools-scm/badge/?version=latest)](https://setuptools-scm.readthedocs.io/en/latest/?badge=latest) -[![tidelift](https://tidelift.com/badges/package/pypi/setuptools-scm) ](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 +[![github ci](https://github.com/pypa/setuptools-scm/actions/workflows/python-tests.yml/badge.svg)](https://github.com/pypa/setuptools-scm/actions/workflows/python-tests.yml) +[![Documentation Status](https://readthedocs.org/projects/setuptools-scm/badge/?version=latest)](https://setuptools-scm.readthedocs.io/en/latest/?badge=latest) +[![tidelift](https://tidelift.com/badges/package/pypi/setuptools-scm) ](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