pax_global_header00006660000000000000000000000064147664766730014544gustar00rootroot0000000000000052 comment=94f36a0035b4a1dfb31b5b34086ad8f0860f86aa setuptools-scm-8.2.1/000077500000000000000000000000001476647667300145555ustar00rootroot00000000000000setuptools-scm-8.2.1/.git_archival.txt000066400000000000000000000001521476647667300200260ustar00rootroot00000000000000node: 94f36a0035b4a1dfb31b5b34086ad8f0860f86aa node-date: 2025-03-19T09:18:03+01:00 describe-name: v8.2.1 setuptools-scm-8.2.1/.gitattributes000066400000000000000000000000401476647667300174420ustar00rootroot00000000000000.git_archival.txt export-subst setuptools-scm-8.2.1/.github/000077500000000000000000000000001476647667300161155ustar00rootroot00000000000000setuptools-scm-8.2.1/.github/FUNDING.yml000066400000000000000000000000361476647667300177310ustar00rootroot00000000000000tidelift: pypi/setuptools-scm setuptools-scm-8.2.1/.github/dependabot.yml000066400000000000000000000003151476647667300207440ustar00rootroot00000000000000version: 2 updates: - package-ecosystem: "github-actions" directory: "/" schedule: interval: "weekly" - package-ecosystem: "pip" directory: "/" schedule: interval: "weekly" setuptools-scm-8.2.1/.github/release.yml000066400000000000000000000001141476647667300202540ustar00rootroot00000000000000changelog: exclude: authors: - dependabot - pre-commit-ci setuptools-scm-8.2.1/.github/workflows/000077500000000000000000000000001476647667300201525ustar00rootroot00000000000000setuptools-scm-8.2.1/.github/workflows/python-tests.yml000066400000000000000000000103131476647667300233540ustar00rootroot00000000000000name: python tests+artifacts+release on: pull_request: push: branches: - "*" tags: - "v*" concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }} cancel-in-progress: true env: FORCE_COLOR: 1 jobs: package: name: Build & inspect our package. runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - uses: hynek/build-and-inspect-python-package@v2 test: needs: [package] runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: python_version: [ '3.8', '3.9', '3.10', '3.11', '3.12', '3.13', 'pypy-3.10' ] os: [windows-latest, ubuntu-latest] #, macos-latest] include: - os: windows-latest python_version: 'msys2' name: ${{ matrix.os }} - Python ${{ matrix.python_version }} steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - name: Setup python uses: actions/setup-python@v5 if: matrix.python_version != 'msys2' with: python-version: ${{ matrix.python_version }} architecture: x64 - name: Setup MSYS2 uses: msys2/setup-msys2@v2 if: matrix.python_version == 'msys2' with: msystem: MINGW64 install: git mingw-w64-x86_64-python mingw-w64-x86_64-python-setuptools update: true - name: Setup GnuPG # At present, the Windows VMs only come with the copy of GnuPG that's bundled # with Git for Windows. If we want to use this version _and_ be able to set # arbitrary GnuPG home directories, then the test would need to figure out when # to convert Windows-style paths into Unix-style paths with cygpath, which is # unreasonable. # # Instead, we'll install a version of GnuPG that can handle Windows-style paths. # However, due to , installation fails if the PATH # environment variable is too long. Consequently, we need to shorten PATH to # something minimal before we can install GnuPG. For further details, see # . # # Additionally, we'll explicitly set `gpg.program` to ensure Git for Windows # doesn't invoke the bundled GnuPG, otherwise we'll run into # . See also: . run: | $env:PATH = "C:\Program Files\Git\bin;C:\Windows\system32;C:\Windows;C:\Windows\System32\Wbem;C:\Windows\System32\WindowsPowerShell\v1.0\;C:\ProgramData\Chocolatey\bin" [Environment]::SetEnvironmentVariable("Path", $env:PATH, "Machine") choco install gnupg -y --no-progress echo "C:\Program Files (x86)\gnupg\bin" >> $env:GITHUB_PATH git config --system gpg.program "C:\Program Files (x86)\gnupg\bin\gpg.exe" if: runner.os == 'Windows' - run: pip install -U 'setuptools>=61' - uses: actions/download-artifact@v4 with: name: Packages path: dist - shell: bash run: pip install "$(echo -n dist/*whl)[toml,test]" - 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: pytest timeout-minutes: 15 dist_upload: runs-on: ubuntu-latest if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags') permissions: id-token: write needs: [test] steps: - uses: actions/download-artifact@v4 with: name: Packages path: dist - name: Publish package to PyPI uses: pypa/gh-action-pypi-publish@release/v1 test-pypi-upload: runs-on: ubuntu-latest needs: [test] permissions: id-token: write steps: - uses: actions/download-artifact@v4 with: name: Packages path: dist - name: Publish package to PyPI continue-on-error: true uses: pypa/gh-action-pypi-publish@release/v1 with: repository-url: https://test.pypi.org/legacy/ setuptools-scm-8.2.1/.gitignore000066400000000000000000000012071476647667300165450ustar00rootroot00000000000000### JetBrains template # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion *.iml ## Directory-based project format: .idea/ ### Other editors .*.swp .vscode/ ### Python template # Byte-compiled / optimized __pycache__/ *.py[cod] *$py.class # Distribution / packaging .env/ env/ .venv/ venv/ build/ dist/ .eggs/ lib/ lib64/ *.egg-info/ # Installer logs pip-log.txt pip-delete-this-directory.txt pip-wheel-metadata # Unit test / coverage reports htmlcov/ .tox/ .coverage .coverage.* .cache .pytest_cache .mypy_cache/ .entangled/ nosetests.xml coverage.xml *,cover .hypothesis/ # Sphinx documentation docs/_build/ setuptools-scm-8.2.1/.pre-commit-config.yaml000066400000000000000000000015001476647667300210320ustar00rootroot00000000000000repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v5.0.0 hooks: - id: trailing-whitespace - id: check-yaml - id: debug-statements - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.11.0 hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix, --show-fixes] - id: ruff-format - repo: https://github.com/pre-commit/mirrors-mypy rev: v1.15.0 hooks: - id: mypy args: [--strict] language_version: "3.10" additional_dependencies: - types-setuptools - tokenize-rt==3.2.0 - pytest == 7.1 - importlib_metadata - typing-extensions>=4.5 - rich - repo: https://github.com/scientific-python/cookie rev: 2025.01.22 hooks: - id: sp-repo-review setuptools-scm-8.2.1/.readthedocs.yaml000066400000000000000000000004021476647667300200000ustar00rootroot00000000000000version: 2 build: os: ubuntu-22.04 tools: python: "3.11" mkdocs: configuration: mkdocs.yml # Optionally declare the Python requirements required to build your docs python: install: - method: pip path: . extra_requirements: - docssetuptools-scm-8.2.1/CHANGELOG.md000066400000000000000000000547641476647667300164060ustar00rootroot00000000000000# Changelog ## v8.2.1 ### Fixed - fix #1119: also include pre/post release details in version_tuple - fix #1112: unpin setuptools for own dependencies due to ubuntu lts bugs - add python 3.13 to the support matrix ## v8.2.0 ### Added - fix #960: add a ``--force-write-version-files`` flag for the cli ### Changed - fix #950: ensure to pass encodings to io usage - fix #957: add subprocess timeout control env var - add sp-repo-review pre-commit hook ### Fixed - fix #1018: allow non-normalized versions for semver - fix #1103: respect GIT_CEILING_DIRECTORIES when trying to find git toplevels - fix #1081: add name normalized pipx entrypoint - fix #1080: clean pdm from PYTHONPATH to protect mercurial ## v8.1.0 ### Changed - inclusion of `__all__` in autogenerated `version.py` files to aid IDE autoimports ## v8.0.4 ### Changed - introduce scriv for changelog management - reconfigure local build backend to use an attribute instead of star imports from setuptools - introduce ruff as a linter - ensure the setuptools version keyword correctly load pyproject.toml configuration - add build and wheel to the test requirements for regression testing - move internal toml handling to own module ### Fixed - fix #925: allow `write_to` to be an absolute path when it's a subdirectory of the root - fix #932: ensure type annotations in version file don't cause linter issues - fix #930: temporary restore `DEFAULT_VERSION_SCHEME` and `DEFAULT_LOCAL_SCHEME` on the `setuptools-scm` package ## v8.0.3 ### Fixed - fix #918 for good - remove external importlib-metadata to avoid source only loop - fix #926: ensure mypy on python3.8 works with the version file ## v8.0.2 ### Fixed - fix #919: restore legacy version-file behaviour for external callers + add Deprecation warning - fix #918: use packaging from setuptools for self-build - fix #914: ignore the deprecated git archival plugin as its integrated now - fix #912: ensure mypy safety of the version template + regression test - fix #913: use 240s timeout instead of 20 for `git unshallow` to account for large repos or slow connections ## v8.0.1 ### Fixed - update version file template to work on older python versions by using type comments - ensure tag regex from setup.py is parsed into regex ## v8.0.0 ### breaking - remove legacy version parser api - config arg always required - turn Configuration into a dataclass - require configuration to always pass into helpers - hide file-finders implementation in private module - renamed setuptools_scm.hacks to setuptools_scm.fallbacks and drop support for pip-egg-info - remove trace function and use logging instead - unify `distance=None` and `distance=0` they should mean the same andwhere hiding dirty states that are now explicitly dirty - depend on later importlib for the full selectable api - move setuptools integration code to private sub-package - use normalized dist names for the `SETUPTOOLS_SCM_PRETEND_VERSION_FOR_${DIST_NAME}` env var - drop support for python 3.7 - introduce `version_file` as replacement for `write_to` - renameed the project from `setuptools_scm` to `setuptools-scm` ### Added - created a directory for the vcs-versioning package and added it to pypi - git: expect main as possible default branch - drop version_from_scm helper - trim down exposed public api - no longer self-call twice in setuptools - add support for version schemes by import - chores - migrate own metadata to pyproject.toml - consolidate version schemes - stricter tag typing - pre-compiled regex - move helpers to private modules - support passing log levels to SETUPTOOLS_SCM_DEBUG - support using rich.logging as console log handler if installed - fix #527: type annotation in default version template - fix #549: use fallbacks when scm search raises CommandNotFoundError ### Fixed - fix #883: use HeadersParser to ensure only mime metadata in headers is used - fix #884: parse calver dates from versions with the v prefix - don't use a C locale without UTF-8 support, when running commands. ## v7.1.0 - \#748: use `tomllib` from stdlib - fix #762: handle non-ascii in setup.cfg - \#752: implement fallback file finders for archives - \#765: removed coding header in python template - declared Python 3.11 support - fix #759: update .git_archival.txt templates match git-describe invocation - fix #772: fix handling of .git-archival.txt from tagged commit ## v7.0.5 - fixes #742, #745: correctly handle accidentally released archival files ## v7.0.4 - fix #727: correctly handle incomplete archival from setuptools_scm_git_archival - fix #691: correctly handle specifying root in pyproject.toml - correct root override check condition (to ensure absolute path matching) - allow root by the cli to be considered relative to the cli (using abspath) ## v7.0.3 - fix mercurial usage when pip primes a isolated environment - fix regression for branch names on git + add a test ## v7.0.2 - fix #723 and #722: remove bootstrap dependencies - ensure we read the distribution name from `setup.cfg` if needed even for `pyproject.toml` ## v7.0.1 - fix #718: Avoid `ModuleNotFoundError` by requiring `importlib_metadata` in `python<3.8` ## v7.0.0 - drop python 3.6 support - include git archival support - fix #707: support git version detection even when git protects against mismatched owners (common with misconfigured containers, thanks @chrisburr ) - fix #548: correctly handle parsing the commit timestamp of HEAD when `log.showSignature` is set ## v6.4.2 - fix #671: `NoReturn` is not available in painfully dead python 3.6 ## v6.4.1 - fix regression #669: restore get_version signature - fix #668: harden the self-test for distribution extras ## 6.4.0 - compatibility adjustments for setuptools \>58 - only put minimal setuptools version into toml extra to warn people with old strict pins - correctly handle hg-git self-use - better mercurial detection - modernize packaging setup - python 3.10 support - better handling of setuptools install command deprecation - consider `pyproject.tomls` when running as command - use list in git describe command to avoid shell expansions while supporting both windows and posix - add `--strip-dev` flag to `python -m setuptools_scm` to print the next guessed version cleanly - ensure no-guess-dev will fail on bad tags instead of generating invalid versions - ensure we use utc everywhere to avoid confusion ## v6.3.2 - fix #629: correctly convert Version data in tags_to_version parser to avoid errors ## v6.3.1 - fix #625: restore tomli in install_requires after the regression changes in took it out and some users never added it even tho they have pyproject.toml files ## v6.3.0 ### warning This release explicitly warns on unsupported setuptools. This unfortunately has to happen as the legacy `setup_requires` mechanism incorrectly configures the setuptools working-set when a more recent setuptools version than available is required. As all releases of setuptools are affected as the historic mechanism for ensuring a working setuptools setup was shipping a `ez_setup` file next to `setup.py`, which would install the required version of setuptools. This mechanism has long since been deprecated and removed as most people haven\'t been using it ### Fixed - fix #612: depend on packaging to ensure version parsing parts - fix #611: correct the typo that hid away the toml extra and add it in `setup.py` as well - fix #615: restore support for the git_archive plugin which doesn't pass over the config - restore the ability to run on old setuptools while to avoid breaking pipelines ## v6.2.0 - fix #608: resolve tomli dependency issue by making it a hard dependency as all intended/supported install options use pip/wheel this is only a feature release - ensure python 3.10 works ## v6.1.1 - fix #605: completely disallow bdist_egg - modern enough setuptools\>=45 uses pip - fix #606: re-integrate and harden toml parsing - fix #597: harden and expand support for figuring the current distribution name from [pyproject.toml]{.title-ref} ([project.name]{.title-ref} or [tool.setuptools_scm.dist_name]{.title-ref}) section or [setup.cfg]{.title-ref} ([metadata.name]{.title-ref}) ## v6.1.0 - fix #587: don\'t fail file finders when distribution is not given - fix #524: new parameters `normalize` and `version_cls` to customize the version normalization class. - fix #585: switch from toml to tomli for toml 1.0 support - fix #591: allow to opt in for searching parent directories in the api - fix #589: handle yaml encoding using the expected defaults - fix #575: recommend storing the version_module inside of `mypkg/_version.py` - fix #571: accept branches starting with `v` as release branches - fix #557: Use `packaging.version` for `version_tuple` - fix #544: enhance errors on unsupported python/setuptools versions ## v6.0.1 - fix #537: drop node_date on old git to avoid errors on missing %cI ## v6.0.0 - fix #517: drop dead python support \>3.6 required - drop dead setuptools support \> 45 required (can install wheels) - drop egg building (use wheels) - add git node_date metadata to get the commit time-stamp of HEAD - allow version schemes to be priority ordered lists of version schemes - support for calendar versioning (calver) by date ## v5.0.2 - fix #415: use git for matching prefixes to support the windows situation ## v5.0.1 - fix #509: support `SETUPTOOLS_SCM_PRETEND_VERSION_FOR_${DISTRIBUTION_NAME}` for `pyproject.toml` ## v5.0.0 ### Breaking changes - fix #339: strict errors on missing scm when parsing a scm dir to avoid false version lookups - fix #337: if relative_to is a directory instead of a file, consider it as direct target instead of the containing folder and print a warning ### Fixed - fix #352: add support for generally ignoring specific vcs roots - fix #471: better error for version bump failing on complex but accepted tag - fix #479: raise indicative error when tags carry non-parsable information - Add `no-guess-dev` which does no next version guessing, just adds `.post1.devN` in case there are new commits after the tag - add python3.9 - enhance documentation - consider SOURCE_DATE_EPOCH for versioning - add a version_tuple to write_to templates - fix #321: add support for the `SETUPTOOLS_SCM_PRETEND_VERSION_FOR_${DISTRIBUTION_NAME}` env var to target the pretend key - fix #142: clearly list supported scm - fix #213: better error message for non-zero dev numbers in tags - fix #356: add git branch to version on describe failure ## v4.1.2 - disallow git tags without dots by default again - #449 ## v4.1.1 - drop jaraco.windows from pyproject.toml, allows for wheel builds on python2 ## v4.1.0 - include python 3.9 via the deadsnakes action - return release_branch_semver scheme (it got dropped in a bad rebase) - undo the devendoring of the samefile backport for python2.7 on windows - re-enable the building of universal wheels - fix handling of missing git/hg on python2.7 (python 3 exceptions where used) - correct the tox flake8 invocation - trigger builds on tags again ## v4.0.0 - Add `parentdir_prefix_version` to support installs from GitHub release tarballs. - use Coordinated Universal Time (UTC) - switch to github actions for ci - fix documentation for `tag_regex` and add support for single digit versions - document handling of enterprise distros with unsupported setuptools versions #312 - switch to declarative metadata - drop the internal copy of samefile and use a dependency on jaraco.windows on legacy systems - select git tags based on the presence of numbers instead of dots - enable getting a version form a parent folder prefix - add release-branch-semver version scheme - make global configuration available to version metadata - drop official support for python 3.4 ## v3.5.0 - add `no-local-version` local scheme and improve documentation for schemes ## v3.4.4 - fix #403: also sort out resource warnings when dealing with git file finding ## v3.4.3 - fix #399: ensure the git file finder terminates subprocess after reading archive ## v3.4.2 - fix #395: correctly transfer tag regex in the Configuration constructor - rollback \--first-parent for git describe as it turns out to be a regression for some users ## v3.4.1 - pull in #377 to fix #374: correctly set up the default version scheme for pyproject usage. this bugfix got missed when rushing the release. ## v3.4.0 - fix #181 - add support for projects built under setuptools declarative config by way of the setuptools.finalize_distribution_options hook in Setuptools 42. - fix #305 - ensure the git file finder closes file descriptors even when errors happen - fix #381 - clean out env vars from the git hook system to ensure correct function from within - modernize docs wrt importlib.metadata *edited* - use \--first-parent for git describe ## v3.3.3 - add eggs for python3.7 and 3.8 to the deploy ## v3.3.2 - fix #335 - fix python3.8 support and add builds for up to python3.8 ## v3.3.1 - fix #333 (regression from #198) - use a specific fallback root when calling fallbacks. Remove old hack that resets the root when fallback entrypoints are present. ## v3.3.0 - fix #198 by adding the `fallback_version` option, which sets the version to be used when everything else fails. ## v3.2.0 \* fix #303 and #283 by adding the option `git_describe_command` to allow the user to control the way that [git describe]{.title-ref} is called. ## v3.1.0 - fix #297 - correct the invocation in version_from_scm and deprecate it as its exposed by accident - fix #298 - handle git file listing on empty repositories - fix #268 - deprecate ScmVersion.extra ## v3.0.6 - fix #295 - correctly handle self install from tarballs ## v3.0.5 - fix #292 - match leading `V` character as well ## v3.0.4 - re-release of 3.0.3 after fixing the release process v3.0.3 (pulled from pypi due to a packaging issue) ====== - fix #286 - duo an oversight a helper function was returning a generator instead of a list ## v3.0.2 - fix a regression from tag parsing - support for multi-dashed prefixes - #284 ## v3.0.1 - fix a regression in setuptools_scm.git.parse - reorder arguments so the positional invocation from before works as expected #281 ## v3.0.0 - introduce pre-commit and use black - print the origin module to help testing - switch to src layout (breaking change) - no longer alias tag and parsed_version in order to support understanding a version parse failure - require parse results to be ScmVersion or None (breaking change) - fix #266 by requiring the prefix word to be a word again (breaking change as the bug allowed arbitrary prefixes while the original feature only allowed words\") - introduce an internal config object to allow the configuration for tag parsing and prefixes (thanks to \@punkadiddle for introducing it and passing it through) ## v2.1.0 - enhance docs for sphinx usage - add symlink support to file finder for git #247 (thanks Stéphane Bidoul) - enhance tests handling win32 (thanks Stéphane Bidoul) ## v2.0.0 - fix #237 - correct imports in code examples - improve mercurial commit detection (thanks Aaron) - breaking change: remove support for setuptools before parsed versions - reintroduce manifest as the travis deploy can\'t use the file finder - reconfigure flake8 for future compatibility with black - introduce support for branch name in version metadata and support a opt-in simplified semver version scheme ## v1.17.0 - fix regression in git support - use a function to ensure it works in egg installed mode - actually fail if file finding fails in order to see broken setups instead of generating broken dists (thanks Mehdi ABAAKOUK for both) ## v1.16.2 - fix regression in handling git export ignores (thanks Mehdi ABAAKOUK) ## v1.16.1 - fix regression in support for old setuptools versions (thanks Marco Clemencic) ## v1.16.0 - drop support for eol python versions - #214 - fix misuse in surrogate-escape api - add the node-and-timestamp local version scheme - respect git export ignores - avoid shlex.split on windows - fix #218 - better handling of mercurial edge-cases with tag commits being considered as the tagged commit - fix #223 - remove the dependency on the internal `SetuptoolsVersion` as it was removed after long-standing deprecation ## v1.15.7 - Fix #174 with #207: Reuse samefile backport as developed in jaraco.windows, and only use the backport where samefile is not available. ## v1.15.6 - fix #171 by unpinning the py version to allow a fixed one to get installed ## v1.15.5 - fix #167 by correctly respecting preformatted version metadata from PKG-INFO/EGG-INFO ## v1.15.4 - fix issue #164: iterate all found entry points to avoid errors when pip remakes egg-info - enhance self-use to enable pip install from github again ## v1.15.3 - bring back correctly getting our version in the own sdist, finalizes #114 - fix issue #150: strip local components of tags ## v1.15.2 - fix issue #128: return None when a scm specific parse fails in a worktree to ease parse reuse ## v1.15.1 - fix issue #126: the local part of any tags is discarded when guessing new versions - minor performance optimization by doing fewer git calls in the usual cases ## v1.15.0 - more sophisticated ignoring of mercurial tag commits when considering distance in commits (thanks Petre Mierlutiu) - fix issue #114: stop trying to be smart for the sdist and ensure it's always correctly using itself - update trove classifiers - fix issue #84: document using the installed package metadata for sphinx - fix issue #81: fail more gracious when git/hg are missing - address issue #93: provide an experimental api to customize behavior on shallow git repos a custom parse function may pick pre parse actions to do when using git ## v1.14.1 - fix #109: when detecting a dirty git workdir don't consider untracked file (this was a regression due to #86 in v1.13.1) - consider the distance 0 when the git node is unknown (happens when you haven't committed anything) ## v1.14.0 - publish bdist_egg for python 2.6, 2.7 and 3.3-3.5 - fix issue #107 - dont use node if it is None ## v1.13.1 - fix issue #86 - detect dirty git workdir without tags ## v1.13.0 - fix regression caused by the fix of #101 - assert types for version dumping - strictly pass all versions through parsed version metadata ## v1.12.0 - fix issue #97 - add support for mercurial plugins - fix issue #101 - write version cache even for pretend version (thanks anarcat for reporting and fixing) ## v1.11.1 - fix issue #88 - better docs for sphinx usage (thanks Jason) - fix issue #89 - use normpath to deal with windows (thanks Te-jé Rodgers for reporting and fixing) ## v1.11.0 - always run tag_to_version so in order to handle prefixes on old setuptools (thanks to Brian May) - drop support for python 3.2 - extend the error message on missing scm metadata (thanks Markus Unterwaditzer) - fix bug when using callable version_scheme (thanks Esben Haabendal) ## v1.10.1 - fix issue #73 - in hg pre commit merge, consider parent1 instead of failing ## v1.10.0 - add support for overriding the version number via the environment variable SETUPTOOLS_SCM_PRETEND_VERSION - fix issue #63 by adding the `--match` parameter to the git describe call and prepare the possibility of passing more options to scm backends - fix issue #70 and #71 by introducing the parse keyword to specify custom scm parsing, it's an expert feature, use with caution this change also introduces the setuptools_scm.parse_scm_fallback entrypoint which can be used to register custom archive fallbacks ## v1.9.0 - Add `relative_to` parameter to `get_version` function, fixes #44 per #45. ## v1.8.0 - fix issue with setuptools wrong version warnings being printed to standard out. User is informed now by distutils-warnings. - restructure root finding, we now reliably ignore outer scm and prefer PKG-INFO over scm, fixes #43 and #45 ## v1.7.0 - correct the url to github thanks David Szotten - enhance scm not found errors with a note on git tarballs thanks Markus - add support for `write_to_template` ## v1.6.0 - bail out early if the scm is missing this brings issues with git tarballs and older devpi-client releases to light, before we would let the setup stay at version 0.0, now there is a ValueError - properly raise errors on write_to misuse (thanks Te-jé Rodgers) ## v1.5.5 - Fix bug on Python 2 on Windows when environment has unicode fields. ## v1.5.4 - Fix bug on Python 2 when version is loaded from existing metadata. ## v1.5.3 - #28: Fix decoding error when PKG-INFO contains non-ASCII. ## v1.5.2 - add zip_safe flag ## v1.5.1 - fix file access bug I missed in 1.5 ## v1.5.0 - moved setuptools integration related code to own file - support storing version strings into a module/text file using the `write_to` configuration parameter ## v1.4.0 - proper handling for sdist - fix file-finder failure from windows - reshuffle docs ## v1.3.0 - support setuptools easy_install egg creation details by hardwire-ing the version in the sdist ## v1.2.0 - enhance self-use ## v1.1.0 - enable self-use ## v1.0.0 - documentation enhancements ## v0.26 - rename to setuptools_scm - split into package, add lots of entry points for extension - pluggable version schemes ## v0.25 - fix pep440 support this reshuffles the complete code for version guessing ## v0.24 - don't drop dirty flag on node finding - fix distance for dirty flagged versions - use dashes for time again, its normalisation with setuptools - remove the own version attribute, it was too fragile to test for - include file finding - handle edge cases around dirty tagged versions ## v0.23 - windows compatibility fix (thanks stefan) drop samefile since it`s missing in some python2 versions on windows - add tests to the source tarballs ## v0.22 - windows compatibility fix (thanks stefan) use samefile since it does path normalisation ## v0.21 - fix the own version attribute (thanks stefan) ## v0.20 - fix issue 11: always take git describe long format to avoid the source of the ambiguity - fix issue 12: add a `__version__` attribute via pkginfo ## v0.19 - configurable next version guessing - fix distance guessing (thanks stefan) setuptools-scm-8.2.1/LICENSE000066400000000000000000000017771476647667300155760ustar00rootroot00000000000000Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. setuptools-scm-8.2.1/MANIFEST.in000066400000000000000000000010321476647667300163070ustar00rootroot00000000000000exclude *.nix exclude .pre-commit-config.yaml exclude changelog.d/* exclude .git_archival.txt exclude .readthedocs.yaml include *.py include testing/*.py include tox.ini include *.rst include LICENSE include *.toml include mypy.ini include testing/Dockerfile.* include src/setuptools_scm/.git_archival.txt include README.md include CHANGELOG.md recursive-include testing *.bash prune nextgen recursive-include docs *.md include docs/examples/version_scheme_code/*.py include docs/examples/version_scheme_code/*.toml include mkdocs.yml setuptools-scm-8.2.1/README.md000066400000000000000000000072071476647667300160420ustar00rootroot00000000000000# 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]. ## `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. 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>=64", "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] ``` 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. ## 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. setuptools-scm-8.2.1/_own_version_helper.py000066400000000000000000000033161476647667300212000ustar00rootroot00000000000000""" 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 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 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: return get_version( relative_to=__file__, parse=parse, version_scheme=guess_next_dev_version, local_scheme=get_local_node_and_date, ) version: str def __getattr__(name: str) -> str: if name == "version": global version version = scm_version() return version raise AttributeError(name) setuptools-scm-8.2.1/changelog.d/000077500000000000000000000000001476647667300167265ustar00rootroot00000000000000setuptools-scm-8.2.1/changelog.d/.keep000066400000000000000000000000001476647667300176410ustar00rootroot00000000000000setuptools-scm-8.2.1/docs/000077500000000000000000000000001476647667300155055ustar00rootroot00000000000000setuptools-scm-8.2.1/docs/changelog.md000066400000000000000000000000541476647667300177550ustar00rootroot00000000000000{% include-markdown "../CHANGELOG.md" %} setuptools-scm-8.2.1/docs/config.md000066400000000000000000000147611476647667300173050ustar00rootroot00000000000000# Configuration ## configuration parameters 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` `version_scheme : str | Callable[[ScmVersion], str]` : Configures how the version number is constructed; either an entrypoint name or a callable. See [Version number construction](extending.md#setuptools_scmversion_scheme) for predefined implementations. `local_scheme : str | Callable[[ScmVersion], str]` : Configures how the local component of the version (the optional part after the `+`) is constructed; 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][] `parentdir_prefix_version: str|None = None` : If the normal methods for detecting the version (SCM version, sdist metadata) fail, and the parent directory name starts with `parentdir_prefix_version`, then this prefix is stripped and the rest of the parent directory name is matched with `tag_regex` to get a version string. If this parameter is unset (the default), then this fallback is not used. This was intended to cover GitHub's "release tarballs", which extract into directories named `projectname-tag/` (in which case `parentdir_prefix_version` can be set e.g. to `projectname-`). `fallback_version: str | None = None` : A version string that will be used if no other method for detecting the version worked (e.g., when using a tarball with no metadata). If this is unset (the default), `setuptools-scm` will error if it fails to detect the version. `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. `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][] `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][] `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 [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). !!! 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. ## environment variables `SETUPTOOLS_SCM_PRETEND_VERSION` : used as the primary source for the version number in which case it will be an unparsed string !!! warning "" it is strongly recommended to use distribution-specific pretend versions (see below). `SETUPTOOLS_SCM_PRETEND_VERSION_FOR_${NORMALIZED_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 ``setuptools-scm`` 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 this will take precedence over ``SETUPTOOLS_SCM_PRETEND_VERSION`` `SETUPTOOLS_SCM_DEBUG` : enable the debug logging `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 (https://reproducible-builds.org/docs/source-date-epoch/) `SETUPTOOLS_SCM_IGNORE_VCS_ROOTS` : a ``os.pathsep`` separated list of directory names to ignore for root finding ## api reference ### constants ::: setuptools_scm._config.DEFAULT_TAG_REGEX options: heading_level: 4 ::: setuptools_scm.git.DEFAULT_DESCRIBE options: heading_level: 4 ### the configuration class ::: setuptools_scm.Configuration options: heading_level: 4 setuptools-scm-8.2.1/docs/customizing.md000066400000000000000000000034341476647667300204060ustar00rootroot00000000000000# Customizing ## providing project local version schemes As PEP 621 provides no way to specify local code as a build backend plugin, setuptools-scm has to piggyback on setuptools for passing functions over. To facilitate that one needs to write a `setup.py` file and pass partial setuptools-scm configuration in via the use_scm_version keyword. It's strongly recommended to experiment with using stock version schemes or creating plugins as package. (This recommendation will change if there ever is something like build-time entrypoints). ``` { .python title="setup.py" file="docs/examples/version_scheme_code/setup.py" } # we presume installed build dependencies from __future__ import annotations from setuptools import setup from setuptools_scm import ScmVersion def myversion_func(version: ScmVersion) -> str: from setuptools_scm.version import guess_next_version return version.format_next_version(guess_next_version, "{guessed}b{distance}") setup(use_scm_version={"version_scheme": myversion_func}) ``` ``` { .toml title="pyproject.toml" file="docs/examples/version_scheme_code/pyproject.toml" } [build-system] requires = ["setuptools>=64", "setuptools-scm>=8"] build-backend = "setuptools.build_meta" [project] name = "scm-example" dynamic = [ "version", ] [tool.setuptools_scm] ``` - [ ] add a build block that adds example output ## Importing in setup.py With the pep 517/518 build backend, setuptools-scm is importable from `setup.py` ``` { .python title="setup.py" } import setuptools from setuptools_scm.version import get_local_dirty_tag def clean_scheme(version): return get_local_dirty_tag(version) if version.dirty else '+clean' setup(use_scm_version={'local_scheme': clean_scheme}) ``` ## alternative version classes ::: setuptools_scm.NonNormalizedVersion setuptools-scm-8.2.1/docs/examples/000077500000000000000000000000001476647667300173235ustar00rootroot00000000000000setuptools-scm-8.2.1/docs/examples/version_scheme_code/000077500000000000000000000000001476647667300233265ustar00rootroot00000000000000setuptools-scm-8.2.1/docs/examples/version_scheme_code/pyproject.toml000066400000000000000000000004411476647667300262410ustar00rootroot00000000000000# ~/~ begin <>[init] [build-system] requires = ["setuptools>=64", "setuptools-scm>=8"] build-backend = "setuptools.build_meta" [project] name = "scm-example" dynamic = [ "version", ] [tool.setuptools_scm] # ~/~ end setuptools-scm-8.2.1/docs/examples/version_scheme_code/setup.py000066400000000000000000000007551476647667300250470ustar00rootroot00000000000000# ~/~ begin <>[init] # we presume installed build dependencies from __future__ import annotations from setuptools import setup from setuptools_scm import ScmVersion def myversion_func(version: ScmVersion) -> str: from setuptools_scm.version import guess_next_version return version.format_next_version(guess_next_version, "{guessed}b{distance}") setup(use_scm_version={"version_scheme": myversion_func}) # ~/~ end setuptools-scm-8.2.1/docs/extending.md000066400000000000000000000075511476647667300200240ustar00rootroot00000000000000# Extending setuptools-scm `setuptools-scm` uses [entry-point][entry-point] based hooks to extend its default capabilities. [entry-point]: https://packaging.python.org/en/latest/specifications/entry-points/ ## Adding a new SCM `setuptools-scm` provides two entrypoints for adding new SCMs: `setuptools_scm.parse_scm` : A function used to parse the metadata of the current workdir using the name of the control directory/file of your SCM as the 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`][]. `setuptools_scm.files_command` : Either a string containing a shell command that prints all SCM managed files in its current working directory or a callable, that given a pathname will return that list. Also uses then name of your SCM control directory as name of the entrypoint. ### api reference for scm version objects ::: setuptools_scm.version.ScmVersion options: show_root_heading: yes heading_level: 4 ::: setuptools_scm.version.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 representing the version. ### Available implementations `guess-next-dev (default)` : Automatically guesses the next development version (default). Guesses the upcoming release by incrementing the pre-release segment if present, otherwise by incrementing the micro segment. Then appends :code:`.devN`. In case the tag ends with `.dev0` the version is not bumped and custom `.devN` versions will trigger a error. `post-release (deprecated)` : Generates post release versions (adds `.postN`) after review of the version number pep this is considered a bad idea as post releases are intended to be chosen not autogenerated. !!! warning "the recommended replacement is `no-guess-dev`" `python-simplified-semver` : Basic semantic versioning. Guesses the upcoming release by incrementing the minor segment and setting the micro segment to zero if the current branch contains the string `feature`, otherwise by incrementing the micro version. Then appending `.devN`. This scheme is not compatible with pre-releases. `release-branch-semver` : Semantic versioning for projects with release branches. The same as `guess-next-dev` (incrementing the pre-release or micro segment) however when on a release branch: a branch whose name (ignoring namespace) parses as a version that matches the most recent tag up to the minor segment. Otherwise if on a non-release branch, increments the minor segment and sets the micro segment to zero, then appends `.devN` `no-guess-dev` : Does no next version guessing, just adds `.post1.devN` `only-version` : Only use the version from the tag, as given. !!! warning "This means version is no longer pseudo unique per commit" ### `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 representing the local version. Dates and times are in Coordinated Universal Time (UTC), because as part of the version, they should be location independent. #### Available implementations `node-and-date (default)` : adds the node on dev versions and the date on dirty workdir `node-and-timestamp` : like `node-and-date` but with a timestamp of the form `%Y%m%d%H%M%S` instead `dirty-tag` : adds `+dirty` if the current workdir has changes `no-local-version` : omits local version, useful e.g. because pypi does not support it setuptools-scm-8.2.1/docs/index.md000066400000000000000000000023461476647667300171430ustar00rootroot00000000000000# 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]. [git-archive-docs]: usage.md#builtin-mechanisms-for-obtaining-version-numbers ## Basic usage ### With setuptools Note: `setuptools-scm>=8` intentionally doesn't depend on setuptools to ease non-setuptools usage. Please ensure a recent version of setuptools (>=64) is installed. ```toml title="pyproject.toml" [build-system] requires = ["setuptools>=64", "setuptools-scm>=8"] build-backend = "setuptools.build_meta" [project] name = "example" # Important: Remove any existing version declaration # version = "0.0.1" dynamic = ["version"] # more missing [tool.setuptools_scm] ``` ### With hatch [Hatch-vcs](https://github.com/ofek/hatch-vcs) integrates with setuptools-scm but provides its own configuration options, please see its [documentation](https://github.com/ofek/hatch-vcs#readme) setuptools-scm-8.2.1/docs/overrides.md000066400000000000000000000016211476647667300200310ustar00rootroot00000000000000# Overrides ## pretend versions setuptools-scm provides a mechanism to override the version number build time. the environment variable `SETUPTOOLS_SCM_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_${NORMALIZED_DIST_NAME}` where the dist name normalization follows adapted PEP 503 semantics. ## config overrides setuptools-scm parses the environment variable `SETUPTOOLS_SCM_OVERRIDES_FOR_${NORMALIZED_DIST_NAME}` as a toml inline map to override the configuration data from `pyproject.toml`. ## subprocess timeouts 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. setuptools-scm-8.2.1/docs/usage.md000066400000000000000000000237551476647667300171470ustar00rootroot00000000000000# Usage ## At build time The preferred way to configure `setuptools-scm` is to author settings in the `tool.setuptools_scm` section of `pyproject.toml`. It's necessary to use a setuptools version released after 2022. ```toml title="pyproject.toml" [build-system] requires = ["setuptools>=64", "setuptools-scm>=8"] build-backend = "setuptools.build_meta" [project] # version = "0.0.1" # Remove any existing version parameter. dynamic = ["version"] [tool.setuptools_scm] # can be empty if no extra settings are needed, presence enables setuptools-scm ``` That will be sufficient to require `setuptools-scm` for projects that support PEP 518 ([pip](https://pypi.org/project/pip) and [pep517](https://pypi.org/project/pep517/)). Tools that still invoke `setup.py` must ensure build requirements are installed ### Version files Version files can be created with the ``version_file`` directive. ```toml title="pyproject.toml" ... [tool.setuptools_scm] version_file = "pkg/_version.py" ``` Where ``pkg`` is the name of your package. Unless the small overhead of introspecting the version at runtime via `importlib.metadata` is a concern or you need a version file in an alternative format such as plain-text (see ``version_file_template``) you most likely do _not_ need to write a separate version file; see the runtime discussion below for more details. ## As cli tool If you need to confirm which version string is being generated or debug the configuration, you can install [setuptools-scm](https://github.com/pypa/setuptools-scm) directly in your working environment and run: ```commandline $ python -m setuptools_scm # example from running local after changes 7.1.1.dev149+g5197d0f.d20230727 ``` and to list all tracked by the scm: ```commandline $ python -m setuptools_scm ls # output trimmed for brevity ./LICENSE ... ./src/setuptools_scm/__init__.py ./src/... ... ``` !!! note "Committed files only" currently only committed files are listed, this might change in the future !!! warning "sdists/archives don't provide file lists" Currently there is no builtin mechanism to safely transfer the file lists to sdists or obtaining them from archives. Coordination for setuptools and hatch is ongoing. To explore other options, try ```commandline $ python -m setuptools_scm --help ``` ## At runtime ### Python Metadata The standard method to retrieve the version number at runtime is via [PEP-0566](https://www.python.org/dev/peps/pep-0566/) metadata using ``importlib.metadata`` from the standard library (added in Python 3.8) or the [`importlib_metadata`](https://pypi.org/project/importlib-metadata/) backport for earlier versions: ```python title="package_name/__init__.py" from importlib.metadata import version, PackageNotFoundError try: __version__ = version("package-name") except PackageNotFoundError: # package is not installed pass ``` ### Via your version file If you have opted to create a Python version file via the standard template, you can import that file, where you will have a ``version`` string and a ``version_tuple`` tuple with elements corresponding to the version tags. ```python title="Using package_name/_version.py" import package_name._version as v print(v.version) print(v.version_tuple) ``` ### Via setuptools_scm (strongly discouraged) While the most simple **looking** way to use `setuptools_scm` at runtime is: ```python from setuptools_scm import get_version version = get_version() ``` it is strongly discouraged to call directly into `setuptools_scm` over the standard Python `importlib.metadata`. In order to use `setuptools_scm` from code that is one directory deeper than the project's root, you can use: ```python from setuptools_scm import get_version version = get_version(root='..', relative_to=__file__) ``` ### Usage from Sphinx ``` {.python file=docs/.entangled/sphinx_conf.py} from importlib.metadata import version as get_version release: str = get_version("package-name") # for example take major/minor version: str = ".".join(release.split('.')[:2]) ``` The underlying reason is that services like *Read the Docs* sometimes change the working directory for good reasons and using the installed metadata prevents using needless volatile data there. ### With Docker/Podman In some situations, Docker may not copy the `.git` into the container when building images. Because of this, builds with version inference may fail. The following snippet exposes the external `.git` directory without copying. This allows the version to be inferred properly form inside the container without copying the entire `.git` folder into the container image. ```dockerfile RUN --mount=source=.git,target=.git,type=bind \ pip install --no-cache-dir -e . ``` However, this build step introduces a dependency to the state of your local `.git` folder the build cache and triggers the long-running pip install process on every build. To optimize build caching, one can use an environment variable to pretend a pseudo version that is used to cache the results of the pip install process: ```dockerfile FROM python COPY pyproject.toml ARG PSEUDO_VERSION=1 # strongly recommended to update based on git describe RUN SETUPTOOLS_SCM_PRETEND_VERSION_FOR_MY_PACKAGE=${PSEUDO_VERSION} pip install -e .[test] RUN --mount=source=.git,target=.git,type=bind pip install -e . ``` Note that running this Dockerfile requires docker with BuildKit enabled [docs](https://github.com/moby/buildkit/blob/v0.8.3/frontend/dockerfile/docs/syntax.md). To avoid BuildKit and mounting of the .git folder altogether, one can also pass the desired version as a build argument. Note that `SETUPTOOLS_SCM_PRETEND_VERSION_FOR_${NORMALIZED_DIST_NAME}` is preferred over `SETUPTOOLS_SCM_PRETEND_VERSION`. ## Default versioning scheme In the standard configuration `setuptools-scm` takes a look at three things: 1. latest tag (with a version number) 2. the distance to this tag (e.g. number of revisions since latest tag) 3. workdir state (e.g. uncommitted changes since latest tag) and uses roughly the following logic to render the version: | distance | state | format | |----------|-----------|----------------------------------------------------------------------| | no | unchanged | `{tag}` | | yes | unchanged | `{next_version}.dev{distance}+{scm letter}{revision hash}` | | no | changed | `{tag}+dYYYYMMDD` | | yes | changed | `{next_version}.dev{distance}+{scm letter}{revision hash}.dYYYYMMDD` | where `{next_version}` is the next version number after the latest tag The next version is calculated by adding `1` to the last numeric component of the tag. For Git projects, the version relies on [git describe](https://git-scm.com/docs/git-describe), so you will see an additional `g` prepended to the `{revision hash}`. !!! note According to [PEP 440](https://peps.python.org/pep-0440/#local-version-identifiers>), if a version includes a local component, the package cannot be published to public package indexes like PyPI or TestPyPI. The disallowed version segments may be seen in auto-publishing workflows or when a configuration mistake is made. However, some package indexes such as devpi or other alternatives allow local versions. Local version identifiers must comply with [PEP 440](https://peps.python.org/pep-0440/#local-version-identifiers>). ## Semantic Versioning (SemVer) Due to the default behavior it's necessary to always include a patch version (the `3` in `1.2.3`), or else the automatic guessing will increment the wrong part of the SemVer (e.g. tag `2.0` results in `2.1.devX` instead of `2.0.1.devX`). So please make sure to tag accordingly. ## Builtin mechanisms for obtaining version numbers 1. the SCM itself (Git/Mercurial) 2. `.hg_archival` files (Mercurial archives) 3. `.git_archival.txt` files (Git archives, see subsection below) 4. `PKG-INFO` ### Git archives Git archives are supported, but a few changes to your repository are required. Ensure the content of the following files: ```{ .text file=".git_archival.txt"} node: $Format:%H$ node-date: $Format:%cI$ describe-name: $Format:%(describe:tags=true,match=*[0-9]*)$ ``` Feel free to alter the `match` field in `describe-name` to match your project's tagging style. !!! note If your git host provider does not properly expand `describe-name`, you may need to include `ref-names: $Format:%D$`. But **beware**, this can often lead to the git archive's checksum changing after a commit is added post-release. See [this issue][git-archive-issue] for more details. ``` {.text file=".gitattributes"} .git_archival.txt export-subst ``` Finally, don't forget to commit the two files: ```commandline $ git add .git_archival.txt .gitattributes && git commit -m "add export config" ``` Note that if you are creating a `_version.py` file, note that it should not be kept in version control. It's strongly recommended to be put into gitignore. [git-archive-issue]: https://github.com/pypa/setuptools-scm/issues/806 ### File finders hook makes most of `MANIFEST.in` unnecessary `setuptools-scm` implements a [file_finders] entry point which returns all files tracked by your SCM. This eliminates the need for a manually constructed `MANIFEST.in` in most cases where this would be required when not using `setuptools-scm`, namely: * To ensure all relevant files are packaged when running the `sdist` command. * When using [include_package_data] to include package data as part of the `build` or `bdist_wheel`. `MANIFEST.in` may still be used: anything defined there overrides the hook. This is mostly useful to exclude files tracked in your SCM from packages, although in principle it can be used to explicitly include non-tracked files too. [file_finders]: https://setuptools.pypa.io/en/latest/userguide/extension.html#adding-support-for-revision-control-systems [include_package_data]: https://setuptools.readthedocs.io/en/latest/setuptools.html#including-data-files setuptools-scm-8.2.1/hatch.toml000066400000000000000000000007111476647667300165400ustar00rootroot00000000000000[envs.test] extras = ["test", "dev"] [envs.test.scripts] all = "pytest {args}" [[env.test.matrix]] python = ["3.8", "3.9", "3.10", "3.11"] [envs.docs] python = "3.11" extras = ["docs"] dependencies = ["scriv"] [envs.docs.scripts] build = "mkdocs build --clean --strict" serve = "mkdocs serve --dev-addr localhost:8000" init = "mkdocs {args}" sync = ["entangled sync"] changelog-create = "scriv create {args}" changelog-collect = "scriv collect {args}"setuptools-scm-8.2.1/mkdocs.yml000066400000000000000000000011741476647667300165630ustar00rootroot00000000000000site_name: setuptools scm nav: - index.md - usage.md - customizing.md - config.md - extending.md - overrides.md - changelog.md theme: name: material watch: - src/setuptools_scm - docs markdown_extensions: - def_list - admonition - pymdownx.tasklist: custom_checkbox: true - pymdownx.superfences plugins: - entangled - search - include-markdown - mkdocstrings: default_handler: python handlers: python: paths: [ src ] options: separate_signature: true show_signature_annotations: true allow_inspection: true show_root_heading: true setuptools-scm-8.2.1/mypy.ini000066400000000000000000000002021476647667300162460ustar00rootroot00000000000000[mypy] python_version = 3.8 warn_return_any = True warn_unused_configs = True mypy_path = $MYPY_CONFIG_FILE_DIR/src strict = true setuptools-scm-8.2.1/nextgen/000077500000000000000000000000001476647667300162255ustar00rootroot00000000000000setuptools-scm-8.2.1/nextgen/vcs-versioning/000077500000000000000000000000001476647667300212015ustar00rootroot00000000000000setuptools-scm-8.2.1/nextgen/vcs-versioning/LICENSE.txt000066400000000000000000000020731476647667300230260ustar00rootroot00000000000000MIT License Copyright (c) 2023-present Ronny Pfannschmidt Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. setuptools-scm-8.2.1/nextgen/vcs-versioning/README.md000066400000000000000000000010101476647667300224500ustar00rootroot00000000000000# vcs-versioning [![PyPI - Version](https://img.shields.io/pypi/v/vcs-versioning.svg)](https://pypi.org/project/vcs-versioning) [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/vcs-versioning.svg)](https://pypi.org/project/vcs-versioning) ----- **Table of Contents** - [Installation](#installation) - [License](#license) ## Installation ```console pip install vcs-versioning ``` ## License `vcs-versioning` is distributed under the terms of the [MIT](https://spdx.org/licenses/MIT.html) license. setuptools-scm-8.2.1/nextgen/vcs-versioning/pyproject.toml000066400000000000000000000031041476647667300241130ustar00rootroot00000000000000[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:", ] setuptools-scm-8.2.1/nextgen/vcs-versioning/tests/000077500000000000000000000000001476647667300223435ustar00rootroot00000000000000setuptools-scm-8.2.1/nextgen/vcs-versioning/tests/__init__.py000066400000000000000000000000431476647667300244510ustar00rootroot00000000000000from __future__ import annotations setuptools-scm-8.2.1/nextgen/vcs-versioning/vcs_versioning/000077500000000000000000000000001476647667300242375ustar00rootroot00000000000000setuptools-scm-8.2.1/nextgen/vcs-versioning/vcs_versioning/__about__.py000066400000000000000000000000721476647667300265160ustar00rootroot00000000000000from __future__ import annotations __version__ = "0.0.1" setuptools-scm-8.2.1/nextgen/vcs-versioning/vcs_versioning/__init__.py000066400000000000000000000000431476647667300263450ustar00rootroot00000000000000from __future__ import annotations setuptools-scm-8.2.1/pyproject.toml000066400000000000000000000120611476647667300174710ustar00rootroot00000000000000 [build-system] build-backend = "_own_version_helper:build_meta" requires = [ "setuptools>=61", 'tomli<=2.0.2; python_version < "3.11"', ] backend-path = [ ".", "src", ] [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", ] 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"', ] [project.optional-dependencies] docs = [ "entangled-cli~=2.0", "mkdocs", "mkdocs-entangled-plugin", "mkdocs-include-markdown-plugin", "mkdocs-material", "mkdocstrings[python]", "pygments", ] rich = [ "rich", ] test = [ "build", "pytest", "rich", 'typing-extensions; python_version < "3.11"', "wheel", ] toml = [ ] [project.urls] documentation = "https://setuptools-scm.readthedocs.io/" repository = "https://github.com/pypa/setuptools-scm/" [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" [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" [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" [tool.setuptools.packages.find] where = ["src"] namespaces = false [tool.setuptools.dynamic] version = { attr = "_own_version_helper.version"} [tool.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.repo-review] ignore = ["PP305", "GH103", "GH212", "MY100", "PC111", "PC160", "PC170", "PC180", "PC901"] [tool.pytest.ini_options] minversion = "7" testpaths = ["testing"] 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", "-p", "no:unraisableexception"] markers = [ "issue(id): reference to github issue", "skip_commit: allows to skip committing in the helpers", ] [tool.scriv] format = "md" setuptools-scm-8.2.1/src/000077500000000000000000000000001476647667300153445ustar00rootroot00000000000000setuptools-scm-8.2.1/src/setuptools_scm/000077500000000000000000000000001476647667300204275ustar00rootroot00000000000000setuptools-scm-8.2.1/src/setuptools_scm/.git_archival.txt000066400000000000000000000002071476647667300237010ustar00rootroot00000000000000node: 94f36a0035b4a1dfb31b5b34086ad8f0860f86aa node-date: 2025-03-19T09:18:03+01:00 describe-name: %(describe:tags=true,match=*[0-9]*) setuptools-scm-8.2.1/src/setuptools_scm/__init__.py000066400000000000000000000014211476647667300225360ustar00rootroot00000000000000""" :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", ] setuptools-scm-8.2.1/src/setuptools_scm/__main__.py000066400000000000000000000001641476647667300225220ustar00rootroot00000000000000from __future__ import annotations from ._cli import main if __name__ == "__main__": raise SystemExit(main()) setuptools-scm-8.2.1/src/setuptools_scm/_cli.py000066400000000000000000000127351476647667300217170ustar00rootroot00000000000000from __future__ import annotations import argparse import json import os import sys 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.discover import walk_potential_roots def main(args: list[str] | 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), ) 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( 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) 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.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 setuptools-scm-8.2.1/src/setuptools_scm/_config.py000066400000000000000000000115501476647667300224070ustar00rootroot00000000000000"""configuration""" from __future__ import annotations import dataclasses import os import re import warnings from pathlib import Path from typing import Any from typing import Pattern from typing import Protocol from . import _log from . import _types as _t 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 ._version_cls import Version as _Version from ._version_cls import _validate_version_cls from ._version_cls import _VersionT log = _log.log.getChild("config") DEFAULT_TAG_REGEX = re.compile( r"^(?:[\w-]+-)?(?P[vV]?\d+(?:\.\d+){0,2}[^\+]*)(?:\+.*)?$" ) """default tag regex that tries to match PEP440 style versions with prefix consisting of dashed words""" DEFAULT_VERSION_SCHEME = "guess-next-dev" DEFAULT_LOCAL_SCHEME = "node-and-date" def _check_tag_regex(value: str | Pattern[str] | None) -> Pattern[str]: if not value: regex = DEFAULT_TAG_REGEX else: regex = re.compile(value) group_names = regex.groupindex.keys() if regex.groups == 0 or (regex.groups > 1 and "version" not in group_names): warnings.warn( "Expected tag_regex to contain a single match group or a group named" " 'version' to identify the version part of any tag." ) return regex class ParseFunction(Protocol): def __call__( self, root: _t.PathT, *, config: Configuration ) -> _t.SCMVERSION | None: ... def _check_absolute_root(root: _t.PathT, relative_to: _t.PathT | None) -> str: log.debug("check absolute root=%s relative_to=%s", root, relative_to) if relative_to: if ( os.path.isabs(root) and os.path.isabs(relative_to) and not os.path.commonpath([root, relative_to]) == root ): warnings.warn( f"absolute root path '{root}' overrides relative_to '{relative_to}'" ) 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" ) log.debug("dir %s", relative_to) root = os.path.join(relative_to, root) else: log.debug("file %s", relative_to) root = os.path.join(os.path.dirname(relative_to), root) return os.path.abspath(root) @dataclasses.dataclass class Configuration: """Global configuration model""" relative_to: _t.PathT | None = None root: _t.PathT = "." version_scheme: _t.VERSION_SCHEME = DEFAULT_VERSION_SCHEME local_scheme: _t.VERSION_SCHEME = DEFAULT_LOCAL_SCHEME tag_regex: Pattern[str] = DEFAULT_TAG_REGEX parentdir_prefix_version: str | None = None fallback_version: str | None = None fallback_root: _t.PathT = "." write_to: _t.PathT | None = None write_to_template: str | None = None version_file: _t.PathT | None = None version_file_template: str | None = None parse: ParseFunction | None = None git_describe_command: _t.CMD_TYPE | None = None dist_name: str | None = None version_cls: type[_VersionT] = _Version search_parent_directories: bool = False parent: _t.PathT | None = None @property def absolute_root(self) -> str: return _check_absolute_root(self.root, self.relative_to) @classmethod def from_file( cls, name: str | os.PathLike[str] = "pyproject.toml", dist_name: str | None = None, _require_section: bool = True, **kwargs: Any, ) -> Configuration: """ Read Configuration from pyproject.toml (or similar). Raises exceptions when file is not found or toml is not installed or the file has invalid format or does not contain the [tool.setuptools_scm] section. """ pyproject_data = _read_pyproject(Path(name), require_section=_require_section) 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) return cls.from_data(relative_to=relative_to, data=args) @classmethod def from_data( cls, relative_to: str | os.PathLike[str], data: dict[str, Any] ) -> Configuration: """ given configuration data create a config instance after validating tag regex/version class """ tag_regex = _check_tag_regex(data.pop("tag_regex", None)) version_cls = _validate_version_cls( data.pop("version_cls", None), data.pop("normalize", True) ) return cls( relative_to=relative_to, version_cls=version_cls, tag_regex=tag_regex, **data, ) setuptools-scm-8.2.1/src/setuptools_scm/_entrypoints.py000066400000000000000000000073651476647667300235510ustar00rootroot00000000000000from __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 typing import overload from . import _log from . import version if TYPE_CHECKING: from . import _types as _t from ._config import Configuration from ._config import ParseFunction from importlib.metadata import EntryPoint as EntryPoint if sys.version_info[:2] < (3, 10): from importlib.metadata import entry_points as legacy_entry_points class EntryPoints: _groupdata: list[EntryPoint] def __init__(self, groupdata: list[EntryPoint]) -> None: self._groupdata = groupdata def select(self, name: str) -> EntryPoints: return EntryPoints([x for x in self._groupdata if x.name == name]) def __iter__(self) -> Iterator[EntryPoint]: return iter(self._groupdata) def entry_points(group: str) -> EntryPoints: return EntryPoints(legacy_entry_points()[group]) else: from importlib.metadata import EntryPoints from importlib.metadata import entry_points log = _log.log.getChild("entrypoints") def version_from_entrypoint( config: Configuration, *, entrypoint: str, root: _t.PathT ) -> version.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) log.debug("%s found %r", ep, maybe_version) if maybe_version is not None: return maybe_version return None def iter_entry_points(group: str, name: str | None = None) -> Iterator[EntryPoint]: eps: EntryPoints = entry_points(group=group) res = eps if name is None else eps.select(name=name) return iter(res) def _get_ep(group: str, name: str) -> Any | None: for ep in iter_entry_points(group, name): log.debug("ep found: %s", ep.name) return ep.load() return None def _get_from_object_reference_str(path: str, group: str) -> Any | None: # todo: remove for importlib native spelling ep = EntryPoint(path, path, group) try: return ep.load() except (AttributeError, ModuleNotFoundError): return None def _iter_version_schemes( entrypoint: str, scheme_value: _t.VERSION_SCHEMES, _memo: set[object] | None = None, ) -> Iterator[Callable[[version.ScmVersion], str]]: if _memo is None: _memo = set() if isinstance(scheme_value, str): scheme_value = cast( "_t.VERSION_SCHEMES", _get_ep(entrypoint, scheme_value) or _get_from_object_reference_str(scheme_value, entrypoint), ) if isinstance(scheme_value, (list, tuple)): for variant in scheme_value: if variant not in _memo: _memo.add(variant) yield from _iter_version_schemes(entrypoint, variant, _memo=_memo) elif callable(scheme_value): yield scheme_value @overload def _call_version_scheme( version: version.ScmVersion, entrypoint: str, given_value: _t.VERSION_SCHEMES, default: str, ) -> str: ... @overload def _call_version_scheme( version: version.ScmVersion, entrypoint: str, given_value: _t.VERSION_SCHEMES, default: None, ) -> str | None: ... def _call_version_scheme( version: version.ScmVersion, entrypoint: str, given_value: _t.VERSION_SCHEMES, default: str | None, ) -> str | None: for scheme in _iter_version_schemes(entrypoint, given_value): result = scheme(version) if result is not None: return result return default setuptools-scm-8.2.1/src/setuptools_scm/_file_finders/000077500000000000000000000000001476647667300232175ustar00rootroot00000000000000setuptools-scm-8.2.1/src/setuptools_scm/_file_finders/__init__.py000066400000000000000000000072471476647667300253420ustar00rootroot00000000000000from __future__ import annotations import itertools import os from typing import TYPE_CHECKING from typing import Callable from .. import _log from .. import _types as _t from .._entrypoints import iter_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") def scm_find_files( path: _t.PathT, scm_files: set[str], scm_dirs: set[str], force_all_files: bool = False, ) -> list[str]: """ setuptools compatible file finder that follows symlinks - path: the root directory from which to search - scm_files: set of scm controlled files and symlinks (including symlinks to directories) - scm_dirs: set of scm controlled directories (including directories containing no scm controlled files) - force_all_files: ignore ``scm_files`` and ``scm_dirs`` and list everything. 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() res: list[str] = [] for dirpath, dirnames, filenames in os.walk(realpath, followlinks=True): # dirpath with symlinks resolved realdirpath = norm_real(dirpath) def _link_not_in_scm(n: str, realdirpath: str = realdirpath) -> bool: fn = os.path.join(realdirpath, os.path.normcase(n)) return os.path.islink(fn) and fn not in scm_files if not force_all_files and realdirpath not in scm_dirs: # directory not in scm, don't walk it's content dirnames[:] = [] continue if os.path.islink(dirpath) and not os.path.relpath( realdirpath, realpath ).startswith(os.pardir): # a symlink to a directory not outside path: # we keep it in the result and don't walk its content res.append(os.path.join(path, os.path.relpath(dirpath, path))) dirnames[:] = [] continue if realdirpath in seen: # symlink loop protection dirnames[:] = [] continue dirnames[:] = [ dn for dn in dirnames if force_all_files or not _link_not_in_scm(dn) ] for filename in filenames: if not force_all_files and _link_not_in_scm(filename): continue # dirpath + filename with symlinks preserved fullfilename = os.path.join(dirpath, filename) is_tracked = norm_real(fullfilename) in scm_files if force_all_files or is_tracked: res.append(os.path.join(path, os.path.relpath(fullfilename, realpath))) seen.add(realdirpath) return res def is_toplevel_acceptable(toplevel: str | None) -> TypeGuard[str]: """ """ if toplevel is None: return False ignored: list[str] = os.environ.get("SETUPTOOLS_SCM_IGNORE_VCS_ROOTS", "").split( os.pathsep ) ignored = [os.path.normcase(p) for p in ignored] log.debug("toplevel: %r\n ignored %s", toplevel, ignored) return toplevel not in ignored def find_files(path: _t.PathT = "") -> list[str]: for ep in itertools.chain( iter_entry_points("setuptools_scm.files_command"), iter_entry_points("setuptools_scm.files_command_fallback"), ): command: Callable[[_t.PathT], list[str]] = ep.load() res: list[str] = command(path) if res: return res return [] setuptools-scm-8.2.1/src/setuptools_scm/_file_finders/git.py000066400000000000000000000101661476647667300243600ustar00rootroot00000000000000from __future__ import annotations import logging import os import subprocess import tarfile from typing import IO from .. import _types as _t 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 log = logging.getLogger(__name__) def _git_toplevel(path: str) -> str | None: try: cwd = os.path.abspath(path or ".") res = _run(["git", "rev-parse", "HEAD"], cwd=cwd) if res.returncode: # BAIL if there is no commit log.error("listing git files failed - pretending there aren't any") return None res = _run( ["git", "rev-parse", "--show-prefix"], cwd=cwd, ) if res.returncode: return None out = res.stdout[:-1] # remove the trailing pathsep if not out: out = cwd else: # Here, ``out`` is a relative path to root of git. # ``cwd`` is absolute path to current working directory. # the below method removes the length of ``out`` from # ``cwd``, which gives the git toplevel assert cwd.replace("\\", "/").endswith(out), f"cwd={cwd!r}\nout={out!r}" # In windows cwd contains ``\`` which should be replaced by ``/`` # for this assertion to work. Length of string isn't changed by replace # ``\\`` is just and escape for `\` out = cwd[: -len(out)] log.debug("find files toplevel %s", out) return norm_real(out) except subprocess.CalledProcessError: # git returned error, we are not in a git repo return None except OSError: # git command not found, probably return None def _git_interpret_archive(fd: IO[bytes], toplevel: str) -> tuple[set[str], set[str]]: with tarfile.open(fileobj=fd, mode="r|*") as tf: git_files = set() git_dirs = {toplevel} for member in tf.getmembers(): name = os.path.normcase(member.name).replace("/", os.path.sep) if member.type == tarfile.DIRTYPE: git_dirs.add(name) else: git_files.add(name) return git_files, git_dirs def _git_ls_files_and_dirs(toplevel: str) -> tuple[set[str], set[str]]: # use git archive instead of git ls-file to honor # export-ignore git attribute cmd = ["git", "archive", "--prefix", toplevel + os.path.sep, "HEAD"] log.info("running %s", " ".join(str(x) for x in cmd)) proc = subprocess.Popen( cmd, stdout=subprocess.PIPE, cwd=toplevel, stderr=subprocess.DEVNULL ) assert proc.stdout is not None try: try: return _git_interpret_archive(proc.stdout, toplevel) finally: # ensure we avoid resource warnings by cleaning up the process proc.stdout.close() proc.terminate() except Exception: if proc.wait() != 0: log.error("listing git files failed - pretending there aren't any") return set(), set() def git_find_files(path: _t.PathT = "") -> list[str]: toplevel = _git_toplevel(os.fspath(path)) if not is_toplevel_acceptable(toplevel): return [] fullpath = norm_real(path) if not fullpath.startswith(toplevel): log.warning("toplevel mismatch computed %s vs resolved %s ", toplevel, fullpath) git_files, git_dirs = _git_ls_files_and_dirs(toplevel) return scm_find_files(path, git_files, git_dirs) def git_archive_find_files(path: _t.PathT = "") -> list[str]: # 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") if not os.path.exists(archival): return [] data = data_from_mime(archival) if "$Format" in data.get("node", ""): # Substitutions have not been performed, so not a reliable archive return [] log.warning("git archive detected - fallback to listing all files") return scm_find_files(path, set(), set(), force_all_files=True) setuptools-scm-8.2.1/src/setuptools_scm/_file_finders/hg.py000066400000000000000000000043061476647667300241720ustar00rootroot00000000000000from __future__ import annotations import logging import os import subprocess from .. import _types as _t from .._file_finders import is_toplevel_acceptable from .._file_finders import scm_find_files from .._run_cmd import run as _run from ..integration import data_from_mime from .pathtools import norm_real log = logging.getLogger(__name__) def _hg_toplevel(path: str) -> str | None: try: return _run( ["hg", "root"], cwd=(path or "."), check=True, ).parse_success(norm_real) except subprocess.CalledProcessError: # hg returned error, we are not in a mercurial repo return None except OSError: # hg command not found, probably return None def _hg_ls_files_and_dirs(toplevel: str) -> tuple[set[str], set[str]]: hg_files: set[str] = set() hg_dirs = {toplevel} res = _run(["hg", "files"], cwd=toplevel) if res.returncode: return set(), set() for name in res.stdout.splitlines(): name = os.path.normcase(name).replace("/", os.path.sep) fullname = os.path.join(toplevel, name) hg_files.add(fullname) dirname = os.path.dirname(fullname) while len(dirname) > len(toplevel) and dirname not in hg_dirs: hg_dirs.add(dirname) dirname = os.path.dirname(dirname) return hg_files, hg_dirs def hg_find_files(path: str = "") -> list[str]: toplevel = _hg_toplevel(path) if not is_toplevel_acceptable(toplevel): return [] assert toplevel is not None hg_files, hg_dirs = _hg_ls_files_and_dirs(toplevel) return scm_find_files(path, hg_files, hg_dirs) def hg_archive_find_files(path: _t.PathT = "") -> list[str]: # 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") if not os.path.exists(archival): return [] data = data_from_mime(archival) if "node" not in data: # Ensure file is valid return [] log.warning("hg archive detected - fallback to listing all files") return scm_find_files(path, set(), set(), force_all_files=True) setuptools-scm-8.2.1/src/setuptools_scm/_file_finders/pathtools.py000066400000000000000000000002631476647667300256070ustar00rootroot00000000000000from __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)) setuptools-scm-8.2.1/src/setuptools_scm/_get_version_impl.py000066400000000000000000000140301476647667300245030ustar00rootroot00000000000000from __future__ import annotations import logging import re import warnings from pathlib import Path from typing import Any from typing import NoReturn from typing import Pattern from . import _config from . import _entrypoints from . import _run_cmd from . import _types as _t from ._config import Configuration from ._overrides import _read_pretended_version_for from ._version_cls import _validate_version_cls from .version import ScmVersion from .version import format_version as _format_version EMPTY_TAG_REGEX_DEPRECATION = DeprecationWarning( "empty regex for tag regex is invalid, using default" ) _log = logging.getLogger(__name__) def parse_scm_version(config: Configuration) -> ScmVersion | None: try: if config.parse is not None: parse_result = config.parse(config.absolute_root, config=config) if parse_result is not None and not isinstance(parse_result, ScmVersion): raise TypeError( f"version parse result was {str!r}\n" "please return a parsed version (ScmVersion)" ) return parse_result else: return _entrypoints.version_from_entrypoint( config, entrypoint="setuptools_scm.parse_scm", root=config.absolute_root, ) except _run_cmd.CommandNotFoundError as e: _log.exception("command %s not found while parsing the scm, using fallbacks", e) return None def parse_fallback_version(config: Configuration) -> ScmVersion | None: return _entrypoints.version_from_entrypoint( config, entrypoint="setuptools_scm.parse_scm_fallback", root=config.fallback_root, ) def parse_version(config: Configuration) -> ScmVersion | None: return ( _read_pretended_version_for(config) or parse_scm_version(config) or parse_fallback_version(config) ) 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 dump_version( root=config.root, version=version, scm_version=scm_version, write_to=config.write_to, template=config.write_to_template, ) if config.version_file: from ._integration.dump_version import write_version_to_path version_file = Path(config.version_file) assert not version_file.is_absolute(), f"{version_file=}" # todo: use a better name than fallback root assert config.relative_to is not None target = Path(config.relative_to).parent.joinpath(version_file) write_version_to_path( target, template=config.version_file_template, version=version, scm_version=scm_version, ) def _get_version( config: Configuration, force_write_version_files: bool | None = None ) -> str | None: parsed_version = parse_version(config) if parsed_version is None: return None version_string = _format_version(parsed_version) if force_write_version_files is None: force_write_version_files = True warnings.warn( "force_write_version_files ought to be set," " presuming the legacy True value", DeprecationWarning, ) if force_write_version_files: write_version_files(config, version=version_string, scm_version=parsed_version) return version_string def _version_missing(config: Configuration) -> NoReturn: raise LookupError( f"setuptools-scm was unable to detect version for {config.absolute_root}.\n\n" "Make sure you're either building from a fully intact git repository " "or PyPI tarballs. Most other sources (such as GitHub's tarballs, a " "git checkout without the .git folder) don't contain the necessary " "metadata and will not work.\n\n" "For example, if you're using pip, instead of " "https://github.com/user/proj/archive/master.zip " "use git+https://github.com/user/proj.git#egg=proj\n\n" "Alternatively, set the version with the environment variable " "SETUPTOOLS_SCM_PRETEND_VERSION_FOR_${NORMALIZED_DIST_NAME} as described " "in https://setuptools-scm.readthedocs.io/en/latest/config." ) def get_version( root: _t.PathT = ".", version_scheme: _t.VERSION_SCHEME = _config.DEFAULT_VERSION_SCHEME, local_scheme: _t.VERSION_SCHEME = _config.DEFAULT_LOCAL_SCHEME, write_to: _t.PathT | None = None, write_to_template: str | None = None, version_file: _t.PathT | None = None, version_file_template: str | None = None, relative_to: _t.PathT | None = None, tag_regex: str | Pattern[str] = _config.DEFAULT_TAG_REGEX, parentdir_prefix_version: str | None = None, fallback_version: str | None = None, fallback_root: _t.PathT = ".", parse: Any | None = None, git_describe_command: _t.CMD_TYPE | None = None, dist_name: str | None = None, version_cls: Any | None = None, normalize: bool = True, search_parent_directories: bool = False, ) -> str: """ If supplied, relative_to should be a file from which root may be resolved. Typically called by a script or module that is not in the root of the repository to direct setuptools-scm to the root of the repository by supplying ``__file__``. """ version_cls = _validate_version_cls(version_cls, normalize) del normalize tag_regex = parse_tag_regex(tag_regex) config = Configuration(**locals()) maybe_version = _get_version(config, force_write_version_files=True) if maybe_version is None: _version_missing(config) return maybe_version def parse_tag_regex(tag_regex: str | Pattern[str]) -> Pattern[str]: if isinstance(tag_regex, str): if tag_regex == "": warnings.warn(EMPTY_TAG_REGEX_DEPRECATION) return _config.DEFAULT_TAG_REGEX else: return re.compile(tag_regex) else: return tag_regex setuptools-scm-8.2.1/src/setuptools_scm/_integration/000077500000000000000000000000001476647667300231115ustar00rootroot00000000000000setuptools-scm-8.2.1/src/setuptools_scm/_integration/__init__.py000066400000000000000000000000001476647667300252100ustar00rootroot00000000000000setuptools-scm-8.2.1/src/setuptools_scm/_integration/dump_version.py000066400000000000000000000051741476647667300262040ustar00rootroot00000000000000from __future__ import annotations import warnings from pathlib import Path from .. import _types as _t from .._log import log as parent_log from .._version_cls import _version_as_tuple from ..version import ScmVersion log = parent_log.getChild("dump_version") TEMPLATES = { ".py": """\ # file generated by setuptools-scm # don't change, don't track in version control __all__ = ["__version__", "__version_tuple__", "version", "version_tuple"] TYPE_CHECKING = False if TYPE_CHECKING: from typing import Tuple from typing import Union VERSION_TUPLE = Tuple[Union[int, str], ...] else: VERSION_TUPLE = object version: str __version__: str __version_tuple__: VERSION_TUPLE version_tuple: VERSION_TUPLE __version__ = version = {version!r} __version_tuple__ = version_tuple = {version_tuple!r} """, ".txt": "{version}", } 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 ) def _validate_template(target: Path, template: str | None) -> str: if template == "": warnings.warn(f"{template=} looks like a error, using default instead") template = None if template is None: template = TEMPLATES.get(target.suffix) if template is None: raise ValueError( f"bad file format: {target.suffix!r} (of {target})\n" "only *.txt and *.py have a default template" ) else: return template def write_version_to_path( target: Path, template: str | None, version: str, scm_version: ScmVersion | None ) -> None: final_template = _validate_template(target, template) log.debug("dump %s into %s", version, target) version_tuple = _version_as_tuple(version) if scm_version is not None: content = final_template.format( version=version, version_tuple=version_tuple, scm_version=scm_version, ) else: content = final_template.format(version=version, version_tuple=version_tuple) target.write_text(content, encoding="utf-8") setuptools-scm-8.2.1/src/setuptools_scm/_integration/pyproject_reading.py000066400000000000000000000050541476647667300271770ustar00rootroot00000000000000from __future__ import annotations import warnings from pathlib import Path from typing import NamedTuple from .. import _log from .setuptools import read_dist_name_from_setup_cfg from .toml import TOML_RESULT from .toml import read_toml_content log = _log.log.getChild("pyproject_reading") _ROOT = "root" class PyProjectData(NamedTuple): path: Path tool_name: str project: TOML_RESULT section: TOML_RESULT @property def project_name(self) -> str | None: return self.project.get("name") def read_pyproject( path: Path = Path("pyproject.toml"), tool_name: str = "setuptools_scm", require_section: bool = True, ) -> PyProjectData: defn = read_toml_content(path, None if require_section else {}) try: section = defn.get("tool", {})[tool_name] except LookupError as e: error = f"{path} does not contain a tool.{tool_name} section" if require_section: raise LookupError(error) from e else: log.warning("toml section missing %r", error, exc_info=True) section = {} project = defn.get("project", {}) return PyProjectData(path, tool_name, project, section) def get_args_for_pyproject( pyproject: PyProjectData, dist_name: str | None, kwargs: TOML_RESULT, ) -> TOML_RESULT: """drops problematic details and figures the distribution name""" section = pyproject.section.copy() kwargs = kwargs.copy() if "relative_to" in section: relative = section.pop("relative_to") 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" ) if "dist_name" in section: if dist_name is None: dist_name = section.pop("dist_name") else: assert dist_name == section["dist_name"] section.pop("dist_name") if dist_name is None: # minimal pep 621 support for figuring the pretend keys dist_name = pyproject.project_name if dist_name is None: dist_name = read_dist_name_from_setup_cfg() if _ROOT in kwargs: if kwargs[_ROOT] is None: kwargs.pop(_ROOT, None) elif _ROOT in section: if section[_ROOT] != kwargs[_ROOT]: warnings.warn( f"root {section[_ROOT]} is overridden" f" by the cli arg {kwargs[_ROOT]}" ) section.pop(_ROOT, None) return {"dist_name": dist_name, **section, **kwargs} setuptools-scm-8.2.1/src/setuptools_scm/_integration/setuptools.py000066400000000000000000000066141476647667300257130ustar00rootroot00000000000000from __future__ import annotations import logging import os import warnings from typing import Any from typing import Callable import setuptools from .. import _config log = logging.getLogger(__name__) def read_dist_name_from_setup_cfg( input: str | os.PathLike[str] = "setup.cfg", ) -> str | None: # minimal effort to read dist_name off setup.cfg metadata import configparser parser = configparser.ConfigParser() parser.read([input], encoding="utf-8") dist_name = parser.get("metadata", "name", fallback=None) return dist_name def _warn_on_old_setuptools(_version: str = setuptools.__version__) -> None: if int(_version.split(".")[0]) < 61: warnings.warn( RuntimeWarning( f""" ERROR: setuptools=={_version} is used in combination with setuptools-scm>=8.x Your build configuration is incomplete and previously worked by accident! setuptools-scm requires setuptools>=61 Suggested workaround if applicable: - migrating from the deprecated setup_requires mechanism to pep517/518 and using a pyproject.toml to declare build dependencies which are reliably pre-installed before running the build tools """ ) ) def _assign_version( dist: setuptools.Distribution, config: _config.Configuration ) -> None: from .._get_version_impl import _get_version from .._get_version_impl import _version_missing # todo: build time plugin maybe_version = _get_version(config, force_write_version_files=True) if maybe_version is None: _version_missing(config) else: assert dist.metadata.version is None dist.metadata.version = maybe_version _warn_on_old_setuptools() def _log_hookstart(hook: str, dist: setuptools.Distribution) -> None: log.debug("%s %r", hook, vars(dist.metadata)) def version_keyword( dist: setuptools.Distribution, keyword: str, value: bool | dict[str, Any] | Callable[[], dict[str, Any]], ) -> None: overrides: dict[str, Any] if value is True: overrides = {} elif callable(value): overrides = value() else: assert isinstance(value, dict), "version_keyword expects a dict or True" overrides = value assert "dist_name" not in overrides, ( "dist_name may not be specified in the setup keyword " ) dist_name: str | None = dist.metadata.name _log_hookstart("version_keyword", dist) if dist.metadata.version is not None: warnings.warn(f"version of {dist_name} already set") return if dist_name is None: dist_name = read_dist_name_from_setup_cfg() config = _config.Configuration.from_file( dist_name=dist_name, _require_section=False, **overrides, ) _assign_version(dist, config) def infer_version(dist: setuptools.Distribution) -> None: _log_hookstart("infer_version", dist) log.debug("dist %s %s", id(dist), id(dist.metadata)) if dist.metadata.version is not None: return # metadata already added by hook dist_name = dist.metadata.name if dist_name is None: dist_name = read_dist_name_from_setup_cfg() if not os.path.isfile("pyproject.toml"): return if dist_name == "setuptools-scm": return try: config = _config.Configuration.from_file(dist_name=dist_name) except LookupError as e: log.info(e, exc_info=True) else: _assign_version(dist, config) setuptools-scm-8.2.1/src/setuptools_scm/_integration/toml.py000066400000000000000000000026731476647667300244460ustar00rootroot00000000000000from __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] 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: return load_toml(data) 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 {} elif data[0] == "{": data = "cheat=" + data loaded: _CheatTomlData = cast(_CheatTomlData, load_toml(data)) return loaded["cheat"] return load_toml(data) setuptools-scm-8.2.1/src/setuptools_scm/_log.py000066400000000000000000000041001476647667300217140ustar00rootroot00000000000000""" 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: handler = AlwaysStdErrHandler() handler.setFormatter(logging.Formatter("%(levelname)s %(name)s %(message)s")) return handler _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) setuptools-scm-8.2.1/src/setuptools_scm/_modify_version.py000066400000000000000000000033121476647667300241730ustar00rootroot00000000000000from __future__ import annotations import re from . import _types as _t def strip_local(version_string: str) -> str: public = version_string.partition("+")[0] return public def _add_post(version: str) -> str: if "post" in version: raise ValueError( f"{version} already is a post release, refusing to guess the update" ) return f"{version}.post1" def _bump_dev(version: str) -> str | None: if ".dev" not in version: return None prefix, tail = version.rsplit(".dev", 1) if tail != "0": raise ValueError( "choosing custom numbers for the `.devX` distance " "is not supported.\n " f"The {version} can't be bumped\n" "Please drop the tag or create a new supported one ending in .dev0" ) return prefix def _bump_regex(version: str) -> str: match = re.match(r"(.*?)(\d+)$", version) if match is None: raise ValueError( f"{version} does not end with a number to bump, " "please correct or use a custom version scheme" ) else: prefix, tail = match.groups() return f"{prefix}{int(tail) + 1}" def _format_local_with_time(version: _t.SCMVERSION, time_format: str) -> str: if version.exact or version.node is None: return version.format_choice( "", "+d{time:{time_format}}", time_format=time_format ) else: return version.format_choice( "+{node}", "+{node}.d{time:{time_format}}", time_format=time_format ) def _dont_guess_next_version(tag_version: _t.SCMVERSION) -> str: version = strip_local(str(tag_version.tag)) return _bump_dev(version) or _add_post(version) setuptools-scm-8.2.1/src/setuptools_scm/_overrides.py000066400000000000000000000031671476647667300231510ustar00rootroot00000000000000from __future__ import annotations import os import re from typing import Any 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}" def read_named_env( *, tool: str = "SETUPTOOLS_SCM", name: str, dist_name: str | None ) -> str | None: """ """ if dist_name is not None: # Normalize the dist name as per PEP 503. normalized_dist_name = re.sub(r"[-_.]+", "-", dist_name) env_var_dist_name = normalized_dist_name.replace("-", "_").upper() val = os.environ.get(f"{tool}_{name}_FOR_{env_var_dist_name}") if val is not None: return val return os.environ.get(f"{tool}_{name}") 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: # we use meta here since the pretended version # must adhere to the pep to begin with 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) setuptools-scm-8.2.1/src/setuptools_scm/_run_cmd.py000066400000000000000000000137571476647667300226040ustar00rootroot00000000000000from __future__ import annotations import os import shlex import subprocess import textwrap import warnings 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) BROKEN_TIMEOUT: Final[int] = _get_timeout(os.environ) log = _log.log.getChild("run_cmd") PARSE_RESULT = TypeVar("PARSE_RESULT") T = TypeVar("T") class CompletedProcess(BaseCompletedProcess): @classmethod def from_raw( cls, input: BaseCompletedProcess, strip: bool = True ) -> CompletedProcess: return cls( args=input.args, returncode=input.returncode, stdout=input.stdout.strip() if strip and input.stdout else input.stdout, stderr=input.stderr.strip() if strip and input.stderr else input.stderr, ) @overload def parse_success( self, parse: Callable[[str], PARSE_RESULT], default: None = None, error_msg: str | None = None, ) -> PARSE_RESULT | None: ... @overload def parse_success( self, parse: Callable[[str], PARSE_RESULT], default: T, error_msg: str | None = None, ) -> PARSE_RESULT | T: ... def parse_success( self, parse: Callable[[str], PARSE_RESULT], default: T | None = None, error_msg: str | None = None, ) -> PARSE_RESULT | T | None: if self.returncode: if error_msg: log.warning("%s %s", error_msg, self) return default else: return parse(self.stdout) def no_git_env(env: Mapping[str, str]) -> dict[str, str]: # adapted from pre-commit # Too many bugs dealing with environment variables and GIT: # https://github.com/pre-commit/pre-commit/issues/300 # In git 2.6.3 (maybe others), git exports GIT_WORK_TREE while running # pre-commit hooks # In git 1.9.1 (maybe others), git exports GIT_DIR and GIT_INDEX_FILE # while running pre-commit hooks in submodules. # GIT_DIR: Causes git clone to clone wrong thing # GIT_INDEX_FILE: Causes 'error invalid object ...' during commit for k, v in env.items(): if k.startswith("GIT_"): log.debug("%s: %s", k, v) return { k: v for k, v in env.items() if not k.startswith("GIT_") or k in ("GIT_CEILING_DIRECTORIES", "GIT_EXEC_PATH", "GIT_SSH", "GIT_SSH_COMMAND") } def avoid_pip_isolation(env: Mapping[str, str]) -> dict[str, str]: """ pip build isolation can break Mercurial (see https://github.com/pypa/pip/issues/10635) pip uses PYTHONNOUSERSITE and a path in PYTHONPATH containing "pip-build-env-". """ new_env = {k: v for k, v in env.items() if k != "PYTHONNOUSERSITE"} if "PYTHONPATH" not in new_env: return new_env new_env["PYTHONPATH"] = os.pathsep.join( [ path for path in new_env["PYTHONPATH"].split(os.pathsep) if "-build-env-" not in path ] ) return new_env def ensure_stripped_str(str_or_bytes: str | bytes) -> str: if isinstance(str_or_bytes, str): return str_or_bytes.strip() else: return str_or_bytes.decode("utf-8", "surrogateescape").strip() def run( cmd: _t.CMD_TYPE, cwd: _t.PathT, *, strip: bool = True, trace: bool = True, timeout: int | None = None, check: bool = False, ) -> CompletedProcess: if isinstance(cmd, str): cmd = shlex.split(cmd) else: cmd = [os.fspath(x) for x in cmd] 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 res = subprocess.run( cmd, capture_output=True, cwd=os.fspath(cwd), env=dict( avoid_pip_isolation(no_git_env(os.environ)), # os.environ, # try to disable i18n, but still allow UTF-8 encoded text. LC_ALL="C.UTF-8", LANGUAGE="", HGPLAIN="1", ), text=True, encoding="utf-8", timeout=timeout, ) res = CompletedProcess.from_raw(res, strip=strip) if trace: if res.stdout: log.debug("out:\n%s", textwrap.indent(res.stdout, " ")) if res.stderr: log.debug("err:\n%s", textwrap.indent(res.stderr, " ")) if res.returncode: log.debug("ret: %s", res.returncode) if check: res.check_returncode() return res def _unsafe_quote_for_display(item: _t.PathT) -> str: # give better results than shlex.join in our cases text = os.fspath(item) return text if all(c not in text for c in " {[:") else f'"{text}"' def has_command( name: str, args: Sequence[str] = ["version"], warn: bool = True ) -> bool: try: p = run([name, *args], cwd=".") if p.returncode != 0: log.error("Command '%s' returned non-zero. This is stderr:", name) log.error(p.stderr) except OSError as e: log.warning("command %s missing: %s", name, e) res = False except subprocess.TimeoutExpired as e: log.warning("command %s timed out %s", name, e) res = False else: res = not p.returncode if not res and warn: warnings.warn(f"{name!r} was not found", category=RuntimeWarning) return res class CommandNotFoundError(LookupError, FileNotFoundError): pass def require_command(name: str) -> None: if not has_command(name, warn=False): raise CommandNotFoundError(name) setuptools-scm-8.2.1/src/setuptools_scm/_types.py000066400000000000000000000013041476647667300223020ustar00rootroot00000000000000from __future__ import annotations import os from typing import TYPE_CHECKING from typing import Callable from typing import List from typing import Sequence from typing import Tuple from typing import Union if TYPE_CHECKING: import sys if sys.version_info >= (3, 10): from typing import TypeAlias else: from typing_extensions import TypeAlias from . import version 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" setuptools-scm-8.2.1/src/setuptools_scm/_version_cls.py000066400000000000000000000062701476647667300234730ustar00rootroot00000000000000from __future__ import annotations from typing import Type from typing import Union from typing import 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] InvalidVersion, ) from setuptools.extern.packaging.version import ( # type: ignore[no-redef] Version as Version, ) from . import _log log = _log.log.getChild("version_cls") class NonNormalizedVersion(Version): """A non-normalizing version handler. You can use this class to preserve version verification but skip normalization. For example you can use this to avoid git release candidate version tags ("1.0.0-rc1") to be normalized to "1.0.0rc1". Only use this if you fully trust the version tags. """ def __init__(self, version: str) -> None: # parse and validate using parent super().__init__(version) # store raw for str self._raw_version = version def __str__(self) -> str: # return the non-normalized version (parent returns the normalized) return self._raw_version def __repr__(self) -> str: # same pattern as parent return f"" def _version_as_tuple(version_str: str) -> tuple[int | str, ...]: try: parsed_version = Version(version_str) except InvalidVersion as e: log.error("failed to parse version %s: %s", e, version_str) return (version_str,) else: version_fields: tuple[int | str, ...] = parsed_version.release if parsed_version.epoch: version_fields = (f"{parsed_version.epoch}!", *version_fields) if parsed_version.pre is not None: version_fields += (f"{parsed_version.pre[0]}{parsed_version.pre[1]}",) if parsed_version.post is not None: version_fields += (f"post{parsed_version.post}",) if parsed_version.dev is not None: version_fields += (f"dev{parsed_version.dev}",) if parsed_version.local is not None: version_fields += (parsed_version.local,) return version_fields _VersionT = Union[Version, NonNormalizedVersion] def import_name(name: str) -> object: import importlib pkg_name, cls_name = name.rsplit(".", 1) pkg = importlib.import_module(pkg_name) return getattr(pkg, cls_name) def _validate_version_cls( version_cls: type[_VersionT] | str | None, normalize: bool ) -> type[_VersionT]: if not normalize: if version_cls is not None: raise ValueError( "Providing a custom `version_cls` is not permitted when " "`normalize=False`" ) return NonNormalizedVersion # Use `version_cls` if provided, default to packaging or pkg_resources elif version_cls is None: return Version elif isinstance(version_cls, str): try: return cast(Type[_VersionT], import_name(version_cls)) except Exception: raise ValueError(f"Unable to import version_cls='{version_cls}'") from None else: return version_cls setuptools-scm-8.2.1/src/setuptools_scm/discover.py000066400000000000000000000037531476647667300226270ustar00rootroot00000000000000from __future__ import annotations import os from pathlib import Path from typing import Iterable from typing import Iterator from . import _entrypoints from . import _log from . import _types as _t from ._config import Configuration log = _log.log.getChild("discover") def walk_potential_roots(root: _t.PathT, search_parents: bool = True) -> Iterator[Path]: """ Iterate though a path and each of its parents. :param root: File path. :param search_parents: If ``False`` the parents are not considered. """ root = Path(root) yield root if search_parents: yield from root.parents def match_entrypoint(root: _t.PathT, name: str) -> bool: """ Consider a ``root`` as entry-point. :param root: File path. :param name: Subdirectory name. :return: ``True`` if a subdirectory ``name`` exits in ``root``. """ if os.path.exists(os.path.join(root, name)): if not os.path.isabs(name): return True log.debug("ignoring bad ep %s", name) return False # blocked entrypints from legacy plugins _BLOCKED_EP_TARGETS = {"setuptools_scm_git_archive:parse"} def iter_matching_entrypoints( root: _t.PathT, entrypoint: str, config: Configuration ) -> Iterable[_entrypoints.EntryPoint]: """ Consider different entry-points in ``root`` and optionally its parents. :param root: File path. :param entrypoint: Entry-point to consider. :param config: Configuration, read ``search_parent_directories``, write found parent to ``parent``. """ log.debug("looking for ep %s in %s", entrypoint, root) from ._entrypoints import iter_entry_points for wd in walk_potential_roots(root, config.search_parent_directories): for ep in iter_entry_points(entrypoint): if ep.value in _BLOCKED_EP_TARGETS: continue if match_entrypoint(wd, ep.name): log.debug("found ep %s in %s", ep, wd) config.parent = wd yield ep setuptools-scm-8.2.1/src/setuptools_scm/fallbacks.py000066400000000000000000000026501476647667300227260ustar00rootroot00000000000000from __future__ import annotations 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 log = logging.getLogger(__name__) _UNKNOWN = "UNKNOWN" def parse_pkginfo(root: _t.PathT, config: Configuration) -> ScmVersion | None: pkginfo = Path(root) / "PKG-INFO" log.debug("pkginfo %s", pkginfo) data = data_from_mime(pkginfo) version = data.get("Version", _UNKNOWN) if version != _UNKNOWN: return meta(version, preformatted=True, config=config) else: return None def fallback_version(root: _t.PathT, config: Configuration) -> ScmVersion | None: if config.parentdir_prefix_version is not None: _, parent_name = os.path.split(os.path.abspath(root)) if parent_name.startswith(config.parentdir_prefix_version): version = tag_to_version( parent_name[len(config.parentdir_prefix_version) :], config ) if version is not None: return meta(str(version), preformatted=True, config=config) if config.fallback_version is not None: log.debug("FALLBACK %s", config.fallback_version) return meta(config.fallback_version, preformatted=True, config=config) return None setuptools-scm-8.2.1/src/setuptools_scm/git.py000066400000000000000000000243451476647667300215740ustar00rootroot00000000000000from __future__ import annotations import dataclasses import logging import operator import os import re import shlex import sys import warnings from datetime import date from datetime import datetime from datetime import timezone 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 .version import ScmVersion from .version import meta from .version import tag_to_version if TYPE_CHECKING: from . import hg_git log = logging.getLogger(__name__) REF_TAG_RE = re.compile(r"(?<=\btag: )([^,]+)\b") DESCRIBE_UNSUPPORTED = "%(describe" # If testing command in shell make sure to quote the match argument like # '*[0-9]*' as it will expand before being sent to git if there are any matching # files in current directory. DEFAULT_DESCRIBE = [ "git", "describe", "--dirty", "--tags", "--long", "--match", "*[0-9]*", ] def run_git( args: Sequence[str | os.PathLike[str]], repo: Path, *, check: bool = False, timeout: int | None = None, ) -> _CompletedProcess: return _run( ["git", "--git-dir", repo / ".git", *args], cwd=repo, check=check, timeout=timeout, ) class GitWorkdir(Workdir): """experimental, may change at any time""" @classmethod def from_potential_worktree(cls, wd: _t.PathT) -> GitWorkdir | None: wd = Path(wd).resolve() real_wd = run_git(["rev-parse", "--show-prefix"], wd).parse_success(parse=str) if real_wd is None: return None else: real_wd = real_wd[:-1] # remove the trailing pathsep if not real_wd: real_wd = os.fspath(wd) else: str_wd = os.fspath(wd) assert str_wd.replace("\\", "/").endswith(real_wd) # In windows wd contains ``\`` which should be replaced by ``/`` # for this assertion to work. Length of string isn't changed by replace # ``\\`` is just and escape for `\` real_wd = str_wd[: -len(real_wd)] log.debug("real root %s", real_wd) if not samefile(real_wd, wd): return None return cls(Path(real_wd)) def is_dirty(self) -> bool: return run_git( ["status", "--porcelain", "--untracked-files=no"], self.path ).parse_success( parse=bool, default=False, ) def get_branch(self) -> str | None: return run_git( ["rev-parse", "--abbrev-ref", "HEAD"], self.path, ).parse_success( parse=str, error_msg="branch err (abbrev-err)", ) or run_git( ["symbolic-ref", "--short", "HEAD"], self.path, ).parse_success( parse=str, error_msg="branch err (symbolic-ref)", ) def get_head_date(self) -> date | None: def parse_timestamp(timestamp_text: str) -> date | None: if "%c" in timestamp_text: log.warning("git too old -> timestamp is %r", timestamp_text) return None if sys.version_info < (3, 11) and timestamp_text.endswith("Z"): timestamp_text = timestamp_text[:-1] + "+00:00" return datetime.fromisoformat(timestamp_text).date() res = run_git( [ *("-c", "log.showSignature=false"), *("log", "-n", "1", "HEAD"), "--format=%cI", ], self.path, ) return res.parse_success( parse=parse_timestamp, error_msg="logging the iso date for head failed", ) def is_shallow(self) -> bool: return self.path.joinpath(".git/shallow").is_file() def fetch_shallow(self) -> None: run_git(["fetch", "--unshallow"], self.path, check=True, timeout=240) def node(self) -> str | None: unsafe_short_node = operator.itemgetter(slice(7)) return run_git( ["rev-parse", "--verify", "--quiet", "HEAD"], self.path ).parse_success( parse=unsafe_short_node, ) def count_all_nodes(self) -> int: res = run_git(["rev-list", "HEAD"], self.path) return res.stdout.count("\n") + 1 def default_describe(self) -> _CompletedProcess: return run_git(DEFAULT_DESCRIBE[1:], self.path) 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') 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') wd.fetch_shallow() def fail_on_shallow(wd: GitWorkdir) -> None: """experimental, may change at any time""" if wd.is_shallow(): raise ValueError( f'{wd.path} is shallow, please correct with "git fetch --unshallow"' ) def get_working_directory(config: Configuration, root: _t.PathT) -> GitWorkdir | None: """ Return the working directory (``GitWorkdir``). """ if config.parent: # todo broken return GitWorkdir.from_potential_worktree(config.parent) for potential_root in discover.walk_potential_roots( root, search_parents=config.search_parent_directories ): potential_wd = GitWorkdir.from_potential_worktree(potential_root) if potential_wd is not None: return potential_wd return GitWorkdir.from_potential_worktree(root) def parse( root: _t.PathT, config: Configuration, describe_command: str | list[str] | None = None, pre_parse: Callable[[GitWorkdir], None] = warn_on_shallow, ) -> ScmVersion | None: """ :param pre_parse: experimental pre_parse action, may change at any time """ _require_command("git") wd = get_working_directory(config, root) if wd: return _git_parse_inner( config, wd, describe_command=describe_command, pre_parse=pre_parse ) else: return None def version_from_describe( wd: GitWorkdir | hg_git.GitWorkdirHgClient, config: Configuration, describe_command: _t.CMD_TYPE | None, ) -> ScmVersion | None: if config.git_describe_command is not None: describe_command = config.git_describe_command if describe_command is not None: if isinstance(describe_command, str): describe_command = shlex.split(describe_command) # todo: figure how to ensure git with gitdir gets correctly invoked if describe_command[0] == "git": describe_res = run_git(describe_command[1:], wd.path) else: describe_res = _run(describe_command, wd.path) else: describe_res = wd.default_describe() def parse_describe(output: str) -> ScmVersion: tag, distance, node, dirty = _git_parse_describe(output) return meta(tag=tag, distance=distance, dirty=dirty, node=node, config=config) return describe_res.parse_success(parse=parse_describe) def _git_parse_inner( config: Configuration, wd: GitWorkdir | hg_git.GitWorkdirHgClient, pre_parse: (Callable[[GitWorkdir | hg_git.GitWorkdirHgClient], None]) | None = None, describe_command: _t.CMD_TYPE | None = None, ) -> ScmVersion: if pre_parse: pre_parse(wd) version = version_from_describe(wd, config, describe_command) if version is None: # If 'git git_describe_command' failed, try to get the information otherwise. tag = config.version_cls("0.0") node = wd.node() if node is None: distance = 0 dirty = True else: distance = wd.count_all_nodes() node = "g" + node dirty = wd.is_dirty() version = meta( tag=tag, distance=distance, dirty=dirty, node=node, config=config ) branch = wd.get_branch() node_date = wd.get_head_date() or datetime.now(timezone.utc).date() return dataclasses.replace(version, branch=branch, node_date=node_date) def _git_parse_describe( describe_output: str, ) -> tuple[str, int, str | None, bool]: # 'describe_output' looks e.g. like 'v1.5.0-0-g4060507' or # 'v1.15.1rc1-37-g9bd1298-dirty'. # It may also just be a bare tag name if this is a tagged commit and we are # parsing a .git_archival.txt file. if describe_output.endswith("-dirty"): dirty = True describe_output = describe_output[:-6] else: dirty = False split = describe_output.rsplit("-", 2) if len(split) < 3: # probably a tagged commit tag = describe_output number = 0 node = None else: tag, number_, node = split number = int(number_) return tag, number, node, dirty def archival_to_version( data: dict[str, str], config: Configuration ) -> ScmVersion | None: node: str | None 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") else: tag, number, node, _ = _git_parse_describe(archival_describe) return meta( tag, config=config, distance=number, node=node, ) for ref in REF_TAG_RE.findall(data.get("ref-names", "")): version = tag_to_version(ref, config) if version is not None: return meta(version, config=config) node = data.get("node") if node is None: return None elif "$FORMAT" in node.upper(): warnings.warn("unprocessed git archival found (no export subst applied)") return None else: return meta("0.0", node=node, config=config) def parse_archival(root: _t.PathT, config: Configuration) -> ScmVersion | None: archival = os.path.join(root, ".git_archival.txt") data = data_from_mime(archival) return archival_to_version(data, config=config) setuptools-scm-8.2.1/src/setuptools_scm/hg.py000066400000000000000000000140561476647667300214050ustar00rootroot00000000000000from __future__ import annotations import datetime import logging import os from pathlib import Path from typing import TYPE_CHECKING from . import Configuration from ._version_cls import Version from .integration import data_from_mime from .scm_workdir import Workdir 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 require_command as _require_command from ._run_cmd import run as _run log = logging.getLogger(__name__) class HgWorkdir(Workdir): @classmethod def from_potential_worktree(cls, wd: _t.PathT) -> HgWorkdir | None: res = _run(["hg", "root"], wd) if res.returncode: return None return cls(Path(res.stdout)) def get_meta(self, config: Configuration) -> ScmVersion | None: node: str tags_str: str node_date_str: str node, tags_str, node_date_str = self.hg_log( ".", "{node}\n{tag}\n{date|shortdate}" ).split("\n") # TODO: support bookmarks and topics (but nowadays bookmarks are # mainly used to emulate Git branches, which is already supported with # the dedicated class GitWorkdirHgClient) branch, dirty_str, dirty_date = _run( ["hg", "id", "-T", "{branch}\n{if(dirty, 1, 0)}\n{date|shortdate}"], cwd=self.path, check=True, ).stdout.split("\n") dirty = bool(int(dirty_str)) node_date = datetime.date.fromisoformat(dirty_date if dirty else node_date_str) if node == "0" * len(node): log.debug("initial node %s", self.path) return meta( Version("0.0"), config=config, dirty=dirty, branch=branch, node_date=node_date, ) node = "h" + node[:7] tags = tags_str.split() if "tip" in tags: # tip is not a real tag tags.remove("tip") if tags: tag = tag_to_version(tags[0], config) if tag: return meta(tag, dirty=dirty, branch=branch, config=config) try: tag_str = self.get_latest_normalizable_tag() if tag_str is None: dist = self.get_distance_revs("") else: dist = self.get_distance_revs(tag_str) if tag_str == "null" or tag_str is None: tag = Version("0.0") dist += 1 else: tag = tag_to_version(tag_str, config=config) assert tag is not None if self.check_changes_since_tag(tag_str) or dirty: return meta( tag, distance=dist, node=node, dirty=dirty, branch=branch, config=config, node_date=node_date, ) else: return meta(tag, config=config, node_date=node_date) except ValueError: # unpacking failed, old hg log.exception("error") return None def hg_log(self, revset: str, template: str) -> str: cmd = ["hg", "log", "-r", revset, "-T", template] return _run(cmd, cwd=self.path, check=True).stdout def get_latest_normalizable_tag(self) -> str | None: # Gets all tags containing a '.' (see #229) from oldest to newest outlines = self.hg_log( revset="ancestors(.) and tag('re:\\.')", template="{tags}{if(tags, '\n', '')}", ).split() if not outlines: return None tag = outlines[-1].split()[-1] return tag def get_distance_revs(self, rev1: str, rev2: str = ".") -> int: revset = f"({rev1}::{rev2})" out = self.hg_log(revset, ".") return len(out) - 1 def check_changes_since_tag(self, tag: str | None) -> bool: if tag == "0.0" or tag is None: return True revset = ( "(branch(.)" # look for revisions in this branch only f" and tag({tag!r})::." # after the last tag # ignore commits that only modify .hgtags and nothing else: " and (merge() or file('re:^(?!\\.hgtags).*$'))" f" and not tag({tag!r}))" # ignore the tagged commit itself ) return bool(self.hg_log(revset, ".")) def parse(root: _t.PathT, config: Configuration) -> ScmVersion | None: _require_command("hg") if os.path.exists(os.path.join(root, ".hg/git")): res = _run(["hg", "path"], root) if not res.returncode: for line in res.stdout.split("\n"): 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 wd_hggit = GitWorkdirHgClient.from_potential_worktree(root) if wd_hggit: return _git_parse_inner(config, wd_hggit) wd = HgWorkdir.from_potential_worktree(config.absolute_root) if wd is None: return None return wd.get_meta(config) def archival_to_version(data: dict[str, str], config: Configuration) -> ScmVersion: log.debug("data %s", data) node = data.get("node", "")[:12] if node: node = "h" + node if "tag" in data: return meta(data["tag"], config=config) elif "latesttag" in data: return meta( data["latesttag"], distance=int(data["latesttagdistance"]), node=node, branch=data.get("branch"), config=config, ) else: return meta(config.version_cls("0.0"), node=node, config=config) def parse_archival(root: _t.PathT, config: Configuration) -> ScmVersion: archival = os.path.join(root, ".hg_archival.txt") data = data_from_mime(archival) return archival_to_version(data, config=config) setuptools-scm-8.2.1/src/setuptools_scm/hg_git.py000066400000000000000000000107021476647667300222420ustar00rootroot00000000000000from __future__ import annotations 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 ._run_cmd import require_command from ._run_cmd import run as _run from .git import GitWorkdir from .hg import HgWorkdir log = logging.getLogger(__name__) _FAKE_GIT_DESCRIBE_ERROR = _CompletedProcess( "fake git describe output for hg", 1, "<>hg git failed to describe", ) class GitWorkdirHgClient(GitWorkdir, HgWorkdir): COMMAND = "hg" @classmethod def from_potential_worktree(cls, wd: _t.PathT) -> GitWorkdirHgClient | None: require_command("hg") res = _run(["hg", "root"], cwd=wd).parse_success(parse=Path) if res is None: return None return cls(res) def is_dirty(self) -> bool: res = _run(["hg", "id", "-T", "{dirty}"], cwd=self.path, check=True) return bool(res.stdout) def get_branch(self) -> str | None: res = _run(["hg", "id", "-T", "{bookmarks}"], cwd=self.path) if res.returncode: log.info("branch err %s", res) return None return res.stdout def get_head_date(self) -> date | None: return _run('hg log -r . -T "{shortdate(date)}"', cwd=self.path).parse_success( parse=date.fromisoformat, error_msg="head date err" ) def is_shallow(self) -> bool: return False def fetch_shallow(self) -> None: pass def get_hg_node(self) -> str | None: res = _run('hg log -r . -T "{node}"', cwd=self.path) if res.returncode: return None else: return res.stdout def _hg2git(self, hg_node: str) -> str | None: with suppress(FileNotFoundError): with open(os.path.join(self.path, ".hg/git-mapfile")) as map_items: for item in map_items: if hg_node in item: git_node, hg_node = item.split() return git_node return None def node(self) -> str | None: hg_node = self.get_hg_node() if hg_node is None: return None git_node = self._hg2git(hg_node) if git_node is None: # trying again after hg -> git _run(["hg", "gexport"], cwd=self.path) git_node = self._hg2git(hg_node) if git_node is None: log.debug("Cannot get git node so we use hg node %s", hg_node) if hg_node == "0" * len(hg_node): # mimic Git behavior return None return hg_node return git_node[:7] def count_all_nodes(self) -> int: res = _run(["hg", "log", "-r", "ancestors(.)", "-T", "."], cwd=self.path) return len(res.stdout) def default_describe(self) -> _CompletedProcess: """ Tentative to reproduce the output of `git describe --dirty --tags --long --match *[0-9]*` """ res = _run( [ "hg", "log", "-r", "(reverse(ancestors(.)) and tag(r're:v?[0-9].*'))", "-T", "{tags}{if(tags, ' ', '')}", ], cwd=self.path, ) if res.returncode: return _FAKE_GIT_DESCRIBE_ERROR hg_tags: list[str] = res.stdout.split() if not hg_tags: return _FAKE_GIT_DESCRIBE_ERROR with self.path.joinpath(".hg/git-tags").open() as fp: git_tags: dict[str, str] = dict(line.split()[::-1] for line in fp) tag: str for hg_tag in hg_tags: if hg_tag in git_tags: tag = hg_tag break else: logging.warning("tag not found hg=%s git=%s", hg_tags, git_tags) return _FAKE_GIT_DESCRIBE_ERROR res = _run(["hg", "log", "-r", f"'{tag}'::.", "-T", "."], cwd=self.path) if res.returncode: return _FAKE_GIT_DESCRIBE_ERROR distance = len(res.stdout) - 1 node = self.node() assert node is not None desc = f"{tag}-{distance}-g{node}" if self.is_dirty(): desc += "-dirty" log.debug("faked describe %r", desc) return _CompletedProcess( ["setuptools-scm", "faked", "describe"], returncode=0, stdout=desc, stderr="", ) setuptools-scm-8.2.1/src/setuptools_scm/integration.py000066400000000000000000000014461476647667300233310ustar00rootroot00000000000000from __future__ import annotations import logging import textwrap from pathlib import Path from . import _types as _t log = logging.getLogger(__name__) def data_from_mime(path: _t.PathT, content: str | None = None) -> dict[str, str]: """return a mapping from mime/pseudo-mime content :param path: path to the mime file :param content: content of the mime file, if None, read from path :rtype: dict[str, str] """ if content is None: content = Path(path).read_text(encoding="utf-8") log.debug("mime %s content:\n%s", path, textwrap.indent(content, " ")) from email.parser import HeaderParser parser = HeaderParser() message = parser.parsestr(content) data = dict(message.items()) log.debug("mime %s data:\n%s", path, data) return data setuptools-scm-8.2.1/src/setuptools_scm/scm_workdir.py000066400000000000000000000005071476647667300233260ustar00rootroot00000000000000from __future__ import annotations from dataclasses import dataclass from pathlib import Path from ._config import Configuration from .version import ScmVersion @dataclass() class Workdir: path: Path def run_describe(self, config: Configuration) -> ScmVersion: raise NotImplementedError(self.run_describe) setuptools-scm-8.2.1/src/setuptools_scm/version.py000066400000000000000000000335111476647667300224710ustar00rootroot00000000000000from __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 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 _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 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 ) result = _TagDict( version=match.group(key), prefix=full[: match.start(key)], suffix=full[match.end(key) :], ) log.debug("tag %r parsed to %r", tag, result) assert result["version"] return result 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 iter_entry_points for ep in iter_entry_points(group, callable_or_name): log.debug("ep found: %s", ep.name) return ep.load() 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) if suffix := tag_dict.get("suffix", ""): warnings.warn(f"tag {tag!r} will be stripped of its suffix {suffix!r}") version: _VersionT = config.version_cls(version_str) log.debug("version=%r", 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 | str """the related tag or preformatted version string""" 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 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=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 _parse_tag( tag: _VersionT | str, preformatted: bool, config: _config.Configuration ) -> _VersionT | str: if preformatted: 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, ) -> ScmVersion: 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}" return ScmVersion( parsed_version, distance=distance, node=node, dirty=dirty, preformatted=preformatted, branch=branch, config=config, node_date=node_date, ) 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: tag_date = today else: tag_date = ( datetime.strptime(match.group("date"), date_fmt) .replace(tzinfo=timezone.utc) .date() ) if tag_date == head_date: patch = "0" if match is None else (match.group("patch") or "0") patch = int(patch) + 1 else: if tag_date > head_date and match is not None: # warn on future times 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 format_version(version: ScmVersion) -> str: log.debug("scm version %s", version) log.debug("config %s", version.config) if version.preformatted: assert isinstance(version.tag, str) return version.tag main_version = _entrypoints._call_version_scheme( version, "setuptools_scm.version_scheme", version.config.version_scheme, None ) 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) return main_version + local_version setuptools-scm-8.2.1/testing/000077500000000000000000000000001476647667300162325ustar00rootroot00000000000000setuptools-scm-8.2.1/testing/Dockerfile.busted-buster000066400000000000000000000002771476647667300230210ustar00rootroot00000000000000FROM debian:buster RUN apt-get update -q && apt-get install -yq python3-pip python3-setuptools RUN printf "[easy_install]\nallow_hosts=localhost\nfind_links=/dist\n" > /root/.pydistutils.cfg setuptools-scm-8.2.1/testing/Dockerfile.rawhide-git000066400000000000000000000003251476647667300224270ustar00rootroot00000000000000FROM registry.fedoraproject.org/fedora:rawhide RUN dnf install git -y RUN git --version USER 1000:1000 VOLUME /repo WORKDIR /repo ENTRYPOINT mkdir git-archived && git archive HEAD -o git-archived/archival.tar.gz setuptools-scm-8.2.1/testing/__init__.py000066400000000000000000000000001476647667300203310ustar00rootroot00000000000000setuptools-scm-8.2.1/testing/conftest.py000066400000000000000000000054271476647667300204410ustar00rootroot00000000000000from __future__ import annotations import contextlib import os import sys 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 if sys.version_info >= (3, 11): from typing import Self else: from typing_extensions import Self from .wd_wrapper import WorkDir def pytest_configure() -> None: # 2009-02-13T23:31:30+00:00 os.environ["SOURCE_DATE_EPOCH"] = "1234567890" os.environ["SETUPTOOLS_SCM_DEBUG"] = "1" VERSION_PKGS = ["setuptools", "setuptools_scm", "packaging", "build", "wheel"] 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__ res.append(f"{pkg} version {pkg_version} from {path!r}") return res def pytest_addoption(parser: Any) -> None: group = parser.getgroup("setuptools_scm") group.addoption( "--test-legacy", dest="scm_test_virtualenv", default=False, action="store_true" ) class DebugMode(contextlib.AbstractContextManager): # type: ignore[type-arg] from setuptools_scm import _log as __module def __init__(self) -> None: self.__stack = contextlib.ExitStack() def __enter__(self) -> Self: self.enable() return self def __exit__( self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: TracebackType | None, ) -> None: self.disable() def enable(self) -> None: self.__stack.enter_context(self.__module.defer_to_pytest()) def disable(self) -> None: self.__stack.close() @pytest.fixture(autouse=True) def debug_mode() -> Iterator[DebugMode]: with DebugMode() as debug_mode: yield debug_mode @pytest.fixture def wd(tmp_path: Path) -> WorkDir: target_wd = tmp_path.resolve() / "wd" target_wd.mkdir() return WorkDir(target_wd) @pytest.fixture def repositories_hg_git(tmp_path: Path) -> tuple[WorkDir, WorkDir]: tmp_path = tmp_path.resolve() path_git = tmp_path / "repo_git" path_git.mkdir() wd = WorkDir(path_git) wd("git init") wd("git config user.email test@example.com") wd('git config user.name "a test"') wd.add_command = "git add ." wd.commit_command = "git commit -m test-{reason}" path_hg = tmp_path / "repo_hg" run(["hg", "clone", path_git, path_hg, "--config", "extensions.hggit="], tmp_path) assert path_hg.exists() with open(path_hg / ".hg/hgrc", "a") as file: file.write("[extensions]\nhggit =\n") wd_hg = WorkDir(path_hg) wd_hg.add_command = "hg add ." wd_hg.commit_command = 'hg commit -m test-{reason} -u test -d "0 0"' return wd_hg, wd setuptools-scm-8.2.1/testing/play_out_381.bash000077500000000000000000000011011476647667300213140ustar00rootroot00000000000000#!/usr/bin/env bash set -euxo pipefail rm -rf y z home venv tmp [ ! -d black ] && git clone https://github.com/psf/black export SETUPTOOLS_SCM_DEBUG=1 export PRE_COMMIT_HOME="$PWD/home" export TMPDIR="$PWD/tmp" git init y git init z git -C z commit --allow-empty -m 'commit!' git -C y submodule add "$PWD/z" cat > "$PWD/y/.git/modules/z/hooks/pre-commit" < None: run([sys.executable, "-c", "print(1)"], cwd=tmp_path) def test_data_from_mime(tmp_path: Path) -> None: tmpfile = tmp_path.joinpath("test.archival") tmpfile.write_bytes(b"name: test\nrevision: 1") res = data_from_mime(str(tmpfile)) assert res == {"name": "test", "revision": "1"} def test_version_from_pkginfo(wd: WorkDir) -> None: wd.write("PKG-INFO", "Version: 0.1") assert wd.get_version() == "0.1" # replicate issue 167 assert wd.get_version(version_scheme="1.{0.distance}.0".format) == "0.1" def assert_root(monkeypatch: pytest.MonkeyPatch, expected_root: str) -> None: """ Patch version_from_scm to simply assert that root is expected root """ def assertion(config: Configuration) -> ScmVersion: assert config.absolute_root == expected_root return ScmVersion("1.0", config=config) monkeypatch.setattr(setuptools_scm._get_version_impl, "parse_version", assertion) def test_root_parameter_creation(monkeypatch: pytest.MonkeyPatch) -> None: assert_root(monkeypatch, os.getcwd()) setuptools_scm.get_version() def test_root_parameter_pass_by( monkeypatch: pytest.MonkeyPatch, tmp_path: Path ) -> None: assert_root(monkeypatch, os.fspath(tmp_path)) setuptools_scm.get_version(root=os.fspath(tmp_path)) setuptools_scm.get_version( os.fspath(tmp_path) ) # issue 669 - posarg difference between Configuration and get_version def test_parentdir_prefix(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.delenv("SETUPTOOLS_SCM_DEBUG") p = tmp_path.joinpath("projectname-v12.34") p.mkdir() p.joinpath("setup.py").write_text( """from setuptools import setup setup(use_scm_version={"parentdir_prefix_version": "projectname-"}) """, encoding="utf-8", ) res = run([sys.executable, "setup.py", "--version"], p) assert res.stdout == "12.34" def test_fallback(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.delenv("SETUPTOOLS_SCM_DEBUG") p = tmp_path / "sub/package" p.mkdir(parents=True) p.joinpath("setup.py").write_text( """from setuptools import setup setup(use_scm_version={"fallback_version": "12.34"}) """, encoding="utf-8", ) res = run([sys.executable, "setup.py", "--version"], p) assert res.stdout == "12.34" def test_empty_pretend_version(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.delenv("SETUPTOOLS_SCM_DEBUG") monkeypatch.setenv("SETUPTOOLS_SCM_PRETEND_VERSION", "") p = tmp_path / "sub/package" p.mkdir(parents=True) p.joinpath("setup.py").write_text( """from setuptools import setup setup(use_scm_version={"fallback_version": "12.34"}) """, encoding="utf-8", ) res = run([sys.executable, "setup.py", "--version"], p) assert res.stdout == "12.34" def test_empty_pretend_version_named( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: monkeypatch.delenv("SETUPTOOLS_SCM_DEBUG") monkeypatch.setenv("SETUPTOOLS_SCM_PRETEND_VERSION", "1.23") monkeypatch.setenv("SETUPTOOLS_SCM_PRETEND_VERSION_FOR_MYSCM", "") p = tmp_path.joinpath("sub/package") p.mkdir(parents=True) p.joinpath("setup.py").write_text( """from setuptools import setup setup(name="myscm", use_scm_version={"fallback_version": "12.34"}) """, encoding="utf-8", ) res = run([sys.executable, "setup.py", "--version"], p) assert res.stdout == "12.34" def test_get_version_blank_tag_regex() -> None: with pytest.warns( DeprecationWarning, match="empty regex for tag regex is invalid, using default" ): setuptools_scm.get_version(tag_regex="") @pytest.mark.parametrize( "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) assert setuptools_scm.get_version() == version def test_root_relative_to(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None: tmp_path.joinpath("setup.cfg").touch() assert_root(monkeypatch, str(tmp_path / "alt")) module = tmp_path / "module/file.py" module.parent.mkdir() module.touch() setuptools_scm.get_version( root="../alt", relative_to=str(module), ) with pytest.warns(UserWarning, match="relative_to is expected to be a file.*"): setuptools_scm.get_version( root="../alt", relative_to=str(module.parent), ) def test_dump_version(tmp_path: Path) -> None: version = "1.0" scm_version = meta(version, config=c) dump_version(tmp_path, version, "first.txt", scm_version=scm_version) def read(name: str) -> str: return tmp_path.joinpath(name).read_text(encoding="utf-8") assert read("first.txt") == "1.0" version = "1.0.dev42" scm_version = meta("1.0", distance=42, config=c) dump_version(tmp_path, version, "first.py", scm_version=scm_version) lines = read("first.py").splitlines() assert lines[-2:] == [ "__version__ = version = '1.0.dev42'", "__version_tuple__ = version_tuple = (1, 0, 'dev42')", ] version = "1.0.1+g4ac9d2c" scm_version = meta("1.0.1", node="g4ac9d2c", config=c) dump_version( tmp_path, version, "second.py", scm_version=scm_version, template=template ) lines = read("second.py").splitlines() assert "__version__ = version = '1.0.1+g4ac9d2c'" in lines assert "__version_tuple__ = version_tuple = (1, 0, 1, 'g4ac9d2c')" in lines assert "__sha__ = 'g4ac9d2c'" in lines version = "1.2.3.dev18+gb366d8b.d20210415" scm_version = meta( "1.2.3", node="gb366d8b", distance=18, node_date=date(2021, 4, 15), config=c ) dump_version( tmp_path, version, "third.py", scm_version=scm_version, template=template ) lines = read("third.py").splitlines() assert "__version__ = version = '1.2.3.dev18+gb366d8b.d20210415'" in lines assert ( "__version_tuple__ = version_tuple = (1, 2, 3, 'dev18', 'gb366d8b.d20210415')" in lines ) assert "__sha__ = 'gb366d8b'" in lines import ast ast.parse(read("third.py")) def test_parse_plain_fails(recwarn: pytest.WarningsRecorder) -> None: def parse(root: object) -> str: return "tricked you" with pytest.raises(TypeError): setuptools_scm.get_version(parse=parse) def test_custom_version_cls() -> None: """Test that `normalize` and `version_cls` work as expected""" class MyVersion: def __init__(self, tag_str: str) -> None: self.version = tag_str def __repr__(self) -> str: return f"hello,{self.version}" # you can not use normalize=False and version_cls at the same time with pytest.raises( ValueError, match="Providing a custom `version_cls`" " is not permitted when `normalize=False`", ): setuptools_scm.get_version(normalize=False, version_cls=MyVersion) # TODO unfortunately with PRETEND_KEY the preformatted flag becomes True # which bypasses our class. which other mechanism would be ok to use here # to create a test? # monkeypatch.setenv(setuptools_scm.PRETEND_KEY, "1.0.1") # assert setuptools_scm.get_version(version_cls=MyVersion) == "1" def test_internal_get_version_warns_for_version_files(tmp_path: Path) -> None: tmp_path.joinpath("PKG-INFO").write_bytes(b"Version: 0.1") c = Configuration(root=tmp_path, fallback_root=tmp_path) with pytest.warns( DeprecationWarning, match="force_write_version_files ought to be set," " presuming the legacy True value", ): ver = setuptools_scm._get_version(c) assert ver == "0.1" # force write won't write as no version file is configured assert setuptools_scm._get_version(c, force_write_version_files=False) == ver assert setuptools_scm._get_version(c, force_write_version_files=True) == ver setuptools-scm-8.2.1/testing/test_cli.py000066400000000000000000000040301476647667300204070ustar00rootroot00000000000000from __future__ import annotations import io from contextlib import redirect_stdout import pytest from setuptools_scm._cli import main from .conftest import DebugMode from .test_git import wd as wd_fixture # noqa: F401 (evil fixture reuse) from .wd_wrapper import WorkDir PYPROJECT_TOML = "pyproject.toml" PYPROJECT_SIMPLE = "[tool.setuptools_scm]" PYPROJECT_ROOT = '[tool.setuptools_scm]\nroot=".."' def get_output(args: list[str]) -> str: with redirect_stdout(io.StringIO()) as out: main(args) return out.getvalue() warns_cli_root_override = pytest.warns( UserWarning, match="root .. is overridden by the cli arg .*" ) exits_with_not_found = pytest.raises(SystemExit, match="no version found for") def test_cli_find_pyproject( wd: WorkDir, monkeypatch: pytest.MonkeyPatch, debug_mode: DebugMode ) -> None: wd.commit_testfile() wd.write(PYPROJECT_TOML, PYPROJECT_SIMPLE) monkeypatch.chdir(wd.cwd) out = get_output([]) assert out.startswith("0.1.dev1+") with exits_with_not_found: get_output(["--root=.."]) wd.write(PYPROJECT_TOML, PYPROJECT_ROOT) with exits_with_not_found: print(get_output(["-c", PYPROJECT_TOML])) with warns_cli_root_override, exits_with_not_found: get_output(["-c", PYPROJECT_TOML, "--root=.."]) with warns_cli_root_override: out = get_output(["-c", PYPROJECT_TOML, "--root=."]) assert out.startswith("0.1.dev1+") def test_cli_force_version_files( wd: WorkDir, monkeypatch: pytest.MonkeyPatch, debug_mode: DebugMode ) -> None: debug_mode.disable() wd.commit_testfile() wd.write( PYPROJECT_TOML, """ [project] name = "test" [tool.setuptools_scm] version_file = "ver.py" """, ) monkeypatch.chdir(wd.cwd) version_file = wd.cwd.joinpath("ver.py") assert not version_file.exists() get_output([]) assert not version_file.exists() output = get_output(["--force-write-version-files"]) assert version_file.exists() assert output[:5] in version_file.read_text("utf-8") setuptools-scm-8.2.1/testing/test_config.py000066400000000000000000000051661476647667300211200ustar00rootroot00000000000000from __future__ import annotations import re import textwrap from pathlib import Path import pytest 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( textwrap.dedent( """ [tool.setuptools_scm] [project] description = "Factory ⸻ A code generator 🏭" authors = [{name = "Łukasz Langa"}] """ ), 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 def test_config_from_file_protects_relative_to(tmp_path: Path) -> None: fn = tmp_path / "pyproject.toml" fn.write_text( textwrap.dedent( """ [tool.setuptools_scm] relative_to = "dont_use_me" [project] description = "Factory ⸻ A code generator 🏭" authors = [{name = "Łukasz Langa"}] """ ), encoding="utf-8", ) with pytest.warns( UserWarning, match=".*pyproject.toml: at \\[tool.setuptools_scm\\]\n" "ignoring value relative_to='dont_use_me'" " as its always relative to the config file", ): assert Configuration.from_file(str(fn)) def test_config_overrides(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: fn = tmp_path / "pyproject.toml" fn.write_text( textwrap.dedent( """ [tool.setuptools_scm] root = "." [project] name = "teSt-.a" """ ), encoding="utf-8", ) pristine = Configuration.from_file(fn) monkeypatch.setenv( "SETUPTOOLS_SCM_OVERRIDES_FOR_TEST_A", '{root="..", fallback_root=".."}' ) overridden = Configuration.from_file(fn) assert pristine.root != overridden.root assert pristine.fallback_root != overridden.fallback_root setuptools-scm-8.2.1/testing/test_file_finder.py000066400000000000000000000204721476647667300221160ustar00rootroot00000000000000from __future__ import annotations import os import sys from typing import Iterable import pytest from setuptools_scm._file_finders import find_files from .wd_wrapper import WorkDir @pytest.fixture(params=["git", "hg"]) def inwd( request: pytest.FixtureRequest, wd: WorkDir, monkeypatch: pytest.MonkeyPatch ) -> WorkDir: param: str = request.param # type: ignore[attr-defined] if param == "git": try: wd("git init") except OSError: pytest.skip("git executable not found") wd("git config user.email test@example.com") wd('git config user.name "a test"') wd.add_command = "git add ." wd.commit_command = "git commit -m test-{reason}" elif param == "hg": try: wd("hg init") except OSError: pytest.skip("hg executable not found") wd.add_command = "hg add ." wd.commit_command = 'hg commit -m test-{reason} -u test -d "0 0"' (wd.cwd / "file1").touch() adir = wd.cwd / "adir" adir.mkdir() (adir / "filea").touch() bdir = wd.cwd / "bdir" bdir.mkdir() (bdir / "fileb").touch() if request.node.get_closest_marker("skip_commit") is None: wd.add_and_commit() monkeypatch.chdir(wd.cwd) return wd def _sep(paths: Iterable[str]) -> set[str]: return {path.replace("/", os.path.sep) for path in paths} def test_basic(inwd: WorkDir) -> None: assert set(find_files()) == _sep({"file1", "adir/filea", "bdir/fileb"}) assert set(find_files(".")) == _sep({"./file1", "./adir/filea", "./bdir/fileb"}) assert set(find_files("adir")) == _sep({"adir/filea"}) def test_whitespace(inwd: WorkDir) -> None: (inwd.cwd / "adir" / "space file").touch() inwd.add_and_commit() assert set(find_files("adir")) == _sep({"adir/space file", "adir/filea"}) def test_case(inwd: WorkDir) -> None: (inwd.cwd / "CamelFile").touch() (inwd.cwd / "file2").touch() inwd.add_and_commit() assert set(find_files()) == _sep( {"CamelFile", "file2", "file1", "adir/filea", "bdir/fileb"} ) @pytest.mark.skipif( os.path.normcase("B") != os.path.normcase("b"), reason="case sensitive filesystem" ) def test_case_cwd_evil(inwd: WorkDir, monkeypatch: pytest.MonkeyPatch) -> None: (inwd.cwd / "CamelFile").touch() (inwd.cwd / "file2").touch() inwd.add_and_commit() monkeypatch.chdir(inwd.cwd.parent.joinpath(inwd.cwd.name.capitalize())) assert set(find_files()) == _sep( {"CamelFile", "file2", "file1", "adir/filea", "bdir/fileb"} ) @pytest.mark.skipif(sys.platform == "win32", reason="symlinks to dir not supported") def test_symlink_dir(inwd: WorkDir) -> None: (inwd.cwd / "adir" / "bdirlink").symlink_to("../bdir") inwd.add_and_commit() assert set(find_files("adir")) == _sep({"adir/filea", "adir/bdirlink/fileb"}) @pytest.mark.skipif(sys.platform == "win32", reason="symlinks to dir not supported") def test_symlink_dir_source_not_in_scm(inwd: WorkDir) -> None: (inwd.cwd / "adir" / "bdirlink").symlink_to("../bdir") assert set(find_files("adir")) == _sep({"adir/filea"}) @pytest.mark.skipif( sys.platform == "win32", reason="symlinks to files not supported on windows" ) def test_symlink_file(inwd: WorkDir) -> None: (inwd.cwd / "adir" / "file1link").symlink_to("../file1") inwd.add_and_commit() assert set(find_files("adir")) == _sep( {"adir/filea", "adir/file1link"} ) # -> ../file1 @pytest.mark.skipif( sys.platform == "win32", reason="symlinks to files not supported on windows" ) def test_symlink_file_source_not_in_scm(inwd: WorkDir) -> None: (inwd.cwd / "adir" / "file1link").symlink_to("../file1") assert set(find_files("adir")) == _sep({"adir/filea"}) @pytest.mark.skipif(sys.platform == "win32", reason="symlinks to dir not supported") def test_symlink_loop(inwd: WorkDir) -> None: (inwd.cwd / "adir" / "loop").symlink_to("../adir") inwd.add_and_commit() assert set(find_files("adir")) == _sep({"adir/filea", "adir/loop"}) # -> ../adir @pytest.mark.skipif(sys.platform == "win32", reason="symlinks to dir not supported") def test_symlink_loop_outside_path(inwd: WorkDir) -> None: (inwd.cwd / "bdir" / "loop").symlink_to("../bdir") (inwd.cwd / "adir" / "bdirlink").symlink_to("../bdir") inwd.add_and_commit() assert set(find_files("adir")) == _sep({"adir/filea", "adir/bdirlink/fileb"}) @pytest.mark.skipif(sys.platform == "win32", reason="symlinks to dir not supported") def test_symlink_dir_out_of_git(inwd: WorkDir) -> None: (inwd.cwd / "adir" / "outsidedirlink").symlink_to(os.path.join(__file__, "..")) inwd.add_and_commit() assert set(find_files("adir")) == _sep({"adir/filea"}) @pytest.mark.skipif( sys.platform == "win32", reason="symlinks to files not supported on windows" ) def test_symlink_file_out_of_git(inwd: WorkDir) -> None: (inwd.cwd / "adir" / "outsidefilelink").symlink_to(__file__) inwd.add_and_commit() assert set(find_files("adir")) == _sep({"adir/filea"}) @pytest.mark.parametrize("path_add", ["{cwd}", "{cwd}" + os.pathsep + "broken"]) def test_ignore_root( inwd: WorkDir, monkeypatch: pytest.MonkeyPatch, path_add: str ) -> None: monkeypatch.setenv("SETUPTOOLS_SCM_IGNORE_VCS_ROOTS", path_add.format(cwd=inwd.cwd)) assert find_files() == [] def test_empty_root(inwd: WorkDir) -> None: subdir = inwd.cwd / "cdir" / "subdir" subdir.mkdir(parents=True) (subdir / "filec").touch() inwd.add_and_commit() assert set(find_files("cdir")) == _sep({"cdir/subdir/filec"}) def test_empty_subdir(inwd: WorkDir) -> None: subdir = inwd.cwd / "adir" / "emptysubdir" / "subdir" subdir.mkdir(parents=True) (subdir / "xfile").touch() inwd.add_and_commit() assert set(find_files("adir")) == _sep( {"adir/filea", "adir/emptysubdir/subdir/xfile"} ) @pytest.mark.skipif(sys.platform == "win32", reason="symlinks not supported on windows") def test_double_include_through_symlink(inwd: WorkDir) -> None: (inwd.cwd / "data").mkdir() (inwd.cwd / "data" / "datafile").touch() (inwd.cwd / "adir" / "datalink").symlink_to("../data") (inwd.cwd / "adir" / "filealink").symlink_to("filea") inwd.add_and_commit() assert set(find_files()) == _sep( { "file1", "adir/datalink", # -> ../data "adir/filealink", # -> filea "adir/filea", "bdir/fileb", "data/datafile", } ) @pytest.mark.skipif(sys.platform == "win32", reason="symlinks not supported on windows") def test_symlink_not_in_scm_while_target_is(inwd: WorkDir) -> None: (inwd.cwd / "data").mkdir() (inwd.cwd / "data" / "datafile").touch() inwd.add_and_commit() (inwd.cwd / "adir" / "datalink").symlink_to("../data") (inwd.cwd / "adir" / "filealink").symlink_to("filea") assert set(find_files()) == _sep( { "file1", "adir/filea", # adir/datalink and adir/afilelink not included # because the symlink_to themselves are not in scm "bdir/fileb", "data/datafile", } ) @pytest.mark.issue(587) @pytest.mark.skip_commit def test_not_commited(inwd: WorkDir) -> None: assert find_files() == [] def test_unexpanded_git_archival(wd: WorkDir, monkeypatch: pytest.MonkeyPatch) -> None: # When substitutions in `.git_archival.txt` are not expanded, files should # not be automatically listed. monkeypatch.chdir(wd.cwd) (wd.cwd / ".git_archival.txt").write_text("node: $Format:%H$", encoding="utf-8") (wd.cwd / "file1.txt").touch() assert find_files() == [] @pytest.mark.parametrize("archive_file", [".git_archival.txt", ".hg_archival.txt"]) def test_archive( wd: WorkDir, monkeypatch: pytest.MonkeyPatch, archive_file: str ) -> None: # When substitutions in `.git_archival.txt` are not expanded, files should # not be automatically listed. monkeypatch.chdir(wd.cwd) sha = "a1bda3d984d1a40d7b00ae1d0869354d6d503001" (wd.cwd / archive_file).write_text(f"node: {sha}", encoding="utf-8") (wd.cwd / "data").mkdir() (wd.cwd / "data" / "datafile").touch() datalink = wd.cwd / "data" / "datalink" if sys.platform != "win32": datalink.symlink_to("data/datafile") else: os.link("data/datafile", datalink) assert set(find_files()) == _sep({archive_file, "data/datafile", "data/datalink"}) setuptools-scm-8.2.1/testing/test_functions.py000066400000000000000000000141751476647667300216630ustar00rootroot00000000000000from __future__ import annotations import shutil import subprocess from pathlib import Path 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 c = Configuration() @pytest.mark.parametrize( ("tag", "expected"), [ ("1.1", "1.2"), ("1.2.dev", "1.2"), ("1.1a2", "1.1a3"), pytest.param( "23.24.post2+deadbeef", "23.24.post3", marks=pytest.mark.filterwarnings( "ignore:.*will be stripped of its suffix.*:UserWarning" ), ), ], ) def test_next_tag(tag: str, expected: str) -> None: version = meta(tag, config=c) assert guess_next_version(version) == expected VERSIONS = { "exact": meta("1.1", distance=0, dirty=False, config=c), "dirty": meta("1.1", distance=0, dirty=True, config=c), "distance-clean": meta("1.1", distance=3, dirty=False, config=c), "distance-dirty": meta("1.1", distance=3, dirty=True, config=c), } @pytest.mark.parametrize( ("version", "version_scheme", "local_scheme", "expected"), [ ("exact", "guess-next-dev", "node-and-date", "1.1"), ("dirty", "guess-next-dev", "node-and-date", "1.2.dev0+d20090213"), ("dirty", "guess-next-dev", "no-local-version", "1.2.dev0"), ("distance-clean", "guess-next-dev", "node-and-date", "1.2.dev3"), ("distance-dirty", "guess-next-dev", "node-and-date", "1.2.dev3+d20090213"), ("exact", "post-release", "node-and-date", "1.1"), ("dirty", "post-release", "node-and-date", "1.1.post0+d20090213"), ("distance-clean", "post-release", "node-and-date", "1.1.post3"), ("distance-dirty", "post-release", "node-and-date", "1.1.post3+d20090213"), ], ) def test_format_version( version: str, version_scheme: str, local_scheme: str, expected: str ) -> None: from dataclasses import replace scm_version = VERSIONS[version] configured_version = replace( scm_version, config=replace( scm_version.config, version_scheme=version_scheme, local_scheme=local_scheme ), ) assert format_version(configured_version) == expected 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 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_has_command() -> None: with pytest.warns(RuntimeWarning, match="yadayada"): assert not has_command("yadayada_setuptools_aint_ne") def test_has_command_logs_stderr(caplog: pytest.LogCaptureFixture) -> None: """ If the name provided to has_command() exists as a command, but gives a non-zero return code, there should be a log message generated. """ with pytest.warns(RuntimeWarning, match="ls"): has_command("ls", ["--a-flag-that-doesnt-exist-should-give-output-on-stderr"]) found_it = False for record in caplog.records: 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 setuptools-scm-8.2.1/testing/test_git.py000066400000000000000000000447611476647667300204420ustar00rootroot00000000000000from __future__ import annotations import contextlib import os import shutil import subprocess import sys from datetime import date from datetime import datetime from datetime import 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 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 from .conftest import DebugMode from .wd_wrapper import WorkDir pytestmark = pytest.mark.skipif( not has_command("git", warn=False), reason="git executable not found" ) @pytest.fixture(name="wd") def wd(wd: WorkDir, monkeypatch: pytest.MonkeyPatch, debug_mode: DebugMode) -> WorkDir: debug_mode.disable() monkeypatch.delenv("HOME", raising=False) wd("git init") wd("git config user.email test@example.com") wd('git config user.name "a test"') wd.add_command = "git add ." wd.commit_command = "git commit -m test-{reason}" debug_mode.enable() return wd @pytest.mark.parametrize( ("given", "tag", "number", "node", "dirty"), [ ("3.3.1-rc26-0-g9df187b", "3.3.1-rc26", 0, "g9df187b", False), ("17.33.0-rc-17-g38c3047c0", "17.33.0-rc", 17, "g38c3047c0", False), ], ) def test_parse_describe_output( given: str, tag: str, number: int, node: str, dirty: bool ) -> None: 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") p = wd.cwd.joinpath("sub/package") p.mkdir(parents=True) p.joinpath("setup.py").write_text( """from setuptools import setup setup(use_scm_version={"root": "../..", "relative_to": __file__}) """, encoding="utf-8", ) res = run([sys.executable, "setup.py", "--version"], p) assert res.stdout == "0.1.dev0+d20090213" def test_root_search_parent_directories( wd: WorkDir, monkeypatch: pytest.MonkeyPatch ) -> None: monkeypatch.delenv("SETUPTOOLS_SCM_DEBUG") p = wd.cwd.joinpath("sub/package") p.mkdir(parents=True) p.joinpath("setup.py").write_text( """from setuptools import setup setup(use_scm_version={"search_parent_directories": True}) """, encoding="utf-8", ) res = run([sys.executable, "setup.py", "--version"], p) assert res.stdout == "0.1.dev0+d20090213" 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]") with pytest.raises(CommandNotFoundError, match=r"git"): git.parse(wd.cwd, Configuration(), git.DEFAULT_DESCRIBE) assert wd.get_version(fallback_version="1.0") == "1.0" @pytest.mark.issue("https://github.com/pypa/setuptools-scm/issues/298") @pytest.mark.issue(403) def test_file_finder_no_history(wd: WorkDir, caplog: pytest.LogCaptureFixture) -> None: file_list = git_find_files(str(wd.cwd)) assert file_list == [] assert "listing git files failed - pretending there aren't any" in caplog.text @pytest.mark.issue("https://github.com/pypa/setuptools-scm/issues/281") def test_parse_call_order(wd: WorkDir) -> None: git.parse(str(wd.cwd), Configuration(), git.DEFAULT_DESCRIBE) def sudo_devnull( args: list[str | os.PathLike[str]], check: bool = False ) -> subprocess.CompletedProcess[bytes]: """shortcut to run sudo with non-interactive input""" return subprocess.run( ["sudo", *args], stdin=subprocess.DEVNULL, check=check, ) @contextlib.contextmanager def break_folder_permissions(path: Path) -> Generator[None, None, None]: """break the permissions of a folder for a while""" if not shutil.which("sudo"): pytest.skip("sudo executable not found") original_stat = path.stat() proc = sudo_devnull(["chown", "-R", "12345", path]) if proc.returncode != 0: pytest.xfail("Failed to change ownership, is passwordless sudo available?") try: sudo_devnull(["chmod", "a+r", path], check=True) sudo_devnull(["chgrp", "-R", "12345", path], check=True) yield finally: # Restore the ownership sudo_devnull(["chown", "-R", str(original_stat.st_uid), path], check=True) sudo_devnull(["chgrp", "-R", str(original_stat.st_gid), path], check=True) @pytest.mark.issue("https://github.com/pypa/setuptools-scm/issues/707") def test_not_owner(wd: WorkDir) -> None: with break_folder_permissions(wd.cwd): assert git.parse(str(wd.cwd), Configuration()) def test_version_from_git(wd: WorkDir) -> None: assert wd.get_version() == "0.1.dev0+d20090213" parsed = git.parse(str(wd.cwd), Configuration(), git.DEFAULT_DESCRIBE) assert parsed is not None assert parsed.branch in ("master", "main") wd.commit_testfile() assert wd.get_version().startswith("0.1.dev1+g") assert not wd.get_version().endswith("1-") wd("git tag v0.1") assert wd.get_version() == "0.1" wd.write("test.txt", "test2") assert wd.get_version().startswith("0.2.dev0+g") wd.commit_testfile() assert wd.get_version().startswith("0.2.dev1+g") wd("git tag version-0.2") assert wd.get_version().startswith("0.2") wd.commit_testfile() wd("git tag version-0.2.post210+gbe48adfpost3+g0cc25f2") with pytest.warns( UserWarning, match="tag '.*' will be stripped of its suffix '.*'" ): assert wd.get_version().startswith("0.2") wd.commit_testfile() wd("git tag 17.33.0-rc") assert wd.get_version() == "17.33.0rc0" # custom normalization assert wd.get_version(normalize=False) == "17.33.0-rc" assert wd.get_version(version_cls=NonNormalizedVersion) == "17.33.0-rc" assert ( wd.get_version(version_cls="setuptools_scm.NonNormalizedVersion") == "17.33.0-rc" ) setup_py_with_normalize: dict[str, str] = { "false": """ from setuptools import setup setup(use_scm_version={'normalize': False, 'write_to': 'VERSION.txt'}) """, "with_created_class": """ from setuptools import setup class MyVersion: def __init__(self, tag_str: str): self.version = tag_str def __repr__(self): return self.version setup(use_scm_version={'version_cls': MyVersion, 'write_to': 'VERSION.txt'}) """, "with_named_import": """ from setuptools import setup setup(use_scm_version={ 'version_cls': 'setuptools_scm.NonNormalizedVersion', 'write_to': 'VERSION.txt' }) """, } @pytest.mark.parametrize( "setup_py_txt", [pytest.param(text, id=key) for key, text in setup_py_with_normalize.items()], ) def test_git_version_unnormalized_setuptools( setup_py_txt: str, wd: WorkDir, monkeypatch: pytest.MonkeyPatch ) -> None: """ Test that when integrating with setuptools without normalization, the version is not normalized in write_to files, but still normalized by setuptools for the final dist metadata. """ # monkeypatch.delenv("SETUPTOOLS_SCM_DEBUG") monkeypatch.chdir(wd.cwd) wd.write("setup.py", dedent(setup_py_txt)) # do git operations and tag wd.commit_testfile() wd("git tag 17.33.0-rc1") # setuptools still normalizes using packaging.Version (removing the dash) res = wd([sys.executable, "setup.py", "--version"]) assert res == "17.33.0rc1" # but the version tag in the file is non-normalized (with the dash) assert wd.cwd.joinpath("VERSION.txt").read_text(encoding="utf-8") == "17.33.0-rc1" @pytest.mark.issue(179) def test_unicode_version_scheme(wd: WorkDir) -> None: scheme = b"guess-next-dev".decode("ascii") assert wd.get_version(version_scheme=scheme) @pytest.mark.issue(108) @pytest.mark.issue(109) def test_git_worktree(wd: WorkDir) -> None: wd.write("test.txt", "test2") # untracked files dont change the state assert wd.get_version() == "0.1.dev0+d20090213" wd("git add test.txt") assert wd.get_version().startswith("0.1.dev0+d") @pytest.mark.issue(86) @pytest.mark.parametrize("today", [False, True]) def test_git_dirty_notag( today: bool, wd: WorkDir, monkeypatch: pytest.MonkeyPatch ) -> None: if today: monkeypatch.delenv("SOURCE_DATE_EPOCH", raising=False) 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") else: tag = ".d20090213" assert version.startswith("0.1.dev1+g") assert version.endswith(tag) @pytest.mark.issue(193) @pytest.mark.xfail(reason="sometimes relative path results") def test_git_worktree_support(wd: WorkDir, tmp_path: Path) -> None: wd.commit_testfile() worktree = tmp_path / "work_tree" wd(f"git worktree add -b work-tree {worktree}") res = run([sys.executable, "-m", "setuptools_scm", "ls"], cwd=worktree) assert "test.txt" in res.stdout assert str(worktree) in res.stdout @pytest.fixture def shallow_wd(wd: WorkDir, tmp_path: Path) -> Path: wd.commit_testfile() wd.commit_testfile() wd.commit_testfile() target = tmp_path / "wd_shallow" run(["git", "clone", f"file://{wd.cwd}", target, "--depth=1"], tmp_path, check=True) return target def test_git_parse_shallow_warns( shallow_wd: Path, recwarn: pytest.WarningsRecorder ) -> None: git.parse(shallow_wd, Configuration()) print(list(recwarn)) msg = recwarn.pop() assert "is shallow and may cause errors" in str(msg.message) def test_git_parse_shallow_fail(shallow_wd: Path) -> None: with pytest.raises(ValueError, match="git fetch"): git.parse(str(shallow_wd), Configuration(), pre_parse=git.fail_on_shallow) def test_git_shallow_autocorrect( shallow_wd: Path, recwarn: pytest.WarningsRecorder ) -> None: git.parse(str(shallow_wd), Configuration(), pre_parse=git.fetch_on_shallow) msg = recwarn.pop() assert "git fetch was used to rectify" in str(msg.message) git.parse(str(shallow_wd), Configuration(), pre_parse=git.fail_on_shallow) def test_find_files_stop_at_root_git(wd: WorkDir) -> None: wd.commit_testfile() project = wd.cwd / "project" project.mkdir() project.joinpath("setup.cfg").touch() assert setuptools_scm._file_finders.find_files(str(project)) == [] @pytest.mark.issue(128) def test_parse_no_worktree(tmp_path: Path) -> None: ret = git.parse(str(tmp_path), Configuration(root=str(tmp_path))) assert ret is None def test_alphanumeric_tags_match(wd: WorkDir) -> None: wd.commit_testfile() wd("git tag newstyle-development-started") assert wd.get_version().startswith("0.1.dev1+g") def test_git_archive_export_ignore( wd: WorkDir, monkeypatch: pytest.MonkeyPatch ) -> None: wd.write("test1.txt", "test") wd.write("test2.txt", "test") wd.write( ".git/info/attributes", # Explicitly include test1.txt so that the test is not affected by # a potentially global gitattributes file on the test machine. "/test1.txt -export-ignore\n/test2.txt 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")] @pytest.mark.issue(228) def test_git_archive_subdirectory(wd: WorkDir, monkeypatch: pytest.MonkeyPatch) -> None: os.mkdir(wd.cwd / "foobar") wd.write("foobar/test1.txt", "test") wd("git add foobar") wd.commit() monkeypatch.chdir(wd.cwd) assert setuptools_scm._file_finders.find_files(".") == [ opj(".", "foobar", "test1.txt") ] @pytest.mark.issue(251) def test_git_archive_run_from_subdirectory( wd: WorkDir, monkeypatch: pytest.MonkeyPatch ) -> None: os.mkdir(wd.cwd / "foobar") wd.write("foobar/test1.txt", "test") wd("git add foobar") wd.commit() monkeypatch.chdir(wd.cwd / "foobar") assert setuptools_scm._file_finders.find_files(".") == [opj(".", "test1.txt")] @pytest.mark.issue("https://github.com/pypa/setuptools-scm/issues/728") def test_git_branch_names_correct(wd: WorkDir) -> None: wd.commit_testfile() wd("git checkout -b test/fun") wd_git = git.GitWorkdir(wd.cwd) assert wd_git.get_branch() == "test/fun" def test_git_feature_branch_increments_major(wd: WorkDir) -> None: wd.commit_testfile() wd("git tag 1.0.0") wd.commit_testfile() assert wd.get_version(version_scheme="python-simplified-semver").startswith("1.0.1") wd("git checkout -b feature/fun") wd.commit_testfile() assert wd.get_version(version_scheme="python-simplified-semver").startswith("1.1.0") @pytest.mark.issue("https://github.com/pypa/setuptools-scm/issues/303") def test_not_matching_tags(wd: WorkDir) -> None: wd.commit_testfile() wd("git tag apache-arrow-0.11.1") wd.commit_testfile() wd("git tag apache-arrow-js-0.9.9") wd.commit_testfile() assert wd.get_version( tag_regex=r"^apache-arrow-([\.0-9]+)$", git_describe_command="git describe --dirty --tags --long --exclude *js* ", ).startswith("0.11.2") @pytest.mark.issue("https://github.com/pypa/setuptools-scm/issues/411") def test_non_dotted_version(wd: WorkDir) -> None: wd.commit_testfile() wd("git tag apache-arrow-1") wd.commit_testfile() assert wd.get_version().startswith("2") def test_non_dotted_version_with_updated_regex(wd: WorkDir) -> None: wd.commit_testfile() wd("git tag apache-arrow-1") wd.commit_testfile() assert wd.get_version(tag_regex=r"^apache-arrow-([\.0-9]+)$").startswith("2") def test_non_dotted_tag_no_version_match(wd: WorkDir) -> None: wd.commit_testfile() wd("git tag apache-arrow-0.11.1") wd.commit_testfile() wd("git tag apache-arrow") wd.commit_testfile() assert wd.get_version().startswith("0.11.2.dev2") @pytest.mark.issue("https://github.com/pypa/setuptools-scm/issues/381") def test_gitdir(monkeypatch: pytest.MonkeyPatch, wd: WorkDir) -> None: """ """ wd.commit_testfile() normal = wd.get_version() # git hooks set this and break subsequent setuptools-scm unless we clean monkeypatch.setenv("GIT_DIR", __file__) assert wd.get_version() == normal def test_git_getdate(wd: WorkDir) -> None: # TODO: case coverage for git wd parse today = datetime.now(timezone.utc).date() def parse_date() -> date: parsed = git.parse(os.fspath(wd.cwd), Configuration()) assert parsed is not None assert parsed.node_date is not None return parsed.node_date git_wd = git.GitWorkdir(wd.cwd) assert git_wd.get_head_date() is None assert parse_date() == today wd.commit_testfile() assert git_wd.get_head_date() == today assert parse_date() == today def test_git_getdate_badgit( wd: WorkDir, caplog: pytest.LogCaptureFixture, monkeypatch: pytest.MonkeyPatch ) -> None: wd.commit_testfile() git_wd = git.GitWorkdir(wd.cwd) fake_date_result = CompletedProcess(args=[], stdout="%cI", stderr="", returncode=0) with patch.object( git, "run_git", Mock(return_value=fake_date_result), ): assert git_wd.get_head_date() is None def test_git_getdate_git_2_45_0_plus( wd: WorkDir, caplog: pytest.LogCaptureFixture, monkeypatch: pytest.MonkeyPatch ) -> None: wd.commit_testfile() git_wd = git.GitWorkdir(wd.cwd) fake_date_result = CompletedProcess( args=[], stdout="2024-04-30T22:33:10Z", stderr="", returncode=0 ) with patch.object( git, "run_git", Mock(return_value=fake_date_result), ): assert git_wd.get_head_date() == date(2024, 4, 30) @pytest.fixture def signed_commit_wd(monkeypatch: pytest.MonkeyPatch, wd: WorkDir) -> WorkDir: if not has_command("gpg", args=["--version"], warn=False): pytest.skip("gpg executable not found") wd.write( ".gpg_batch_params", """\ %no-protection %transient-key Key-Type: RSA Key-Length: 2048 Name-Real: a test Name-Email: test@example.com Expire-Date: 0 """, ) monkeypatch.setenv("GNUPGHOME", str(wd.cwd.resolve(strict=True))) wd("gpg --batch --generate-key .gpg_batch_params") wd("git config log.showSignature true") wd.signed_commit_command = "git commit -S -m test-{reason}" return wd @pytest.mark.issue("https://github.com/pypa/setuptools-scm/issues/548") def test_git_getdate_signed_commit(signed_commit_wd: WorkDir) -> None: today = datetime.now(timezone.utc).date() signed_commit_wd.commit_testfile(signed=True) git_wd = git.GitWorkdir(signed_commit_wd.cwd) assert git_wd.get_head_date() == today @pytest.mark.parametrize( ("expected", "from_data"), [ ( "1.0", {"describe-name": "1.0-0-g0000"}, ), ( "1.1.dev3+g0000", { "describe-name": "1.0-3-g0000", "node": "0" * 20, }, ), ("0.0", {"node": "0" * 20}), ("1.2.2", {"describe-name": "release-1.2.2-0-g00000"}), ("1.2.2.dev0", {"ref-names": "tag: release-1.2.2.dev"}), ("1.2.2", {"describe-name": "v1.2.2"}), ], ) @pytest.mark.filterwarnings("ignore:git archive did not support describe output") def test_git_archival_to_version(expected: str, from_data: dict[str, str]) -> None: config = Configuration( version_scheme="guess-next-dev", local_scheme="node-and-date" ) version = archival_to_version(from_data, config=config) assert version is not None assert format_version(version) == expected @pytest.mark.issue("https://github.com/pypa/setuptools-scm/issues/727") def test_git_archival_node_missing_no_version() -> None: config = Configuration() version = archival_to_version({}, config=config) assert version is None def test_git_archival_from_unfiltered() -> None: config = Configuration() with pytest.warns( UserWarning, match=r"unprocessed git archival found \(no export subst applied\)" ): version = archival_to_version({"node": "$Format:%H$"}, config=config) assert version is None setuptools-scm-8.2.1/testing/test_hg_git.py000066400000000000000000000045451476647667300211140ustar00rootroot00000000000000from __future__ import annotations import pytest from setuptools_scm._run_cmd import has_command from setuptools_scm._run_cmd import run from testing.wd_wrapper import WorkDir @pytest.fixture(scope="module", autouse=True) def _check_hg_git() -> None: if not has_command("hg", warn=False): pytest.skip("hg executable not found") res = run("hg debuginstall --template {pythonexe}", cwd=".") if res.returncode: skip_no_hggit = True else: res = run([res.stdout, "-c", "import hggit"], cwd=".") skip_no_hggit = bool(res.returncode) if skip_no_hggit: pytest.skip("hg-git not installed") def test_base(repositories_hg_git: tuple[WorkDir, WorkDir]) -> None: wd, wd_git = repositories_hg_git assert wd_git.get_version() == "0.1.dev0+d20090213" assert wd.get_version() == "0.1.dev0+d20090213" wd_git.commit_testfile() version_git = wd_git.get_version() wd("hg pull -u") version = wd.get_version() assert version_git.startswith("0.1.dev1+g") assert version.startswith("0.1.dev1+g") assert not version_git.endswith("1-") assert not version.endswith("1-") wd_git("git tag v0.1") wd("hg pull -u") assert wd_git.get_version() == "0.1" assert wd.get_version() == "0.1" wd_git.write("test.txt", "test2") wd.write("test.txt", "test2") assert wd_git.get_version().startswith("0.2.dev0+g") assert wd.get_version().startswith("0.2.dev0+g") wd_git.commit_testfile() wd("hg pull") wd("hg up -C") assert wd_git.get_version().startswith("0.2.dev1+g") assert wd.get_version().startswith("0.2.dev1+g") wd_git("git tag version-0.2") wd("hg pull -u") assert wd_git.get_version().startswith("0.2") assert wd.get_version().startswith("0.2") wd_git.commit_testfile() wd_git("git tag version-0.2.post210+gbe48adfpost3+g0cc25f2") wd("hg pull -u") with pytest.warns( UserWarning, match="tag '.*' will be stripped of its suffix '.*'" ): assert wd_git.get_version().startswith("0.2") with pytest.warns( UserWarning, match="tag '.*' will be stripped of its suffix '.*'" ): assert wd.get_version().startswith("0.2") wd_git.commit_testfile() wd_git("git tag 17.33.0-rc") wd("hg pull -u") assert wd_git.get_version() == "17.33.0rc0" assert wd.get_version() == "17.33.0rc0" setuptools-scm-8.2.1/testing/test_integration.py000066400000000000000000000175511476647667300221770ustar00rootroot00000000000000from __future__ import annotations import importlib.metadata import os import subprocess import sys import textwrap from pathlib import Path import pytest import setuptools_scm._integration.setuptools 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() @pytest.fixture def wd(wd: WorkDir) -> WorkDir: wd("git init") wd("git config user.email test@example.com") wd('git config user.name "a test"') wd.add_command = "git add ." wd.commit_command = "git commit -m test-{reason}" return wd def test_pyproject_support(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: if sys.version_info < (3, 11): pytest.importorskip("tomli") monkeypatch.delenv("SETUPTOOLS_SCM_DEBUG") pkg = tmp_path / "package" pkg.mkdir() pkg.joinpath("pyproject.toml").write_text( textwrap.dedent( """ [tool.setuptools_scm] fallback_version = "12.34" [project] name = "foo" description = "Factory ⸻ A code generator 🏭" authors = [{name = "Łukasz Langa"}] dynamic = ["version"] """ ), encoding="utf-8", ) pkg.joinpath("setup.py").write_text( "__import__('setuptools').setup()", encoding="utf-8" ) res = run([sys.executable, "setup.py", "--version"], pkg) assert res.stdout == "12.34" PYPROJECT_FILES = { "setup.py": "[tool.setuptools_scm]", "setup.cfg": "[tool.setuptools_scm]", "pyproject tool.setuptools_scm": ( "[tool.setuptools_scm]\ndist_name='setuptools_scm_example'" ), "pyproject.project": ( "[project]\nname='setuptools_scm_example'\n" "dynamic=['version']\n[tool.setuptools_scm]" ), } SETUP_PY_PLAIN = "__import__('setuptools').setup()" SETUP_PY_WITH_NAME = "__import__('setuptools').setup(name='setuptools_scm_example')" SETUP_PY_FILES = { "setup.py": SETUP_PY_WITH_NAME, "setup.cfg": SETUP_PY_PLAIN, "pyproject tool.setuptools_scm": SETUP_PY_PLAIN, "pyproject.project": SETUP_PY_PLAIN, } SETUP_CFG_FILES = { "setup.py": "", "setup.cfg": "[metadata]\nname=setuptools_scm_example", "pyproject tool.setuptools_scm": "", "pyproject.project": "", } with_metadata_in = pytest.mark.parametrize( "metadata_in", ["setup.py", "setup.cfg", "pyproject tool.setuptools_scm", "pyproject.project"], ) @with_metadata_in def test_pyproject_support_with_git(wd: WorkDir, metadata_in: str) -> None: if sys.version_info < (3, 11): pytest.importorskip("tomli") wd.write("pyproject.toml", PYPROJECT_FILES[metadata_in]) wd.write("setup.py", SETUP_PY_FILES[metadata_in]) wd.write("setup.cfg", SETUP_CFG_FILES[metadata_in]) res = wd([sys.executable, "setup.py", "--version"]) assert res.endswith("0.1.dev0+d20090213") @pytest.mark.parametrize("use_scm_version", ["True", "{}", "lambda: {}"]) def test_pyproject_missing_setup_hook_works(wd: WorkDir, use_scm_version: str) -> None: wd.write( "setup.py", f"""__import__('setuptools').setup( name="example-scm-unique", use_scm_version={use_scm_version}, )""", ) wd.write( "pyproject.toml", textwrap.dedent( """ [build-system] requires=["setuptools", "setuptools_scm"] build-backend = "setuptools.build_meta" [tool] """ ), ) res = subprocess.run( [sys.executable, "setup.py", "--version"], cwd=wd.cwd, check=True, stdout=subprocess.PIPE, encoding="utf-8", ) stripped = res.stdout.strip() assert stripped.endswith("0.1.dev0+d20090213") res_build = subprocess.run( [sys.executable, "-m", "build", "-nxw"], env={k: v for k, v in os.environ.items() if k != "SETUPTOOLS_SCM_DEBUG"}, cwd=wd.cwd, ) import pprint pprint.pprint(res_build) wheel: Path = next(wd.cwd.joinpath("dist").iterdir()) assert "0.1.dev0+d20090213" in str(wheel) def test_pretend_version(monkeypatch: pytest.MonkeyPatch, wd: WorkDir) -> None: monkeypatch.setenv(PRETEND_KEY, "1.0.0") assert wd.get_version() == "1.0.0" assert wd.get_version(dist_name="ignored") == "1.0.0" @with_metadata_in def test_pretend_version_named_pyproject_integration( monkeypatch: pytest.MonkeyPatch, wd: WorkDir, metadata_in: str ) -> None: test_pyproject_support_with_git(wd, metadata_in) monkeypatch.setenv( PRETEND_KEY_NAMED.format(name="setuptools_scm_example".upper()), "3.2.1" ) res = wd([sys.executable, "setup.py", "--version"]) assert res.endswith("3.2.1") def test_pretend_version_named(monkeypatch: pytest.MonkeyPatch, wd: WorkDir) -> None: monkeypatch.setenv(PRETEND_KEY_NAMED.format(name="test".upper()), "1.0.0") monkeypatch.setenv(PRETEND_KEY_NAMED.format(name="test2".upper()), "2.0.0") assert wd.get_version(dist_name="test") == "1.0.0" assert wd.get_version(dist_name="test2") == "2.0.0" def test_pretend_version_name_takes_precedence( monkeypatch: pytest.MonkeyPatch, wd: WorkDir ) -> None: monkeypatch.setenv(PRETEND_KEY_NAMED.format(name="test".upper()), "1.0.0") monkeypatch.setenv(PRETEND_KEY, "2.0.0") assert wd.get_version(dist_name="test") == "1.0.0" def test_pretend_version_accepts_bad_string( monkeypatch: pytest.MonkeyPatch, wd: WorkDir ) -> None: monkeypatch.setenv(PRETEND_KEY, "dummy") wd.write("setup.py", SETUP_PY_PLAIN) assert wd.get_version(write_to="test.py") == "dummy" pyver = wd([sys.executable, "setup.py", "--version"]) assert pyver == "0.0.0" def testwarn_on_broken_setuptools() -> None: _warn_on_old_setuptools("61") with pytest.warns(RuntimeWarning, match="ERROR: setuptools==60"): _warn_on_old_setuptools("60") @pytest.mark.issue(611) def test_distribution_provides_extras() -> None: from importlib.metadata import distribution dist = distribution("setuptools_scm") pe: list[str] = dist.metadata.get_all("Provides-Extra", []) assert sorted(pe) == ["docs", "rich", "test", "toml"] @pytest.mark.issue(760) def test_unicode_in_setup_cfg(tmp_path: Path) -> None: cfg = tmp_path / "setup.cfg" cfg.write_text( textwrap.dedent( """ [metadata] name = configparser author = Łukasz Langa """ ), encoding="utf-8", ) name = setuptools_scm._integration.setuptools.read_dist_name_from_setup_cfg(cfg) assert name == "configparser" def test_setuptools_version_keyword_ensures_regex( wd: WorkDir, monkeypatch: pytest.MonkeyPatch, ) -> None: wd.commit_testfile("test") wd("git tag 1.0") monkeypatch.chdir(wd.cwd) import setuptools from setuptools_scm._integration.setuptools import version_keyword dist = setuptools.Distribution({"name": "test"}) version_keyword(dist, "use_scm_version", {"tag_regex": "(1.0)"}) @pytest.mark.parametrize( "ep_name", ["setuptools_scm.parse_scm", "setuptools_scm.parse_scm_fallback"] ) def test_git_archival_plugin_ignored(tmp_path: Path, ep_name: str) -> None: tmp_path.joinpath(".git_archival.txt").write_text("broken", encoding="utf-8") try: dist = importlib.metadata.distribution("setuptools_scm_git_archive") except importlib.metadata.PackageNotFoundError: pytest.skip("setuptools_scm_git_archive not installed") else: print(dist.metadata["Name"], dist.version) from setuptools_scm.discover import iter_matching_entrypoints found = list(iter_matching_entrypoints(tmp_path, config=c, entrypoint=ep_name)) imports = [item.value for item in found] assert "setuptools_scm_git_archive:parse" not in imports setuptools-scm-8.2.1/testing/test_internal_log_level.py000066400000000000000000000007371476647667300235160ustar00rootroot00000000000000from __future__ import annotations import logging from setuptools_scm import _log def test_log_levels_when_set() -> 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 setuptools-scm-8.2.1/testing/test_main.py000066400000000000000000000032011476647667300205630ustar00rootroot00000000000000from __future__ import annotations import sys import textwrap from pathlib import Path import pytest from .wd_wrapper import WorkDir def test_main() -> None: mainfile = Path(__file__).parent.parent.joinpath( "src", "setuptools_scm", "__main__.py" ) ns = {"__package__": "setuptools_scm"} code = compile(mainfile.read_text(encoding="utf-8"), "__main__.py", "exec") exec(code, ns) @pytest.fixture def repo(wd: WorkDir) -> WorkDir: wd("git init") wd("git config user.email user@host") wd("git config user.name user") wd.add_command = "git add ." wd.commit_command = "git commit -m test-{reason}" wd.write("README.rst", "My example") wd.add_and_commit() wd("git tag v0.1.0") wd.write("file.txt", "file.txt") wd.add_and_commit() return wd def test_repo_with_config(repo: WorkDir) -> None: pyproject = """\ [tool.setuptools_scm] version_scheme = "no-guess-dev" [project] name = "example" """ repo.write("pyproject.toml", textwrap.dedent(pyproject)) repo.add_and_commit() res = repo([sys.executable, "-m", "setuptools_scm"]) assert res.startswith("0.1.0.post1.dev2") def test_repo_without_config(repo: WorkDir) -> None: res = repo([sys.executable, "-m", "setuptools_scm"]) assert res.startswith("0.1.1.dev1") def test_repo_with_pyproject_missing_setuptools_scm(repo: WorkDir) -> None: pyproject = """\ [project] name = "example" """ repo.write("pyproject.toml", textwrap.dedent(pyproject)) repo.add_and_commit() res = repo([sys.executable, "-m", "setuptools_scm"]) assert res.startswith("0.1.1.dev2") setuptools-scm-8.2.1/testing/test_mercurial.py000066400000000000000000000141331476647667300216300ustar00rootroot00000000000000from __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._run_cmd import has_command 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 pytestmark = pytest.mark.skipif( not has_command("hg", warn=False), reason="hg executable not found" ) @pytest.fixture def wd(wd: WorkDir) -> WorkDir: wd("hg init") wd.add_command = "hg add ." wd.commit_command = 'hg commit -m test-{reason} -u test -d "0 0"' return wd archival_mapping = { "1.0": {"tag": "1.0"}, "1.1.0.dev3+h000000000000": { "latesttag": "1.0", "latesttagdistance": "3", "node": "0" * 20, }, "1.0.1.dev3+h000000000000": { "latesttag": "1.0.0", "latesttagdistance": "3", "branch": "1.0", "node": "0" * 20, }, "0.0": {"node": "0" * 20}, "1.2.2": {"tag": "release-1.2.2"}, "1.2.2.dev0": {"tag": "release-1.2.2.dev"}, } @pytest.mark.parametrize(("expected", "data"), sorted(archival_mapping.items())) def test_archival_to_version(expected: str, data: dict[str, str]) -> None: config = Configuration( version_scheme="release-branch-semver", local_scheme="node-and-date" ) version = archival_to_version(data, config=config) assert format_version(version) == expected 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]") with pytest.raises(CommandNotFoundError, match=r"hg"): parse(wd.cwd, config=config) assert wd.get_version(fallback_version="1.0") == "1.0" def test_find_files_stop_at_root_hg( wd: WorkDir, monkeypatch: pytest.MonkeyPatch ) -> None: wd.commit_testfile() project = wd.cwd / "project" project.mkdir() project.joinpath("setup.cfg").touch() # setup.cfg has not been committed assert setuptools_scm._file_finders.find_files(str(project)) == [] # issue 251 wd.add_and_commit() monkeypatch.chdir(project) assert setuptools_scm._file_finders.find_files() == ["setup.cfg"] # XXX: better tests for tag prefixes def test_version_from_hg_id(wd: WorkDir) -> None: assert wd.get_version() == "0.0" wd.commit_testfile() assert wd.get_version().startswith("0.1.dev1+") # tagging commit is considered the tag wd('hg tag v0.1 -u test -d "0 0"') assert wd.get_version() == "0.1" wd.commit_testfile() assert wd.get_version().startswith("0.2.dev2") wd("hg up v0.1") assert wd.get_version() == "0.1" # commit originating from the tagged revision # that is not an actual tag wd.commit_testfile() assert wd.get_version().startswith("0.2.dev1+") # several tags wd("hg up") wd('hg tag v0.2 -u test -d "0 0"') wd('hg tag v0.3 -u test -d "0 0" -r v0.2') assert wd.get_version() == "0.3" def test_version_from_archival(wd: WorkDir) -> None: # entrypoints are unordered, # cleaning the wd ensure this test won't break randomly wd.cwd.joinpath(".hg").rename(wd.cwd / ".nothg") wd.write( ".hg_archival.txt", """\ node: 000000000000 tag: 0.1 """, ) assert wd.get_version() == "0.1" wd.write( ".hg_archival.txt", """\ node: 000000000000 latesttag: 0.1 latesttagdistance: 3 """, ) assert wd.get_version() == "0.2.dev3+h000000000000" @pytest.mark.issue("#72") def test_version_in_merge(wd: WorkDir) -> None: wd.commit_testfile() wd.commit_testfile() wd("hg up 0") wd.commit_testfile() wd("hg merge --tool :merge") assert wd.get_version() is not None @pytest.mark.issue(128) def test_parse_no_worktree(tmp_path: Path) -> None: config = Configuration() ret = parse(os.fspath(tmp_path), config) assert ret is None @pytest.fixture def version_1_0(wd: WorkDir) -> WorkDir: wd("hg branch default") wd.commit_testfile() wd('hg tag 1.0.0 -u test -d "0 0"') return wd @pytest.fixture def pre_merge_commit_after_tag(version_1_0: WorkDir) -> WorkDir: wd = version_1_0 wd("hg branch testbranch") wd.write("branchfile", "branchtext") wd(wd.add_command) wd.commit() wd("hg update default") wd("hg merge testbranch") return wd @pytest.mark.usefixtures("pre_merge_commit_after_tag") def test_version_bump_before_merge_commit(wd: WorkDir) -> None: assert wd.get_version().startswith("1.0.1.dev1+") @pytest.mark.issue(219) @pytest.mark.usefixtures("pre_merge_commit_after_tag") def test_version_bump_from_merge_commit(wd: WorkDir) -> None: wd.commit() assert wd.get_version().startswith("1.0.1.dev3+") # issue 219 @pytest.mark.usefixtures("version_1_0") def test_version_bump_from_commit_including_hgtag_mods(wd: WorkDir) -> None: """Test the case where a commit includes changes to .hgtags and other files""" with wd.cwd.joinpath(".hgtags").open("ab") as tagfile: tagfile.write(b"0 0\n") wd.write("branchfile", "branchtext") wd(wd.add_command) assert wd.get_version().startswith("1.0.1.dev1+") # bump from dirty version wd.commit() # commits both the testfile _and_ .hgtags assert wd.get_version().startswith("1.0.1.dev2+") @pytest.mark.issue(229) @pytest.mark.usefixtures("version_1_0") def test_latest_tag_detection(wd: WorkDir) -> None: """Tests that tags not containing a "." are ignored, the same as for git. Note that will be superseded by the fix for pypa/setuptools-scm/issues/235 """ wd('hg tag some-random-tag -u test -d "0 0"') assert wd.get_version() == "1.0.0" @pytest.mark.usefixtures("version_1_0") def test_feature_branch_increments_major(wd: WorkDir) -> None: wd.commit_testfile() assert wd.get_version(version_scheme="python-simplified-semver").startswith("1.0.1") wd("hg branch feature/fun") assert wd.get_version(version_scheme="python-simplified-semver").startswith("1.1.0") setuptools-scm-8.2.1/testing/test_regressions.py000066400000000000000000000106371476647667300222150ustar00rootroot00000000000000from __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 setuptools_scm.integration import data_from_mime from setuptools_scm.version import meta def test_data_from_mime_ignores_body() -> None: assert data_from_mime( "test", "version: 1.0\r\n\r\nversion: bad", ) == {"version": "1.0"} def test_pkginfo_noscmroot(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: """if we are indeed a sdist, the root does not apply""" monkeypatch.delenv("SETUPTOOLS_SCM_DEBUG") # we should get the version from pkg-info if git is broken p = tmp_path.joinpath("sub/package") p.mkdir(parents=True) tmp_path.joinpath(".git").mkdir() p.joinpath("setup.py").write_text( """\ from setuptools import setup setup(use_scm_version={"root": ".."}) """, encoding="utf-8", ) res = run([sys.executable, "setup.py", "--version"], p) assert "setuptools-scm was unable to detect version for" in res.stderr assert res.returncode == 1 p.joinpath("PKG-INFO").write_text("Version: 1.0", encoding="utf-8") res = run([sys.executable, "setup.py", "--version"], p) assert res.stdout == "1.0" try: run("git init", p.parent) except OSError: pass else: res = run([sys.executable, "setup.py", "--version"], p) assert res.stdout == "0.1.dev0+d20090213" @pytest.mark.issue(164) def test_pip_download(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.chdir(tmp_path) subprocess.check_call([sys.executable, "-m", "pip", "download", "lz4==0.9.0"]) def test_use_scm_version_callable( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: """use of callable as use_scm_version argument""" monkeypatch.delenv("SETUPTOOLS_SCM_DEBUG") p = tmp_path / "sub" / "package" p.mkdir(parents=True) p.joinpath("setup.py").write_text( """from setuptools import setup def vcfg(): from setuptools_scm.version import guess_next_dev_version def vs(v): return guess_next_dev_version(v) return {"version_scheme": vs} setup(use_scm_version=vcfg) """, encoding="utf-8", ) p.joinpath("PKG-INFO").write_text("Version: 1.0", encoding="utf-8") res = run([sys.executable, "setup.py", "--version"], p) 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 def test_entrypoints_load() -> None: d = distribution("setuptools-scm") eps = d.entry_points failed: list[tuple[EntryPoint, Exception]] = [] for ep in eps: try: ep.load() except Exception as e: 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 setuptools-scm-8.2.1/testing/test_version.py000066400000000000000000000323211476647667300213310ustar00rootroot00000000000000from __future__ import annotations from dataclasses import replace from datetime import date from datetime import timedelta 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 c = Configuration() c_non_normalize = Configuration(version_cls=NonNormalizedVersion) @pytest.mark.parametrize( ("version", "expected_next"), [ pytest.param(meta("1.0.0", config=c), "1.0.0", id="exact"), pytest.param(meta("1.0", config=c), "1.0.0", id="short_tag"), pytest.param( meta("1.0.0", distance=2, branch="default", config=c), "1.0.1.dev2", id="normal_branch", ), pytest.param( meta("1.0", distance=2, branch="default", config=c), "1.0.1.dev2", id="normal_branch_short_tag", ), pytest.param( meta("1.0.0", distance=2, branch="feature", config=c), "1.1.0.dev2", id="feature_branch", ), pytest.param( meta("1.0", distance=2, branch="feature", config=c), "1.1.0.dev2", id="feature_branch_short_tag", ), pytest.param( meta("1.0.0", distance=2, branch="features/test", config=c), "1.1.0.dev2", id="feature_in_branch", ), pytest.param( meta(NonNormalizedVersion("v1.0"), distance=2, branch="default", config=c), "1.0.1.dev2", id="non-normalized-allowed", ), ], ) def test_next_semver(version: ScmVersion, expected_next: str) -> None: computed = simplified_semver_version(version) assert computed == expected_next def test_next_semver_bad_tag() -> None: version = meta("1.0.0-foo", 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"), [ pytest.param(meta("1.0.0", config=c), "1.0.0", id="exact"), pytest.param( meta("1.0.0", distance=2, branch="master", config=c), "1.1.0.dev2", id="development_branch", ), pytest.param( meta("1.0.0rc1", distance=2, branch="master", config=c), "1.1.0.dev2", id="development_branch_release_candidate", ), pytest.param( meta("1.0.0", distance=2, branch="maintenance/1.0.x", config=c), "1.0.1.dev2", id="release_branch_legacy_version", ), pytest.param( meta("1.0.0", distance=2, branch="v1.0.x", config=c), "1.0.1.dev2", id="release_branch_with_v_prefix", ), pytest.param( meta("1.0.0", distance=2, branch="release-1.0", config=c), "1.0.1.dev2", id="release_branch_with_prefix", ), pytest.param( meta("1.0.0", distance=2, branch="bugfix/3434", config=c), "1.1.0.dev2", id="false_positive_release_branch", ), ], ) def test_next_release_branch_semver(version: ScmVersion, expected_next: str) -> None: computed = release_branch_semver_version(version) assert computed == expected_next def m(tag: str, **kw: Any) -> ScmVersion: return meta(tag, **kw, config=c) @pytest.mark.parametrize( ("version", "expected_next"), [ pytest.param( m("1.0.0", distance=2), "1.0.0.post1.dev2", id="dev_distance", ), pytest.param( m("1.0.dev0", distance=2), "1.0.dev2", id="dev_distance_after_dev_tag" ), pytest.param( m("1.0", distance=2), "1.0.post1.dev2", id="dev_distance_short_tag", ), pytest.param( m("1.0.0"), "1.0.0", id="no_dev_distance", ), ], ) def test_no_guess_version(version: ScmVersion, expected_next: str) -> None: computed = no_guess_dev_version(version) assert computed == expected_next @pytest.mark.parametrize( ("version", "match"), [ ("1.0.dev1", "choosing custom numbers for the `.devX` distance"), ("1.0.post1", "already is a post release"), ], ) def test_no_guess_version_bad(version: str, match: str) -> None: with pytest.raises(ValueError, match=match): no_guess_dev_version(m(version, distance=1)) def test_bump_dev_version_zero() -> None: assert guess_next_version(m("1.0.dev0")) == "1.0" def test_bump_dev_version_nonzero_raises() -> None: match = ( "choosing custom numbers for the `.devX` distance " "is not supported.\n " "The 1.0.dev1 can't be bumped\n" "Please drop the tag or create a new supported one ending in .dev0" ) with pytest.raises(ValueError, match=match): guess_next_version(m("1.0.dev1")) @pytest.mark.parametrize( "version", [ "1.dev0", "1.0.dev456", "1.0a1", "1.0a2.dev456", "1.0a12.dev456", "1.0a12", "1.0b1.dev456", "1.0b2", "1.0b2.post345.dev456", "1.0b2.post345", "1.0rc1.dev456", "1.0rc1", "1.0", "1.0.post456.dev34", "1.0.post456", "1.0.15", "1.1.dev1", ], ) def test_only_version(version: str) -> None: assert version == only_version(meta(version, config=c)) assert version == only_version(meta(version, distance=2, config=c)) @pytest.mark.parametrize( ("tag", "expected"), [ ("v1.0.0", "1.0.0"), ("v1.0.0-rc.1", "1.0.0rc1"), ("v1.0.0-rc.1+-25259o4382757gjurh54", "1.0.0rc1"), ], ) def test_tag_regex1(tag: str, expected: str) -> None: if "+" in tag: # pytest bug wrt cardinality with pytest.warns(UserWarning): # noqa: PT030 result = meta(tag, config=c) else: result = meta(tag, config=c) assert not isinstance(result.tag, str) assert result.tag.public == expected @pytest.mark.issue("https://github.com/pypa/setuptools-scm/issues/471") def test_version_bump_bad() -> None: class YikesVersion: val: str def __init__(self, val: str) -> None: self.val = val def __str__(self) -> str: return self.val config = Configuration(version_cls=YikesVersion) # type: ignore[arg-type] with pytest.raises( ValueError, match=r".*does not end with a number to bump, " "please correct or use a custom version scheme", ): guess_next_version(tag_version=meta("2.0.0-alpha.5-PMC", config=config)) def test_format_version_schemes() -> None: version = meta( "1.0", config=replace( c, local_scheme="no-local-version", version_scheme=[ # type: ignore[arg-type] lambda v: None, "guess-next-dev", ], ), ) assert format_version(version) == "1.0" def test_custom_version_schemes() -> None: version = meta( "1.0", config=replace( c, local_scheme="no-local-version", version_scheme="setuptools_scm.version:no_guess_dev_version", ), ) custom_computed = format_version(version) assert custom_computed == no_guess_dev_version(version) def date_offset(base_date: date | None = None, days_offset: int = 0) -> date: if base_date is None: from setuptools_scm.version import _source_epoch_or_utc_now base_date = _source_epoch_or_utc_now().date() return base_date - timedelta(days=days_offset) def date_to_str( base_date: date | None = None, days_offset: int = 0, fmt: str = "%y.%m.%d", ) -> str: return format(date_offset(base_date, days_offset), fmt) @pytest.mark.parametrize( ("version", "expected_next"), [ pytest.param( meta(date_to_str(days_offset=3), config=c_non_normalize), date_to_str(days_offset=3), id="exact", ), pytest.param( meta(date_to_str() + ".1", config=c_non_normalize), date_to_str() + ".1", id="exact patch", ), pytest.param( meta("20.01.02", config=c), "20.1.2", id="leading 0s", ), pytest.param( meta(date_to_str(days_offset=3), config=c_non_normalize, dirty=True), date_to_str() + ".0.dev0", id="dirty other day", ), pytest.param( meta(date_to_str(), config=c_non_normalize, distance=2, branch="default"), date_to_str() + ".1.dev2", id="normal branch", ), pytest.param( meta(date_to_str(fmt="%Y.%m.%d"), config=c_non_normalize), date_to_str(fmt="%Y.%m.%d"), id="4 digits year", ), pytest.param( meta( date_to_str(), config=c_non_normalize, distance=2, branch="release-2021.05.06", ), "2021.05.06", id="release branch", ), pytest.param( meta( date_to_str() + ".2", config=c_non_normalize, distance=2, branch="release-21.5.1", ), "21.5.1", id="release branch short", ), pytest.param( meta( date_to_str(days_offset=3) + ".2", config=c_non_normalize, node_date=date_offset(days_offset=2), ), date_to_str(days_offset=3) + ".2", id="node date clean", ), pytest.param( meta( date_to_str(days_offset=2) + ".2", config=c_non_normalize, distance=2, node_date=date_offset(days_offset=2), ), date_to_str(days_offset=2) + ".3.dev2", id="node date distance", ), pytest.param( meta( "1.2.0", config=c_non_normalize, distance=2, node_date=date_offset(days_offset=2), ), date_to_str(days_offset=2) + ".0.dev2", marks=pytest.mark.filterwarnings( "ignore:.*not correspond to a valid versioning date.*:UserWarning" ), id="using on old version tag", ), ], ) def test_calver_by_date(version: ScmVersion, expected_next: str) -> None: computed = calver_by_date(version) assert computed == expected_next @pytest.mark.parametrize( ("version", "expected_next"), [ pytest.param(meta("1.0.0", config=c), "1.0.0", id="SemVer exact stays"), pytest.param( meta("1.0.0", config=c_non_normalize, dirty=True), "09.02.13.1.dev0", id="SemVer dirty is replaced by date", marks=pytest.mark.filterwarnings("ignore:.*legacy version.*:UserWarning"), ), ], ) def test_calver_by_date_semver(version: ScmVersion, expected_next: str) -> None: computed = calver_by_date(version) assert computed == expected_next def test_calver_by_date_future_warning() -> None: with pytest.warns(UserWarning, match="your previous tag*"): calver_by_date( meta(date_to_str(days_offset=-2), config=c_non_normalize, distance=2) ) @pytest.mark.parametrize( ("tag", "node_date", "expected"), [ pytest.param("20.03.03", date(2020, 3, 4), "20.03.04.0", id="next day"), pytest.param("20.03.03", date(2020, 3, 3), "20.03.03.1", id="same day"), pytest.param( "20.03.03.2", date(2020, 3, 3), "20.03.03.3", id="same day with patch" ), pytest.param( "v20.03.03", date(2020, 3, 4), "v20.03.04.0", id="next day with v prefix" ), ], ) def test_calver_guess_next_data(tag: str, node_date: date, expected: str) -> None: version = meta(tag, config=c_non_normalize, node_date=node_date) next = guess_next_date_ver( version, node_date=node_date, version_cls=c_non_normalize.version_cls, ) assert next == expected def test_custom_version_cls() -> None: """Test that we can pass our own version class instead of pkg_resources""" class MyVersion: def __init__(self, tag_str: str) -> None: self.tag = tag_str def __str__(self) -> str: return f"Custom {self.tag}" def __repr__(self) -> str: return f"MyVersion" config = Configuration(version_cls=MyVersion) # type: ignore[arg-type] scm_version = meta("1.0.0-foo", config=config) assert isinstance(scm_version.tag, MyVersion) assert str(scm_version.tag) == "Custom 1.0.0-foo" setuptools-scm-8.2.1/testing/wd_wrapper.py000066400000000000000000000041321476647667300207560ustar00rootroot00000000000000from __future__ import annotations import itertools from pathlib import Path from typing import Any class WorkDir: """a simple model for a""" commit_command: str signed_commit_command: str add_command: str def __repr__(self) -> str: return f"" def __init__(self, cwd: Path) -> None: self.cwd = cwd self.__counter = itertools.count() def __call__(self, cmd: list[str] | str, **kw: object) -> str: if kw: assert isinstance(cmd, str), "formatting the command requires text input" cmd = cmd.format(**kw) from setuptools_scm._run_cmd import run return run(cmd, cwd=self.cwd).stdout def write(self, name: str, content: str | bytes) -> Path: path = self.cwd / name if isinstance(content, bytes): path.write_bytes(content) else: path.write_text(content, encoding="utf-8") return path def _reason(self, given_reason: str | None) -> str: if given_reason is None: return f"number-{next(self.__counter)}" else: return given_reason def add_and_commit( self, reason: str | None = None, signed: bool = False, **kwargs: object ) -> None: self(self.add_command) self.commit(reason=reason, signed=signed, **kwargs) def commit(self, reason: str | None = None, signed: bool = False) -> None: reason = self._reason(reason) self( self.commit_command if not signed else self.signed_commit_command, reason=reason, ) def commit_testfile(self, reason: str | None = None, signed: bool = False) -> None: reason = self._reason(reason) self.write("test.txt", f"test {reason}") self(self.add_command) self.commit(reason=reason, signed=signed) def get_version(self, **kw: Any) -> str: __tracebackhide__ = True from setuptools_scm import get_version version = get_version(root=self.cwd, fallback_root=self.cwd, **kw) print(self.cwd.name, version, sep=": ") return version setuptools-scm-8.2.1/tox.ini000066400000000000000000000011231476647667300160650ustar00rootroot00000000000000[tox] envlist=py{38,39,310,311,312,313},check_readme,check-dist requires= tox>4 [flake8] max-complexity = 10 max-line-length = 88 ignore=E203,W503 [testenv] usedevelop=True extras=test commands= python -X warn_default_encoding -m pytest {posargs} [testenv:check_readme] skip_install=True deps= check-manifest docutils pygments typing_extensions hatchling rich commands= check-manifest --no-build-isolation [testenv:check_dist] skip_install = true deps= build twine commands= python -m build twine check dist/* #XXX: envs for hg versions