--- /dev/null
+.git_archival.txt export-subst
--- /dev/null
+version: 2
+updates:
+ - package-ecosystem: "github-actions"
+ directory: "/"
+ schedule:
+ interval: "weekly"
+ - package-ecosystem: "pip"
+ directory: "/"
+ schedule:
+ interval: "weekly"
+++ /dev/null
-name: pre-commit
-
-on:
- pull_request:
- push:
- branches: [master]
-
-jobs:
- pre-commit:
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v1
- - uses: actions/setup-python@v1
- - name: set PY
- run: echo "::set-env name=PY::$(python --version --version | sha256sum | cut -d' ' -f1)"
- - uses: actions/cache@v1
- with:
- path: ~/.cache/pre-commit
- key: pre-commit|${{ env.PY }}|${{ hashFiles('.pre-commit-config.yaml') }}
- - uses: pre-commit/action@v1.0.0
pull_request:
push:
branches:
- - master
+ - "*"
tags:
- "v*"
- release:
+
+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@v1
+
test:
+ needs: [package]
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
- python_version: [ '2.7', '3.5', '3.6', '3.7', '3.8', 'pypy2', 'pypy3' ]
+ python_version: [ '3.8', '3.9', '3.10', '3.11', '3.12.0-beta - 3.12', 'pypy-3.8' ]
os: [windows-latest, ubuntu-latest] #, macos-latest]
- exclude:
- - os: windows-latest
- python_version: "pypy2"
include:
- - os: ubuntu-latest
- python_version: '3.9-dev'
+ - os: windows-latest
+ python_version: 'msys2'
name: ${{ matrix.os }} - Python ${{ matrix.python_version }}
steps:
- - uses: actions/checkout@v1
+ - uses: actions/checkout@v4
- name: Setup python
- uses: actions/setup-python@v2
- if: matrix.python_version != '3.9-dev'
+ uses: actions/setup-python@v4
+ if: matrix.python_version != 'msys2'
with:
python-version: ${{ matrix.python_version }}
architecture: x64
- - name: Set up Python ${{ matrix.python_version }} (deadsnakes)
- uses: deadsnakes/action@v1.0.0
- if: matrix.python_version == '3.9-dev'
+ - name: Setup MSYS2
+ uses: msys2/setup-msys2@v2
+ if: matrix.python_version == 'msys2'
with:
- python-version: ${{ matrix.python_version }}
- architecture: x64
- - run: pip install -U setuptools
- - run: pip install -e .[toml] pytest
- - run: pytest
-
- check_selfinstall:
- runs-on: ubuntu-latest
- strategy:
- fail-fast: false
- matrix:
- python_version: [ '2.7', '3.5', '3.6', '3.7', '3.8', 'pypy2', 'pypy3' ]
- name: check self install - Python ${{ matrix.python_version }}
- steps:
- - uses: actions/checkout@v1
- - name: Setup python
- uses: actions/setup-python@v2
+ 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 <https://dev.gnupg.org/T5593>, 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
+ # <https://github.com/actions/virtual-environments/issues/2876>.
+ #
+ # Additionally, we'll explicitly set `gpg.program` to ensure Git for Windows
+ # doesn't invoke the bundled GnuPG, otherwise we'll run into
+ # <https://dev.gnupg.org/T5504>. See also: <https://dev.gnupg.org/T3020>.
+ 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@v3
with:
- python-version: ${{ matrix.python_version }}
- architecture: x64
- # self install testing needs some clarity
- # so its being executed without any other tools running
- - run: pip install -U setuptools
- - run: python setup.py egg_info
- - run: python setup.py sdist
- - run: easy_install dist/*
- - run: python testing/check_self_install.py
+ 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 hopefull helps with os caches, hg init sometimes gets 20s timeouts
+ - run: hg version
+ - run: pytest
+ timeout-minutes: 15
+ dist_upload:
- eggs:
runs-on: ubuntu-latest
-
+ if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags')
+ permissions:
+ id-token: write
needs: [test]
- name: Python ${{ matrix.python_version }} eggs
- strategy:
- matrix:
- python_version: ['2.7', '3.5', '3.6', '3.7', '3.8', '3.9-dev']
steps:
- - uses: actions/checkout@v1
- - name: Setup python
- uses: actions/setup-python@v2
- if: matrix.python_version != '3.9-dev'
- with:
- python-version: ${{ matrix.python_version }}
- architecture: x64
- - name: Set up Python ${{ matrix.python_version }} (deadsnakes)
- uses: deadsnakes/action@v1.0.0
- if: matrix.python_version == '3.9-dev'
- with:
- python-version: ${{ matrix.python_version }}
- architecture: x64
- - name: Install dependencies
- run: |
- python -m pip install --upgrade pip
- pip install --upgrade wheel setuptools
- - run: python setup.py egg_info
- - name: Build package
- run: python setup.py bdist_egg
- - uses: actions/upload-artifact@v2
+ - uses: actions/download-artifact@v3
with:
- name: dist
+ name: Packages
path: dist
+ - name: Publish package to PyPI
+ uses: pypa/gh-action-pypi-publish@release/v1
- dist:
+ test-pypi-upload:
runs-on: ubuntu-latest
-
needs: [test]
- name: Python bdist/wheel
- steps:
- - uses: actions/checkout@v1
- - uses: actions/setup-python@v1
- with:
- python-version: "3.8"
- - name: Install dependencies
- run: |
- python -m pip install --upgrade pip
- pip install --upgrade wheel setuptools
- - run: python setup.py egg_info
- - name: Build package
- run: python setup.py bdist_wheel sdist
- - uses: actions/upload-artifact@v2
- with:
- name: dist
- path: dist
-
-
- dist_check:
- runs-on: ubuntu-latest
- needs: [eggs, dist]
- steps:
- - uses: actions/setup-python@v2
- with:
- python-version: "3.8"
- - name: Install dependencies
- run: pip install twine
- - uses: actions/download-artifact@v2
- with:
- name: dist
- path: dist
- - run: twine check dist/*
-
- dist_upload:
-
- runs-on: ubuntu-latest
- if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags')
- needs: [dist_check]
+ permissions:
+ id-token: write
steps:
- - uses: actions/download-artifact@v2
+ - uses: actions/download-artifact@v3
with:
- name: dist
+ name: Packages
path: dist
- name: Publish package to PyPI
- uses: pypa/gh-action-pypi-publish@master
+ continue-on-error: true
+ uses: pypa/gh-action-pypi-publish@release/v1
with:
- user: __token__
- password: ${{ secrets.pypi_token }}
+ repository-url: https://test.pypi.org/legacy/
+++ /dev/null
-### JetBrains template
-# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion
-
-*.iml
-
-## Directory-based project format:
-.idea/
-
-### Other editors
-.*.swp
-
-
-### 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
-
-# Unit test / coverage reports
-htmlcov/
-.tox/
-.coverage
-.coverage.*
-.cache
-.pytest_cache
-nosetests.xml
-coverage.xml
-*,cover
-
-# Sphinx documentation
-docs/_build/
--- /dev/null
+
+# 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
+
+## bugfix
+
+- 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
+
+## bugfix
+
+- 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
+
+## bugfix
+
+- 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`
+
+## features
+
+- 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
+
+## bugfixes
+
+- 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 hande 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
+
+# 6.3.2
+
+- fix #629: correctly convert Version data in tags_to_version parser to avoid errors
+
+# 6.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
+
+# 6.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
+
+## bugfixes
+
+- 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
+
+Bugfixes:
+
+- 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
+
+ <https://www.python.org/dev/peps/pep-0440/#preceding-v-character>
+
+# 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: Re-use 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
+ behaviour 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)
+++ /dev/null
-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_project_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 ruushing 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 filedescriptors 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` 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 selfinstall from tarballs
-
-v3.0.5
-======
-
-* fix #292 - match leading ``V`` character as well
-
- https://www.python.org/dev/peps/pep-0440/#preceding-v-character
-
-v3.0.4
-=======
-
-* rerelease 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 functio nwas 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 a internal config object to allow the configruation fo tag parsing and prefixes
- (thanks to @punkadiddle for introducing it and passing it trough)
-
-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 cant 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 isntalled 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 missuse in surogate-escape api
-* add the node-and-timestamp local version sheme
-* 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 interal SetupttoolsVersion
- as it was removed after long-standing deprecation
-
-v1.15.7
-======
-
-* Fix #174 with #207: Re-use 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 erros 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 its always correctly usign 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 behaviour 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 commited 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 trough 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 isssue #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, its 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 :code:`relative_to` parameter to :code:`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 :code:`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
-
-* propperly raise errors on write_to missuse (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 :code:`write_to` coniguration parameter
-
-v1.4.0
-======
-
-* propper handling for sdist
-* fix file-finder failure from windows
-* resuffle docs
-
-v1.3.0
-======
-
-* support setuptools easy_install egg creation details
- by hardwireing 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
-=====
-
-* dont 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 its 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)
exclude *.nix
-exclude .travis.yaml
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
Metadata-Version: 2.1
-Name: setuptools_scm
-Version: 4.1.2
+Name: setuptools-scm
+Version: 8.0.4
Summary: the blessed package to manage your versions by scm tags
-Home-page: https://github.com/pypa/setuptools_scm/
-Author: Ronny Pfannschmidt
-Author-email: opensource@ronnypfannschmidt.de
-License: MIT
-Description: setuptools_scm
- ===============
-
- ``setuptools_scm`` handles managing your Python package versions
- in SCM metadata instead of declaring them as the version argument
- or in a 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 of the SCM-managed files to the sdist).
- Unwanted files must be excluded by discarding them via ``MANIFEST.in``.
-
- .. image:: https://travis-ci.org/pypa/setuptools_scm.svg?branch=master
- :target: https://travis-ci.org/pypa/setuptools_scm
-
- .. image:: https://tidelift.com/badges/package/pypi/setuptools-scm
- :target: https://tidelift.com/subscription/pkg/pypi-setuptools-scm?utm_source=pypi-setuptools-scm&utm_medium=readme
-
-
- ``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 42 or later, released in Nov, 2019.
- If your project needs to support build from sdist on older versions
- of Setuptools, you will need to also implement the ``setup.py usage``
- for those legacy environments.
-
- First, ensure that ``setuptools_scm`` is present during the project's
- built step by specifying it as one of the build requirements.
-
- .. code:: toml
-
- # pyproject.toml
- [build-system]
- requires = ["setuptools>=42", "wheel", "setuptools_scm[toml]>=3.4"]
-
- Note that the ``toml`` extra must be supplied.
-
- 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/>`_). Many tools,
- especially those that invoke ``setup.py`` for any reason, may
- continue to rely on ``setup_requires``. For maximum compatibility
- with those uses, consider also including a ``setup_requires`` directive
- (described below in ``setup.py usage`` and ``setup.cfg``).
-
- To enable version inference, add this section to your pyproject.toml:
-
- .. code:: toml
-
- # pyproject.toml
- [tool.setuptools_scm]
-
- Including this section is comparable to supplying
- ``use_scm_version=True`` in ``setup.py``. Additionally,
- include arbitrary keyword arguments in that section
- to be supplied to ``get_version()``. For example:
-
- .. code:: toml
-
- # pyproject.toml
-
- [tool.setuptools_scm]
- write_to = "pkg/version.py"
-
-
- ``setup.py`` usage
- ------------------
-
- The following settings are considered legacy behavior and
- superseded by the ``pyproject.toml`` usage, but for maximal
- compatibility, projects may also supply the configuration in
- this older form.
-
- To use ``setuptools_scm`` just modify your project's ``setup.py`` file
- like this:
-
- * Add ``setuptools_scm`` to the ``setup_requires`` parameter.
- * Add the ``use_scm_version`` parameter and set it to ``True``.
-
- For example:
-
- .. code:: python
-
- from setuptools import setup
- setup(
- ...,
- use_scm_version=True,
- setup_requires=['setuptools_scm'],
- ...,
- )
-
- Arguments to ``get_version()`` (see below) may be passed as a dictionary to
- ``use_scm_version``. For example:
-
- .. code:: python
-
- from setuptools import setup
- setup(
- ...,
- use_scm_version = {
- "root": "..",
- "relative_to": __file__,
- "local_scheme": "node-and-timestamp"
- },
- setup_requires=['setuptools_scm'],
- ...,
- )
-
- You can confirm the version number locally via ``setup.py``:
-
- .. code-block:: shell
-
- $ python setup.py --version
-
- .. note::
-
- If you see unusual version numbers for packages but ``python setup.py
- --version`` reports the expected version number, ensure ``[egg_info]`` is
- not defined in ``setup.cfg``.
-
-
- ``setup.cfg`` usage
- -------------------
-
- If using `setuptools 30.3.0
- <https://setuptools.readthedocs.io/en/latest/setuptools.html#configuring-setup-using-setup-cfg-files>`_
- or greater, you can store ``setup_requires`` configuration in ``setup.cfg``.
- However, ``use_scm_version`` must still be placed in ``setup.py``. For example:
-
- .. code:: python
-
- # setup.py
- from setuptools import setup
- setup(
- use_scm_version=True,
- )
-
- .. code:: ini
-
- # setup.cfg
- [metadata]
- ...
-
- [options]
- setup_requires =
- setuptools_scm
- ...
-
- .. important::
-
- Ensure neither the ``[metadata]`` ``version`` option nor the ``[egg_info]``
- section are defined, as these will interfere with ``setuptools_scm``.
-
- You may also need to define a ``pyproject.toml`` file (`PEP-0518
- <https://www.python.org/dev/peps/pep-0518>`_) to ensure you have the required
- version of ``setuptools``:
-
- .. code:: ini
-
- # pyproject.toml
- [build-system]
- requires = ["setuptools>=30.3.0", "wheel", "setuptools_scm"]
-
- For more information, refer to the `setuptools issue #1002
- <https://github.com/pypa/setuptools/issues/1002>`_.
-
-
- Programmatic usage
- ------------------
-
- In order to use ``setuptools_scm`` from code that is one directory deeper
- than the project's root, you can use:
-
- .. code:: python
-
- from setuptools_scm import get_version
- version = get_version(root='..', relative_to=__file__)
-
- See `setup.py Usage`_ above for how to use this within ``setup.py``.
-
-
- Retrieving package version at runtime
- -------------------------------------
-
- If you have opted not to hardcode the version number inside the package,
- you can retrieve it at runtime from PEP-0566_ metadata using
- ``importlib.metadata`` from the standard library
- or the `importlib_metadata`_ backport:
-
- .. code:: python
-
- from importlib.metadata import version, PackageNotFoundError
-
- try:
- __version__ = version(__name__)
- except PackageNotFoundError:
- # package is not installed
- pass
-
- Alternatively, you can use ``pkg_resources`` which is included in
- ``setuptools``:
-
- .. code:: python
-
- from pkg_resources import get_distribution, DistributionNotFound
-
- try:
- __version__ = get_distribution(__name__).version
- except DistributionNotFound:
- # package is not installed
- pass
-
- This does place a runtime dependency on ``setuptools``.
-
- .. _PEP-0566: https://www.python.org/dev/peps/pep-0566/
- .. _importlib_metadata: https://pypi.org/project/importlib-metadata/
-
-
- Usage from Sphinx
- -----------------
-
- It is discouraged to use ``setuptools_scm`` from Sphinx itself,
- instead use ``pkg_resources`` after editable/real installation:
-
- .. code:: python
-
- # contents of docs/conf.py
- from pkg_resources import get_distribution
- release = get_distribution('myproject').version
- # for example take major/minor
- version = '.'.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.
-
- Notable Plugins
- ----------------
-
- `setuptools_scm_git_archive <https://pypi.python.org/pypi/setuptools_scm_git_archive>`_
- Provides partial support for obtaining versions from git archives that
- belong to tagged versions. The only reason for not including it in
- ``setuptools_scm`` itself is Git/GitHub not supporting sufficient metadata
- for untagged/followup commits, which is preventing a consistent UX.
-
-
- 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:
-
- no distance and clean:
- ``{tag}``
- distance and clean:
- ``{next_version}.dev{distance}+{scm letter}{revision hash}``
- no distance and not clean:
- ``{tag}+dYYYYMMDD``
- distance and not clean:
- ``{next_version}.dev{distance}+{scm letter}{revision hash}.dYYYYMMDD``
-
- 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}``.
-
- 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.
-
- .. note::
-
- Future versions of ``setuptools_scm`` will switch to `SemVer
- <http://semver.org/>`_ by default hiding the the old behavior as an
- configurable option.
-
-
- Builtin mechanisms for obtaining version numbers
- --------------------------------------------------
-
- 1. the SCM itself (git/hg)
- 2. ``.hg_archival`` files (mercurial archives)
- 3. ``PKG-INFO``
-
- .. note::
-
- Git archives are not supported due to Git shortcomings
-
-
- File finders hook makes most of MANIFEST.in unnecessary
- -------------------------------------------------------
-
- ``setuptools_scm`` implements a `file_finders
- <https://setuptools.readthedocs.io/en/latest/setuptools.html#adding-support-for-revision-control-systems>`_
- 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 <https://setuptools.readthedocs.io/en/latest/setuptools.html#including-data-files>`_
- 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.
-
-
- Configuration parameters
- ------------------------
-
- In order to configure the way ``use_scm_version`` works you can provide
- a mapping with options instead of a boolean value.
-
- The currently supported configuration keys are:
-
- :root:
- Relative path to cwd, used for finding the SCM root; defaults to ``.``
-
- :version_scheme:
- Configures how the local version number is constructed; either an
- entrypoint name or a callable.
-
- :local_scheme:
- Configures how the local component of the version is constructed; either an
- entrypoint name or a callable.
-
- :write_to:
- 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 `pkg_resources.get_distribution`
- (which adds some overhead).
-
- .. warning::
-
- Only files with :code:`.py` and :code:`.txt` extensions have builtin
- templates, for other file types it is necessary to provide
- :code:`write_to_template`.
-
- :write_to_template:
- A newstyle format string that is given the current version as
- the ``version`` keyword argument for formatting.
-
- :relative_to:
- A file 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:
- 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``
- (see `config.py <src/setuptools_scm/config.py>`_).
-
- :parentdir_prefix_version:
- 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 is 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:
- 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:
- 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`` command.
- Use with caution, this is a function for advanced use, and you should be
- familiar with the ``setuptools_scm`` internals to use it.
-
- Defaults to the value set by ``setuptools_scm.git.DEFAULT_DESCRIBE``
- (see `git.py <src/setuptools_scm/git.py>`_).
-
- To use ``setuptools_scm`` in other Python code you can use the ``get_version``
- function:
-
- .. code:: python
-
- from setuptools_scm import get_version
- my_version = get_version()
-
- It optionally accepts the keys of the ``use_scm_version`` parameter as
- keyword arguments.
-
- Example configuration in ``setup.py`` format:
-
- .. code:: python
-
- from setuptools import setup
-
- setup(
- use_scm_version={
- 'write_to': 'version.py',
- 'write_to_template': '__version__ = "{version}"',
- 'tag_regex': r'^(?P<prefix>v)?(?P<version>[^\+]+)(?P<suffix>.*)?$',
- }
- )
-
- Environment variables
- ---------------------
-
- :SETUPTOOLS_SCM_PRETEND_VERSION:
- when defined and not empty,
- its used as the primary source for the version number
- in which case it will be a unparsed string
-
- :SETUPTOOLS_SCM_DEBUG:
- when defined and not empty,
- a lot of debug information will be printed as part of ``setuptools_scm``
- operating
-
- Extending setuptools_scm
- ------------------------
-
- ``setuptools_scm`` ships with a few ``setuptools`` entrypoints based hooks to
- extend its default capabilities.
-
- 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 use then name of your SCM control directory as name of the entrypoint.
-
- Version number construction
- ~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
- ``setuptools_scm.version_scheme``
- Configures how the version number is constructed given a
- ``setuptools_scm.version.ScmVersion`` instance and should return a string
- representing the version.
-
- Available implementations:
-
- :guess-next-dev: 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`.
- :post-release: generates post release versions (adds :code:`.postN`)
- :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 appends :code:`.devN`. 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) if 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 :code:`.devN`.
-
- ``setuptools_scm.local_scheme``
- Configures how the local part of a version is rendered given a
- ``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: adds the node on dev versions and the date on dirty
- workdir (default)
- :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
-
-
- Importing in ``setup.py``
- ~~~~~~~~~~~~~~~~~~~~~~~~~
-
- To support usage in ``setup.py`` passing a callable into ``use_scm_version``
- is supported.
-
- Within that callable, ``setuptools_scm`` is available for import.
- The callable must return the configuration.
-
-
- .. code:: python
-
- # content of setup.py
- import setuptools
-
- def myversion():
- from setuptools_scm.version import get_local_dirty_tag
- def clean_scheme(version):
- return get_local_dirty_tag(version) if version.dirty else '+clean'
-
- return {'local_scheme': clean_scheme}
-
- setup(
- ...,
- use_scm_version=myversion,
- ...
- )
-
-
- Note on testing non-installed versions
- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
- While the general advice is to test against a installed version,
- some environments require a test prior to install,
-
- .. code::
-
- $ python setup.py egg_info
- $ PYTHONPATH=$PWD:$PWD/src pytest
-
-
- Interaction with Enterprise Distributions
- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
- Some enterprise distributions like RHEL7 and others
- ship rather old setuptools versions due to various release management details.
-
- On such distributions one might observe errors like:
-
- :code:``setuptools_scm.version.SetuptoolsOutdatedWarning: your setuptools is too old (<12)``
-
- In those case its typically possible to build by using a 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.
-
- In case the project you need to build can not be patched to either use old setuptools_scm,
- its still possible to install a more recent version of setuptools in order to handle the build
- and/or install the package by using wheels or eggs.
-
-
-
-
- Code of Conduct
- ---------------
-
- Everyone interacting in the ``setuptools_scm`` project's codebases, issue
- trackers, chat rooms, and mailing lists is expected to follow the
- `PyPA Code of Conduct`_.
-
- .. _PyPA Code of Conduct: https://www.pypa.io/en/latest/code-of-conduct/
-
- Security Contact
- ================
-
- To report a security vulnerability, please use the
- `Tidelift security contact <https://tidelift.com/security>`_.
- Tidelift will coordinate the fix and disclosure.
-
-Platform: UNKNOWN
+Author-email: Ronny Pfannschmidt <opensource@ronnypfannschmidt.de>
+License: 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.
+
+Project-URL: documentation, https://setuptools-scm.readthedocs.io/
+Project-URL: repository, https://github.com/pypa/setuptools_scm/
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python
-Classifier: Programming Language :: Python :: 2.7
-Classifier: Programming Language :: Python :: 3
-Classifier: Programming Language :: Python :: 3.5
-Classifier: Programming Language :: Python :: 3.6
-Classifier: Programming Language :: Python :: 3.7
+Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.8
+Classifier: Programming Language :: Python :: 3.9
+Classifier: Programming Language :: Python :: 3.10
+Classifier: Programming Language :: Python :: 3.11
Classifier: Topic :: Software Development :: Libraries
Classifier: Topic :: Software Development :: Version Control
Classifier: Topic :: System :: Software Distribution
Classifier: Topic :: Utilities
-Requires-Python: !=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7
+Requires-Python: >=3.8
+Description-Content-Type: text/markdown
+License-File: LICENSE
+Requires-Dist: packaging>=20
+Requires-Dist: setuptools
+Requires-Dist: tomli>=1; python_version < "3.11"
+Requires-Dist: typing-extensions
+Provides-Extra: docs
+Requires-Dist: entangled_cli[rich]; extra == "docs"
+Requires-Dist: mkdocs; extra == "docs"
+Requires-Dist: mkdocs-entangled-plugin; extra == "docs"
+Requires-Dist: mkdocs-material; extra == "docs"
+Requires-Dist: mkdocstrings[python]; extra == "docs"
+Requires-Dist: pygments; extra == "docs"
+Provides-Extra: rich
+Requires-Dist: rich; extra == "rich"
+Provides-Extra: test
+Requires-Dist: build; extra == "test"
+Requires-Dist: pytest; extra == "test"
+Requires-Dist: rich; extra == "test"
+Requires-Dist: wheel; extra == "test"
Provides-Extra: toml
+
+# setuptools_scm
+[](https://github.com/pypa/setuptools_scm/actions)
+[](https://setuptools-scm.readthedocs.io/en/latest/?badge=latest)
+[ ](https://tidelift.com/subscription/pkg/pypi-setuptools-scm?utm_source=pypi-setuptools-scm&utm_medium=readme)
+
+## about
+
+[setuptools-scm] extracts Python package versions from `git` or
+`hg` metadata instead of declaring them as the version argument
+or in an SCM managed file.
+
+Additionally, [setuptools-scm] provides setuptools
+with a list of files that are managed by the SCM <br/>
+(i.e. it automatically adds **all of** the SCM-managed files to the sdist).<br/>
+Unwanted files must be excluded via `MANIFEST.in`.
+
+
+## `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 60 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
+[build-system]
+requires = [
+ "setuptools>=60",
+ "setuptools-scm>=8.0"]
+```
+
+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/
+
+
+## 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.
--- /dev/null
+# setuptools_scm
+[](https://github.com/pypa/setuptools_scm/actions)
+[](https://setuptools-scm.readthedocs.io/en/latest/?badge=latest)
+[ ](https://tidelift.com/subscription/pkg/pypi-setuptools-scm?utm_source=pypi-setuptools-scm&utm_medium=readme)
+
+## about
+
+[setuptools-scm] extracts Python package versions from `git` or
+`hg` metadata instead of declaring them as the version argument
+or in an SCM managed file.
+
+Additionally, [setuptools-scm] provides setuptools
+with a list of files that are managed by the SCM <br/>
+(i.e. it automatically adds **all of** the SCM-managed files to the sdist).<br/>
+Unwanted files must be excluded via `MANIFEST.in`.
+
+
+## `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 60 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
+[build-system]
+requires = [
+ "setuptools>=60",
+ "setuptools-scm>=8.0"]
+```
+
+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/
+
+
+## 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.
+++ /dev/null
-setuptools_scm
-===============
-
-``setuptools_scm`` handles managing your Python package versions
-in SCM metadata instead of declaring them as the version argument
-or in a 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 of the SCM-managed files to the sdist).
-Unwanted files must be excluded by discarding them via ``MANIFEST.in``.
-
-.. image:: https://travis-ci.org/pypa/setuptools_scm.svg?branch=master
- :target: https://travis-ci.org/pypa/setuptools_scm
-
-.. image:: https://tidelift.com/badges/package/pypi/setuptools-scm
- :target: https://tidelift.com/subscription/pkg/pypi-setuptools-scm?utm_source=pypi-setuptools-scm&utm_medium=readme
-
-
-``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 42 or later, released in Nov, 2019.
-If your project needs to support build from sdist on older versions
-of Setuptools, you will need to also implement the ``setup.py usage``
-for those legacy environments.
-
-First, ensure that ``setuptools_scm`` is present during the project's
-built step by specifying it as one of the build requirements.
-
-.. code:: toml
-
- # pyproject.toml
- [build-system]
- requires = ["setuptools>=42", "wheel", "setuptools_scm[toml]>=3.4"]
-
-Note that the ``toml`` extra must be supplied.
-
-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/>`_). Many tools,
-especially those that invoke ``setup.py`` for any reason, may
-continue to rely on ``setup_requires``. For maximum compatibility
-with those uses, consider also including a ``setup_requires`` directive
-(described below in ``setup.py usage`` and ``setup.cfg``).
-
-To enable version inference, add this section to your pyproject.toml:
-
-.. code:: toml
-
- # pyproject.toml
- [tool.setuptools_scm]
-
-Including this section is comparable to supplying
-``use_scm_version=True`` in ``setup.py``. Additionally,
-include arbitrary keyword arguments in that section
-to be supplied to ``get_version()``. For example:
-
-.. code:: toml
-
- # pyproject.toml
-
- [tool.setuptools_scm]
- write_to = "pkg/version.py"
-
-
-``setup.py`` usage
-------------------
-
-The following settings are considered legacy behavior and
-superseded by the ``pyproject.toml`` usage, but for maximal
-compatibility, projects may also supply the configuration in
-this older form.
-
-To use ``setuptools_scm`` just modify your project's ``setup.py`` file
-like this:
-
-* Add ``setuptools_scm`` to the ``setup_requires`` parameter.
-* Add the ``use_scm_version`` parameter and set it to ``True``.
-
-For example:
-
-.. code:: python
-
- from setuptools import setup
- setup(
- ...,
- use_scm_version=True,
- setup_requires=['setuptools_scm'],
- ...,
- )
-
-Arguments to ``get_version()`` (see below) may be passed as a dictionary to
-``use_scm_version``. For example:
-
-.. code:: python
-
- from setuptools import setup
- setup(
- ...,
- use_scm_version = {
- "root": "..",
- "relative_to": __file__,
- "local_scheme": "node-and-timestamp"
- },
- setup_requires=['setuptools_scm'],
- ...,
- )
-
-You can confirm the version number locally via ``setup.py``:
-
-.. code-block:: shell
-
- $ python setup.py --version
-
-.. note::
-
- If you see unusual version numbers for packages but ``python setup.py
- --version`` reports the expected version number, ensure ``[egg_info]`` is
- not defined in ``setup.cfg``.
-
-
-``setup.cfg`` usage
--------------------
-
-If using `setuptools 30.3.0
-<https://setuptools.readthedocs.io/en/latest/setuptools.html#configuring-setup-using-setup-cfg-files>`_
-or greater, you can store ``setup_requires`` configuration in ``setup.cfg``.
-However, ``use_scm_version`` must still be placed in ``setup.py``. For example:
-
-.. code:: python
-
- # setup.py
- from setuptools import setup
- setup(
- use_scm_version=True,
- )
-
-.. code:: ini
-
- # setup.cfg
- [metadata]
- ...
-
- [options]
- setup_requires =
- setuptools_scm
- ...
-
-.. important::
-
- Ensure neither the ``[metadata]`` ``version`` option nor the ``[egg_info]``
- section are defined, as these will interfere with ``setuptools_scm``.
-
-You may also need to define a ``pyproject.toml`` file (`PEP-0518
-<https://www.python.org/dev/peps/pep-0518>`_) to ensure you have the required
-version of ``setuptools``:
-
-.. code:: ini
-
- # pyproject.toml
- [build-system]
- requires = ["setuptools>=30.3.0", "wheel", "setuptools_scm"]
-
-For more information, refer to the `setuptools issue #1002
-<https://github.com/pypa/setuptools/issues/1002>`_.
-
-
-Programmatic usage
-------------------
-
-In order to use ``setuptools_scm`` from code that is one directory deeper
-than the project's root, you can use:
-
-.. code:: python
-
- from setuptools_scm import get_version
- version = get_version(root='..', relative_to=__file__)
-
-See `setup.py Usage`_ above for how to use this within ``setup.py``.
-
-
-Retrieving package version at runtime
--------------------------------------
-
-If you have opted not to hardcode the version number inside the package,
-you can retrieve it at runtime from PEP-0566_ metadata using
-``importlib.metadata`` from the standard library
-or the `importlib_metadata`_ backport:
-
-.. code:: python
-
- from importlib.metadata import version, PackageNotFoundError
-
- try:
- __version__ = version(__name__)
- except PackageNotFoundError:
- # package is not installed
- pass
-
-Alternatively, you can use ``pkg_resources`` which is included in
-``setuptools``:
-
-.. code:: python
-
- from pkg_resources import get_distribution, DistributionNotFound
-
- try:
- __version__ = get_distribution(__name__).version
- except DistributionNotFound:
- # package is not installed
- pass
-
-This does place a runtime dependency on ``setuptools``.
-
-.. _PEP-0566: https://www.python.org/dev/peps/pep-0566/
-.. _importlib_metadata: https://pypi.org/project/importlib-metadata/
-
-
-Usage from Sphinx
------------------
-
-It is discouraged to use ``setuptools_scm`` from Sphinx itself,
-instead use ``pkg_resources`` after editable/real installation:
-
-.. code:: python
-
- # contents of docs/conf.py
- from pkg_resources import get_distribution
- release = get_distribution('myproject').version
- # for example take major/minor
- version = '.'.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.
-
-Notable Plugins
-----------------
-
-`setuptools_scm_git_archive <https://pypi.python.org/pypi/setuptools_scm_git_archive>`_
- Provides partial support for obtaining versions from git archives that
- belong to tagged versions. The only reason for not including it in
- ``setuptools_scm`` itself is Git/GitHub not supporting sufficient metadata
- for untagged/followup commits, which is preventing a consistent UX.
-
-
-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:
-
-no distance and clean:
- ``{tag}``
-distance and clean:
- ``{next_version}.dev{distance}+{scm letter}{revision hash}``
-no distance and not clean:
- ``{tag}+dYYYYMMDD``
-distance and not clean:
- ``{next_version}.dev{distance}+{scm letter}{revision hash}.dYYYYMMDD``
-
-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}``.
-
-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.
-
-.. note::
-
- Future versions of ``setuptools_scm`` will switch to `SemVer
- <http://semver.org/>`_ by default hiding the the old behavior as an
- configurable option.
-
-
-Builtin mechanisms for obtaining version numbers
---------------------------------------------------
-
-1. the SCM itself (git/hg)
-2. ``.hg_archival`` files (mercurial archives)
-3. ``PKG-INFO``
-
-.. note::
-
- Git archives are not supported due to Git shortcomings
-
-
-File finders hook makes most of MANIFEST.in unnecessary
--------------------------------------------------------
-
-``setuptools_scm`` implements a `file_finders
-<https://setuptools.readthedocs.io/en/latest/setuptools.html#adding-support-for-revision-control-systems>`_
-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 <https://setuptools.readthedocs.io/en/latest/setuptools.html#including-data-files>`_
- 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.
-
-
-Configuration parameters
-------------------------
-
-In order to configure the way ``use_scm_version`` works you can provide
-a mapping with options instead of a boolean value.
-
-The currently supported configuration keys are:
-
-:root:
- Relative path to cwd, used for finding the SCM root; defaults to ``.``
-
-:version_scheme:
- Configures how the local version number is constructed; either an
- entrypoint name or a callable.
-
-:local_scheme:
- Configures how the local component of the version is constructed; either an
- entrypoint name or a callable.
-
-:write_to:
- 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 `pkg_resources.get_distribution`
- (which adds some overhead).
-
- .. warning::
-
- Only files with :code:`.py` and :code:`.txt` extensions have builtin
- templates, for other file types it is necessary to provide
- :code:`write_to_template`.
-
-:write_to_template:
- A newstyle format string that is given the current version as
- the ``version`` keyword argument for formatting.
-
-:relative_to:
- A file 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:
- 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``
- (see `config.py <src/setuptools_scm/config.py>`_).
-
-:parentdir_prefix_version:
- 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 is 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:
- 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:
- 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`` command.
- Use with caution, this is a function for advanced use, and you should be
- familiar with the ``setuptools_scm`` internals to use it.
-
- Defaults to the value set by ``setuptools_scm.git.DEFAULT_DESCRIBE``
- (see `git.py <src/setuptools_scm/git.py>`_).
-
-To use ``setuptools_scm`` in other Python code you can use the ``get_version``
-function:
-
-.. code:: python
-
- from setuptools_scm import get_version
- my_version = get_version()
-
-It optionally accepts the keys of the ``use_scm_version`` parameter as
-keyword arguments.
-
-Example configuration in ``setup.py`` format:
-
-.. code:: python
-
- from setuptools import setup
-
- setup(
- use_scm_version={
- 'write_to': 'version.py',
- 'write_to_template': '__version__ = "{version}"',
- 'tag_regex': r'^(?P<prefix>v)?(?P<version>[^\+]+)(?P<suffix>.*)?$',
- }
- )
-
-Environment variables
----------------------
-
-:SETUPTOOLS_SCM_PRETEND_VERSION:
- when defined and not empty,
- its used as the primary source for the version number
- in which case it will be a unparsed string
-
-:SETUPTOOLS_SCM_DEBUG:
- when defined and not empty,
- a lot of debug information will be printed as part of ``setuptools_scm``
- operating
-
-Extending setuptools_scm
-------------------------
-
-``setuptools_scm`` ships with a few ``setuptools`` entrypoints based hooks to
-extend its default capabilities.
-
-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 use then name of your SCM control directory as name of the entrypoint.
-
-Version number construction
-~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-``setuptools_scm.version_scheme``
- Configures how the version number is constructed given a
- ``setuptools_scm.version.ScmVersion`` instance and should return a string
- representing the version.
-
- Available implementations:
-
- :guess-next-dev: 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`.
- :post-release: generates post release versions (adds :code:`.postN`)
- :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 appends :code:`.devN`. 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) if 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 :code:`.devN`.
-
-``setuptools_scm.local_scheme``
- Configures how the local part of a version is rendered given a
- ``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: adds the node on dev versions and the date on dirty
- workdir (default)
- :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
-
-
-Importing in ``setup.py``
-~~~~~~~~~~~~~~~~~~~~~~~~~
-
-To support usage in ``setup.py`` passing a callable into ``use_scm_version``
-is supported.
-
-Within that callable, ``setuptools_scm`` is available for import.
-The callable must return the configuration.
-
-
-.. code:: python
-
- # content of setup.py
- import setuptools
-
- def myversion():
- from setuptools_scm.version import get_local_dirty_tag
- def clean_scheme(version):
- return get_local_dirty_tag(version) if version.dirty else '+clean'
-
- return {'local_scheme': clean_scheme}
-
- setup(
- ...,
- use_scm_version=myversion,
- ...
- )
-
-
-Note on testing non-installed versions
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-While the general advice is to test against a installed version,
-some environments require a test prior to install,
-
-.. code::
-
- $ python setup.py egg_info
- $ PYTHONPATH=$PWD:$PWD/src pytest
-
-
-Interaction with Enterprise Distributions
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-Some enterprise distributions like RHEL7 and others
-ship rather old setuptools versions due to various release management details.
-
-On such distributions one might observe errors like:
-
-:code:``setuptools_scm.version.SetuptoolsOutdatedWarning: your setuptools is too old (<12)``
-
-In those case its typically possible to build by using a 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.
-
-In case the project you need to build can not be patched to either use old setuptools_scm,
-its still possible to install a more recent version of setuptools in order to handle the build
-and/or install the package by using wheels or eggs.
-
-
-
-
-Code of Conduct
----------------
-
-Everyone interacting in the ``setuptools_scm`` project's codebases, issue
-trackers, chat rooms, and mailing lists is expected to follow the
-`PyPA Code of Conduct`_.
-
-.. _PyPA Code of Conduct: https://www.pypa.io/en/latest/code-of-conduct/
-
-Security Contact
-================
-
-To report a security vulnerability, please use the
-`Tidelift security contact <https://tidelift.com/security>`_.
-Tidelift will coordinate the fix and disclosure.
--- /dev/null
+"""
+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 # noqa
+
+from setuptools_scm import _types as _t
+from setuptools_scm import Configuration
+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 get_local_node_and_date
+from setuptools_scm.version import guess_next_dev_version
+from setuptools_scm.version import ScmVersion
+
+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
+ else:
+ 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)
--- /dev/null
+# 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 local version number is constructed; either an entrypoint name or a callable.
+
+`local_scheme : str | Callable[[ScmVersion], str]`
+: Configures how the local component of the version is constructed
+ either an entrypoint name or a callable.
+
+
+`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 `pkg_resources.get_distribution`
+ (which adds some overhead).
+
+ !!! warning ""
+
+ Only files with `.py` and `.txt` extensions have builtin templates,
+ for other file types it is necessary to provide `write_to_template`.
+
+`version_file_template_template: str | None = None`
+: A new-style format string that is given the current version as
+ the `version` keyword argument for formatting.
+
+`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 use distribution name specific pretend versions"
+
+
+`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
+
+ 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
+
+ it takes 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
--- /dev/null
+# 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})
+```
+
+
+``` { .python title="pyproject.toml" file="docs/examples/version_scheme_code/pyproject.toml" }
+[build-system]
+requires = [
+ "setuptools>=64",
+ "setuptools_scm>=8",
+ "wheel",
+]
+
+[project]
+name = "scm-example"
+dynamic = [
+ "version",
+]
+
+[tool.setuptools_scm]
+```
+
+- [ ] add a build block that adds example output
+- [ ] correct config after [entangled mkdocs bug] is fixed
+
+[entangled mkdocs bug]: https://github.com/entangled/mkdocs-plugin/issues/1
+
+
+
+
+## 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
--- /dev/null
+# ~/~ begin <<docs/customizing.md#docs/examples/version_scheme_code/pyproject.toml>>[init]
+[build-system]
+requires = [
+ "setuptools>=64",
+ "setuptools_scm>=8",
+ "wheel",
+]
+
+[project]
+name = "scm-example"
+dynamic = [
+ "version",
+]
+
+[tool.setuptools_scm]
+# ~/~ end
\ No newline at end of file
--- /dev/null
+# ~/~ begin <<docs/customizing.md#docs/examples/version_scheme_code/setup.py>>[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
--- /dev/null
+# 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`
+
+
+### `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
--- /dev/null
+# About
+
+
+`setuptools_scm` extracts Python package versions from `git` or `hg` metadata
+instead of declaring them as the version argument or in a 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`.
+
+## 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
+# pyproject.toml
+[build-system]
+requires = [
+ "setuptools>=64",
+ "setuptools_scm>=8",
+ "wheel",
+]
+[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)
--- /dev/null
+# 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`.
--- /dev/null
+# 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
+# pyproject.toml
+[build-system]
+requires = ["setuptools>=62", "setuptools_scm[toml]>=8.0"]
+
+[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
+
+```toml
+# pyproject.toml
+...
+[tool.setuptools_scm]
+version_file = "pkg/_version.py"
+```
+Where ``pkg`` is the name of your package.
+
+
+.. code-block:: shell
+
+ $ python -m setuptools_scm
+
+ # To explore other options, try:
+ $ python -m setuptools_scm --help
+
+
+## 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
+
+## at runtime (strongly discouraged)
+
+the most simple **looking** way to use `setuptools_scm` at runtime is:
+
+```python
+from setuptools_scm import get_version
+version = get_version()
+```
+
+
+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__)
+```
+
+
+## Python package metadata
+
+
+
+
+### version at runtime
+
+If you have opted not to hardcode the version number inside the package,
+you can retrieve it at runtime from PEP-0566_ metadata using
+``importlib.metadata`` from the standard library (added in Python 3.8)
+or the `importlib_metadata`_ backport:
+
+```python
+# contents of package_name/__init__.py
+from importlib.metadata import version, PackageNotFoundError
+
+try:
+ __version__ = version("package-name")
+except PackageNotFoundError:
+ # package is not installed
+ pass
+```
+
+.. _PEP-0566: https://www.python.org/dev/peps/pep-0566/
+.. _importlib_metadata: https://pypi.org/project/importlib-metadata/
+
+
+### Usage from Sphinx
+
+
+``` {.python file=docs/.entangled/sphinx_conf.py}
+from importlib.metadata import version as get_version
+release: str = get_version('setuptools_scm')
+# 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
+
+
+By default, Docker will not copy the `.git` folder into your container.
+Therefore, builds with version inference might fail.
+Consequently, you can use the following snippet to infer the version from
+the host OS without copying the entire `.git` folder to your `Dockerfile`.
+
+```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].
+
+## 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]*)$
+ref-names: $Format:%D$
+```
+
+``` {.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.
+
+
+
+### 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
--- /dev/null
+[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}"
\ No newline at end of file
--- /dev/null
+site_name: setuptools scm
+
+nav:
+ - index.md
+ - usage.md
+ - customizing.md
+ - config.md
+ - extending.md
+ - overrides.md
+theme:
+ name: material
+
+watch:
+- src/setuptools_scm
+- docs
+markdown_extensions:
+ - def_list
+ - admonition
+ - pymdownx.tasklist:
+ custom_checkbox: true
+
+ - pymdownx.superfences
+
+plugins:
+- entangled
+- search
+- mkdocstrings:
+ default_handler: python
+ handlers:
+ python:
+ paths: [ src ]
+
+ options:
+ separate_signature: true
+ show_signature_annotations: true
+ allow_inspection: true
+ show_root_heading: true
--- /dev/null
+[mypy]
+python_version = 3.8
+warn_return_any = True
+warn_unused_configs = True
+mypy_path = $MYPY_CONFIG_FILE_DIR/src
+strict = true
--- /dev/null
+<manifest>
+ <request>
+ <domain name="_"/>
+ </request>
+</manifest>
--- /dev/null
+Name: python3-setuptools_scm
+Version: 8.0.4
+Release: 1
+Summary: setuptools_scm handles managing your Python package versions
+License: MIT
+Group: Development/Languages/Python
+Source: setuptools_scm-%{version}.tar.gz
+Source1001: %{name}.manifest
+Source1002: setuptools_scm-8.0.4-py3-none-any.whl
+
+BuildRequires: pkgconfig(python3)
+BuildRequires: python3-pip
+BuildRequires: python3-packaging
+BuildRequires: python3-typing_extensions
+
+Requires: python3-packaging
+Requires: python3-typing_extensions
+
+BuildArch: noarch
+
+%description
+setuptools_scm handles managing your Python package versions in SCM metadata instead of declaring them as the version argument or in a SCM managed file.
+
+%prep
+%setup -q -n setuptools_scm-%{version}
+
+%build
+cp %{SOURCE1001} .
+
+%install
+%{_bindir}/python3 -m pip install --root %{buildroot} %{SOURCE1002}
+rm -f %{buildroot}/%{python3_sitelib}/setuptools_scm/.git_archival.txt
+
+%files
+%manifest %{name}.manifest
+%license LICENSE
+%{python3_sitelib}/setuptools_scm/*.py
+%{python3_sitelib}/setuptools_scm/__pycache__/*
+%{python3_sitelib}/setuptools_scm/_file_finders/*
+%{python3_sitelib}/setuptools_scm/_integration/*
+%{python3_sitelib}/setuptools_scm-%{version}.dist-info/*
+
+
[build-system]
-requires = ["setuptools>=34.4", "wheel"]
-build-backend = "setuptools.build_meta"
+build-backend = "_own_version_helper:build_meta"
+requires = [
+ "setuptools>=61",
+ 'tomli; python_version <= "3.12"',
+]
+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",
+ "Topic :: Software Development :: Libraries",
+ "Topic :: Software Development :: Version Control",
+ "Topic :: System :: Software Distribution",
+ "Topic :: Utilities",
+]
+dynamic = [
+ "version",
+]
+dependencies = [
+ "packaging>=20",
+ "setuptools",
+ 'tomli>=1; python_version <= "3.12"',
+ "typing-extensions",
+]
+[project.optional-dependencies]
+docs = [
+ "entangled_cli[rich]",
+ "mkdocs",
+ "mkdocs-entangled-plugin",
+ "mkdocs-material",
+ "mkdocstrings[python]",
+ "pygments",
+]
+rich = [
+ "rich",
+]
+test = [
+ "build",
+ "pytest",
+ "rich",
+ "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."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"
+"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]
+select = ["E", "F", "B", "U", "YTT", "C", "DTZ", "PYI", "PT"]
+ignore = ["B028"]
+
+[tool.pytest.ini_options]
+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 = ["-p", "no:unraisableexception"]
+markers = [
+ "issue(id): reference to github issue",
+ "skip_commit: allows to skip committing in the helpers",
+]
+
+[tool.scriv]
+format = "md"
-[metadata]
-license_file = LICENSE
-license = MIT
-name = setuptools_scm
-url = https://github.com/pypa/setuptools_scm/
-author = Ronny Pfannschmidt
-author_email = opensource@ronnypfannschmidt.de
-description = the blessed package to manage your versions by scm tags
-long_description = file:README.rst
-classifiers =
- Development Status :: 5 - Production/Stable
- Intended Audience :: Developers
- License :: OSI Approved :: MIT License
- Programming Language :: Python
- Programming Language :: Python :: 2.7
- Programming Language :: Python :: 3
- Programming Language :: Python :: 3.5
- Programming Language :: Python :: 3.6
- Programming Language :: Python :: 3.7
- Programming Language :: Python :: 3.8
- Topic :: Software Development :: Libraries
- Topic :: Software Development :: Version Control
- Topic :: System :: Software Distribution
- Topic :: Utilities
-
-[options]
-zip_safe = true
-python_requires = >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*
-install_requires =
- setuptools
-packages = find:
-package_dir =
- =src
-
-[options.packages.find]
-where = src
-
-[options.extras_require]
-toml = toml
-
-[options.entry_points]
-distutils.setup_keywords =
- use_scm_version = setuptools_scm.integration:version_keyword
-setuptools.file_finders =
- setuptools_scm = setuptools_scm.integration:find_files
-setuptools.finalize_distribution_options =
- setuptools_scm = setuptools_scm.integration:infer_version
-setuptools_scm.parse_scm =
- .hg = setuptools_scm.hg:parse
- .git = setuptools_scm.git:parse
-setuptools_scm.parse_scm_fallback =
- .hg_archival.txt = setuptools_scm.hg:parse_archival
- PKG-INFO = setuptools_scm.hacks:parse_pkginfo
- pip-egg-info = setuptools_scm.hacks:parse_pip_egg_info
- setup.py = setuptools_scm.hacks:fallback_version
-setuptools_scm.files_command =
- .hg = setuptools_scm.file_finder_hg:hg_find_files
- .git = setuptools_scm.file_finder_git:git_find_files
-setuptools_scm.version_scheme =
- guess-next-dev = setuptools_scm.version:guess_next_dev_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
-setuptools_scm.local_scheme =
- node-and-date = setuptools_scm.version:get_local_node_and_date
- node-and-timestamp = setuptools_scm.version:get_local_node_and_timestamp
- dirty-tag = setuptools_scm.version:get_local_dirty_tag
- no-local-version = setuptools_scm.version:get_no_local_node
-
-[bdist_wheel]
-universal = 1
-
[egg_info]
tag_build =
tag_date = 0
+++ /dev/null
-"""\
-important note:
-
-the setup of setuptools_scm is self-using,
-the first execution of `python setup.py egg_info`
-will generate partial data
-its critical to run `python setup.py egg_info`
-once before running sdist or easy_install on a fresh checkouts
-
-pip usage is recommended
-"""
-from __future__ import print_function
-import os
-import sys
-import setuptools
-
-
-def scm_config():
- here = os.path.dirname(os.path.abspath(__file__))
- src = os.path.join(here, "src")
- egg_info = os.path.join(src, "setuptools_scm.egg-info")
- has_entrypoints = os.path.isdir(egg_info)
- import pkg_resources
-
- sys.path.insert(0, src)
- pkg_resources.working_set.add_entry(src)
- # FIXME: remove debug
- print(src)
- print(pkg_resources.working_set)
- from setuptools_scm.hacks import parse_pkginfo
- from setuptools_scm.git import parse as parse_git
- from setuptools_scm.version import guess_next_dev_version, get_local_node_and_date
-
- def parse(root):
- try:
- return parse_pkginfo(root)
- except IOError:
- return parse_git(root)
-
- config = dict(
- version_scheme=guess_next_dev_version, local_scheme=get_local_node_and_date
- )
-
- if has_entrypoints:
- return dict(use_scm_version=config)
- else:
- from setuptools_scm import get_version
-
- return dict(version=get_version(root=here, parse=parse, **config))
-
-
-if __name__ == "__main__":
- setuptools.setup(**scm_config())
+++ /dev/null
-Metadata-Version: 2.1
-Name: setuptools-scm
-Version: 4.1.2
-Summary: the blessed package to manage your versions by scm tags
-Home-page: https://github.com/pypa/setuptools_scm/
-Author: Ronny Pfannschmidt
-Author-email: opensource@ronnypfannschmidt.de
-License: MIT
-Description: setuptools_scm
- ===============
-
- ``setuptools_scm`` handles managing your Python package versions
- in SCM metadata instead of declaring them as the version argument
- or in a 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 of the SCM-managed files to the sdist).
- Unwanted files must be excluded by discarding them via ``MANIFEST.in``.
-
- .. image:: https://travis-ci.org/pypa/setuptools_scm.svg?branch=master
- :target: https://travis-ci.org/pypa/setuptools_scm
-
- .. image:: https://tidelift.com/badges/package/pypi/setuptools-scm
- :target: https://tidelift.com/subscription/pkg/pypi-setuptools-scm?utm_source=pypi-setuptools-scm&utm_medium=readme
-
-
- ``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 42 or later, released in Nov, 2019.
- If your project needs to support build from sdist on older versions
- of Setuptools, you will need to also implement the ``setup.py usage``
- for those legacy environments.
-
- First, ensure that ``setuptools_scm`` is present during the project's
- built step by specifying it as one of the build requirements.
-
- .. code:: toml
-
- # pyproject.toml
- [build-system]
- requires = ["setuptools>=42", "wheel", "setuptools_scm[toml]>=3.4"]
-
- Note that the ``toml`` extra must be supplied.
-
- 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/>`_). Many tools,
- especially those that invoke ``setup.py`` for any reason, may
- continue to rely on ``setup_requires``. For maximum compatibility
- with those uses, consider also including a ``setup_requires`` directive
- (described below in ``setup.py usage`` and ``setup.cfg``).
-
- To enable version inference, add this section to your pyproject.toml:
-
- .. code:: toml
-
- # pyproject.toml
- [tool.setuptools_scm]
-
- Including this section is comparable to supplying
- ``use_scm_version=True`` in ``setup.py``. Additionally,
- include arbitrary keyword arguments in that section
- to be supplied to ``get_version()``. For example:
-
- .. code:: toml
-
- # pyproject.toml
-
- [tool.setuptools_scm]
- write_to = "pkg/version.py"
-
-
- ``setup.py`` usage
- ------------------
-
- The following settings are considered legacy behavior and
- superseded by the ``pyproject.toml`` usage, but for maximal
- compatibility, projects may also supply the configuration in
- this older form.
-
- To use ``setuptools_scm`` just modify your project's ``setup.py`` file
- like this:
-
- * Add ``setuptools_scm`` to the ``setup_requires`` parameter.
- * Add the ``use_scm_version`` parameter and set it to ``True``.
-
- For example:
-
- .. code:: python
-
- from setuptools import setup
- setup(
- ...,
- use_scm_version=True,
- setup_requires=['setuptools_scm'],
- ...,
- )
-
- Arguments to ``get_version()`` (see below) may be passed as a dictionary to
- ``use_scm_version``. For example:
-
- .. code:: python
-
- from setuptools import setup
- setup(
- ...,
- use_scm_version = {
- "root": "..",
- "relative_to": __file__,
- "local_scheme": "node-and-timestamp"
- },
- setup_requires=['setuptools_scm'],
- ...,
- )
-
- You can confirm the version number locally via ``setup.py``:
-
- .. code-block:: shell
-
- $ python setup.py --version
-
- .. note::
-
- If you see unusual version numbers for packages but ``python setup.py
- --version`` reports the expected version number, ensure ``[egg_info]`` is
- not defined in ``setup.cfg``.
-
-
- ``setup.cfg`` usage
- -------------------
-
- If using `setuptools 30.3.0
- <https://setuptools.readthedocs.io/en/latest/setuptools.html#configuring-setup-using-setup-cfg-files>`_
- or greater, you can store ``setup_requires`` configuration in ``setup.cfg``.
- However, ``use_scm_version`` must still be placed in ``setup.py``. For example:
-
- .. code:: python
-
- # setup.py
- from setuptools import setup
- setup(
- use_scm_version=True,
- )
-
- .. code:: ini
-
- # setup.cfg
- [metadata]
- ...
-
- [options]
- setup_requires =
- setuptools_scm
- ...
-
- .. important::
-
- Ensure neither the ``[metadata]`` ``version`` option nor the ``[egg_info]``
- section are defined, as these will interfere with ``setuptools_scm``.
-
- You may also need to define a ``pyproject.toml`` file (`PEP-0518
- <https://www.python.org/dev/peps/pep-0518>`_) to ensure you have the required
- version of ``setuptools``:
-
- .. code:: ini
-
- # pyproject.toml
- [build-system]
- requires = ["setuptools>=30.3.0", "wheel", "setuptools_scm"]
-
- For more information, refer to the `setuptools issue #1002
- <https://github.com/pypa/setuptools/issues/1002>`_.
-
-
- Programmatic usage
- ------------------
-
- In order to use ``setuptools_scm`` from code that is one directory deeper
- than the project's root, you can use:
-
- .. code:: python
-
- from setuptools_scm import get_version
- version = get_version(root='..', relative_to=__file__)
-
- See `setup.py Usage`_ above for how to use this within ``setup.py``.
-
-
- Retrieving package version at runtime
- -------------------------------------
-
- If you have opted not to hardcode the version number inside the package,
- you can retrieve it at runtime from PEP-0566_ metadata using
- ``importlib.metadata`` from the standard library
- or the `importlib_metadata`_ backport:
-
- .. code:: python
-
- from importlib.metadata import version, PackageNotFoundError
-
- try:
- __version__ = version(__name__)
- except PackageNotFoundError:
- # package is not installed
- pass
-
- Alternatively, you can use ``pkg_resources`` which is included in
- ``setuptools``:
-
- .. code:: python
-
- from pkg_resources import get_distribution, DistributionNotFound
-
- try:
- __version__ = get_distribution(__name__).version
- except DistributionNotFound:
- # package is not installed
- pass
-
- This does place a runtime dependency on ``setuptools``.
-
- .. _PEP-0566: https://www.python.org/dev/peps/pep-0566/
- .. _importlib_metadata: https://pypi.org/project/importlib-metadata/
-
-
- Usage from Sphinx
- -----------------
-
- It is discouraged to use ``setuptools_scm`` from Sphinx itself,
- instead use ``pkg_resources`` after editable/real installation:
-
- .. code:: python
-
- # contents of docs/conf.py
- from pkg_resources import get_distribution
- release = get_distribution('myproject').version
- # for example take major/minor
- version = '.'.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.
-
- Notable Plugins
- ----------------
-
- `setuptools_scm_git_archive <https://pypi.python.org/pypi/setuptools_scm_git_archive>`_
- Provides partial support for obtaining versions from git archives that
- belong to tagged versions. The only reason for not including it in
- ``setuptools_scm`` itself is Git/GitHub not supporting sufficient metadata
- for untagged/followup commits, which is preventing a consistent UX.
-
-
- 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:
-
- no distance and clean:
- ``{tag}``
- distance and clean:
- ``{next_version}.dev{distance}+{scm letter}{revision hash}``
- no distance and not clean:
- ``{tag}+dYYYYMMDD``
- distance and not clean:
- ``{next_version}.dev{distance}+{scm letter}{revision hash}.dYYYYMMDD``
-
- 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}``.
-
- 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.
-
- .. note::
-
- Future versions of ``setuptools_scm`` will switch to `SemVer
- <http://semver.org/>`_ by default hiding the the old behavior as an
- configurable option.
-
-
- Builtin mechanisms for obtaining version numbers
- --------------------------------------------------
-
- 1. the SCM itself (git/hg)
- 2. ``.hg_archival`` files (mercurial archives)
- 3. ``PKG-INFO``
-
- .. note::
-
- Git archives are not supported due to Git shortcomings
-
-
- File finders hook makes most of MANIFEST.in unnecessary
- -------------------------------------------------------
-
- ``setuptools_scm`` implements a `file_finders
- <https://setuptools.readthedocs.io/en/latest/setuptools.html#adding-support-for-revision-control-systems>`_
- 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 <https://setuptools.readthedocs.io/en/latest/setuptools.html#including-data-files>`_
- 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.
-
-
- Configuration parameters
- ------------------------
-
- In order to configure the way ``use_scm_version`` works you can provide
- a mapping with options instead of a boolean value.
-
- The currently supported configuration keys are:
-
- :root:
- Relative path to cwd, used for finding the SCM root; defaults to ``.``
-
- :version_scheme:
- Configures how the local version number is constructed; either an
- entrypoint name or a callable.
-
- :local_scheme:
- Configures how the local component of the version is constructed; either an
- entrypoint name or a callable.
-
- :write_to:
- 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 `pkg_resources.get_distribution`
- (which adds some overhead).
-
- .. warning::
-
- Only files with :code:`.py` and :code:`.txt` extensions have builtin
- templates, for other file types it is necessary to provide
- :code:`write_to_template`.
-
- :write_to_template:
- A newstyle format string that is given the current version as
- the ``version`` keyword argument for formatting.
-
- :relative_to:
- A file 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:
- 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``
- (see `config.py <src/setuptools_scm/config.py>`_).
-
- :parentdir_prefix_version:
- 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 is 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:
- 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:
- 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`` command.
- Use with caution, this is a function for advanced use, and you should be
- familiar with the ``setuptools_scm`` internals to use it.
-
- Defaults to the value set by ``setuptools_scm.git.DEFAULT_DESCRIBE``
- (see `git.py <src/setuptools_scm/git.py>`_).
-
- To use ``setuptools_scm`` in other Python code you can use the ``get_version``
- function:
-
- .. code:: python
-
- from setuptools_scm import get_version
- my_version = get_version()
-
- It optionally accepts the keys of the ``use_scm_version`` parameter as
- keyword arguments.
-
- Example configuration in ``setup.py`` format:
-
- .. code:: python
-
- from setuptools import setup
-
- setup(
- use_scm_version={
- 'write_to': 'version.py',
- 'write_to_template': '__version__ = "{version}"',
- 'tag_regex': r'^(?P<prefix>v)?(?P<version>[^\+]+)(?P<suffix>.*)?$',
- }
- )
-
- Environment variables
- ---------------------
-
- :SETUPTOOLS_SCM_PRETEND_VERSION:
- when defined and not empty,
- its used as the primary source for the version number
- in which case it will be a unparsed string
-
- :SETUPTOOLS_SCM_DEBUG:
- when defined and not empty,
- a lot of debug information will be printed as part of ``setuptools_scm``
- operating
-
- Extending setuptools_scm
- ------------------------
-
- ``setuptools_scm`` ships with a few ``setuptools`` entrypoints based hooks to
- extend its default capabilities.
-
- 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 use then name of your SCM control directory as name of the entrypoint.
-
- Version number construction
- ~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
- ``setuptools_scm.version_scheme``
- Configures how the version number is constructed given a
- ``setuptools_scm.version.ScmVersion`` instance and should return a string
- representing the version.
-
- Available implementations:
-
- :guess-next-dev: 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`.
- :post-release: generates post release versions (adds :code:`.postN`)
- :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 appends :code:`.devN`. 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) if 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 :code:`.devN`.
-
- ``setuptools_scm.local_scheme``
- Configures how the local part of a version is rendered given a
- ``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: adds the node on dev versions and the date on dirty
- workdir (default)
- :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
-
-
- Importing in ``setup.py``
- ~~~~~~~~~~~~~~~~~~~~~~~~~
-
- To support usage in ``setup.py`` passing a callable into ``use_scm_version``
- is supported.
-
- Within that callable, ``setuptools_scm`` is available for import.
- The callable must return the configuration.
-
-
- .. code:: python
-
- # content of setup.py
- import setuptools
-
- def myversion():
- from setuptools_scm.version import get_local_dirty_tag
- def clean_scheme(version):
- return get_local_dirty_tag(version) if version.dirty else '+clean'
-
- return {'local_scheme': clean_scheme}
-
- setup(
- ...,
- use_scm_version=myversion,
- ...
- )
-
-
- Note on testing non-installed versions
- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
- While the general advice is to test against a installed version,
- some environments require a test prior to install,
-
- .. code::
-
- $ python setup.py egg_info
- $ PYTHONPATH=$PWD:$PWD/src pytest
-
-
- Interaction with Enterprise Distributions
- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
- Some enterprise distributions like RHEL7 and others
- ship rather old setuptools versions due to various release management details.
-
- On such distributions one might observe errors like:
-
- :code:``setuptools_scm.version.SetuptoolsOutdatedWarning: your setuptools is too old (<12)``
-
- In those case its typically possible to build by using a 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.
-
- In case the project you need to build can not be patched to either use old setuptools_scm,
- its still possible to install a more recent version of setuptools in order to handle the build
- and/or install the package by using wheels or eggs.
-
-
-
-
- Code of Conduct
- ---------------
-
- Everyone interacting in the ``setuptools_scm`` project's codebases, issue
- trackers, chat rooms, and mailing lists is expected to follow the
- `PyPA Code of Conduct`_.
-
- .. _PyPA Code of Conduct: https://www.pypa.io/en/latest/code-of-conduct/
-
- Security Contact
- ================
-
- To report a security vulnerability, please use the
- `Tidelift security contact <https://tidelift.com/security>`_.
- Tidelift will coordinate the fix and disclosure.
-
-Platform: UNKNOWN
-Classifier: Development Status :: 5 - Production/Stable
-Classifier: Intended Audience :: Developers
-Classifier: License :: OSI Approved :: MIT License
-Classifier: Programming Language :: Python
-Classifier: Programming Language :: Python :: 2.7
-Classifier: Programming Language :: Python :: 3
-Classifier: Programming Language :: Python :: 3.5
-Classifier: Programming Language :: Python :: 3.6
-Classifier: Programming Language :: Python :: 3.7
-Classifier: Programming Language :: Python :: 3.8
-Classifier: Topic :: Software Development :: Libraries
-Classifier: Topic :: Software Development :: Version Control
-Classifier: Topic :: System :: Software Distribution
-Classifier: Topic :: Utilities
-Requires-Python: !=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7
-Provides-Extra: toml
+++ /dev/null
-.gitignore
-CHANGELOG.rst
-LICENSE
-MANIFEST.in
-README.rst
-pyproject.toml
-setup.cfg
-setup.py
-tox.ini
-.github/FUNDING.yml
-.github/workflows/pre-commit.yml
-.github/workflows/python-tests.yml
-src/setuptools_scm/__init__.py
-src/setuptools_scm/__main__.py
-src/setuptools_scm/config.py
-src/setuptools_scm/discover.py
-src/setuptools_scm/file_finder.py
-src/setuptools_scm/file_finder_git.py
-src/setuptools_scm/file_finder_hg.py
-src/setuptools_scm/git.py
-src/setuptools_scm/hacks.py
-src/setuptools_scm/hg.py
-src/setuptools_scm/integration.py
-src/setuptools_scm/utils.py
-src/setuptools_scm/version.py
-src/setuptools_scm/win_py31_compat.py
-src/setuptools_scm.egg-info/PKG-INFO
-src/setuptools_scm.egg-info/SOURCES.txt
-src/setuptools_scm.egg-info/dependency_links.txt
-src/setuptools_scm.egg-info/entry_points.txt
-src/setuptools_scm.egg-info/requires.txt
-src/setuptools_scm.egg-info/top_level.txt
-src/setuptools_scm.egg-info/zip-safe
-testing/check_self_install.py
-testing/conftest.py
-testing/play_out_381.bash
-testing/test_basic_api.py
-testing/test_config.py
-testing/test_file_finder.py
-testing/test_functions.py
-testing/test_git.py
-testing/test_integration.py
-testing/test_main.py
-testing/test_mercurial.py
-testing/test_regressions.py
-testing/test_setuptools_support.py
-testing/test_version.py
\ No newline at end of file
+++ /dev/null
-[distutils.setup_keywords]
-use_scm_version = setuptools_scm.integration:version_keyword
-
-[setuptools.file_finders]
-setuptools_scm = setuptools_scm.integration:find_files
-
-[setuptools.finalize_distribution_options]
-setuptools_scm = setuptools_scm.integration:infer_version
-
-[setuptools_scm.files_command]
-.git = setuptools_scm.file_finder_git:git_find_files
-.hg = setuptools_scm.file_finder_hg:hg_find_files
-
-[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
-
-[setuptools_scm.parse_scm]
-.git = setuptools_scm.git:parse
-.hg = setuptools_scm.hg:parse
-
-[setuptools_scm.parse_scm_fallback]
-.hg_archival.txt = setuptools_scm.hg:parse_archival
-PKG-INFO = setuptools_scm.hacks:parse_pkginfo
-pip-egg-info = setuptools_scm.hacks:parse_pip_egg_info
-setup.py = setuptools_scm.hacks:fallback_version
-
-[setuptools_scm.version_scheme]
-guess-next-dev = setuptools_scm.version:guess_next_dev_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
-
+++ /dev/null
-setuptools
-
-[toml]
-toml
+++ /dev/null
-setuptools_scm
--- /dev/null
+node: e5986362078130fbefc5e7afb9760ba251c523b0
+node-date: 2023-12-18T16:30:45+09:00
+describe-name: %(describe:tags=true,match=*[0-9]*)
+ref-names: HEAD -> master, origin/master, origin/HEAD
"""
-:copyright: 2010-2015 by Ronny Pfannschmidt
+:copyright: 2010-2023 by Ronny Pfannschmidt
:license: MIT
"""
-import os
-import warnings
-
-from .config import (
- Configuration,
- DEFAULT_VERSION_SCHEME,
- DEFAULT_LOCAL_SCHEME,
- DEFAULT_TAG_REGEX,
-)
-from .utils import function_has_arg, string_types
-from .version import format_version, meta
-from .discover import iter_matching_entrypoints
-
-PRETEND_KEY = "SETUPTOOLS_SCM_PRETEND_VERSION"
-
-TEMPLATES = {
- ".py": """\
-# coding: utf-8
-# file generated by setuptools_scm
-# don't change, don't track in version control
-version = {version!r}
-""",
- ".txt": "{version}",
-}
-
-
-def version_from_scm(root):
- warnings.warn(
- "version_from_scm is deprecated please use get_version",
- category=DeprecationWarning,
- )
- config = Configuration()
- config.root = root
- # TODO: Is it API?
- return _version_from_entrypoints(config)
-
-
-def _call_entrypoint_fn(root, config, fn):
- if function_has_arg(fn, "config"):
- return fn(root, config=config)
- else:
- warnings.warn(
- "parse functions are required to provide a named argument"
- " 'config' in the future.",
- category=PendingDeprecationWarning,
- stacklevel=2,
- )
- return fn(root)
-
-
-def _version_from_entrypoints(config, fallback=False):
- if fallback:
- entrypoint = "setuptools_scm.parse_scm_fallback"
- root = config.fallback_root
- else:
- entrypoint = "setuptools_scm.parse_scm"
- root = config.absolute_root
- for ep in iter_matching_entrypoints(root, entrypoint):
- version = _call_entrypoint_fn(root, config, ep.load())
-
- if version:
- return version
-
-
-def dump_version(root, version, write_to, template=None):
- assert isinstance(version, string_types)
- if not write_to:
- return
- target = os.path.normpath(os.path.join(root, write_to))
- ext = os.path.splitext(target)[1]
- template = template or TEMPLATES.get(ext)
-
- if template is None:
- raise ValueError(
- "bad file format: '{}' (of {}) \nonly *.txt and *.py are supported".format(
- os.path.splitext(target)[1], target
- )
- )
- with open(target, "w") as fp:
- fp.write(template.format(version=version))
-
-
-def _do_parse(config):
- pretended = os.environ.get(PRETEND_KEY)
- if pretended:
- # we use meta here since the pretended version
- # must adhere to the pep to begin with
- return meta(tag=pretended, preformatted=True, config=config)
-
- if config.parse:
- parse_result = _call_entrypoint_fn(config.absolute_root, config, config.parse)
- if isinstance(parse_result, string_types):
- raise TypeError(
- "version parse result was a string\nplease return a parsed version"
- )
- version = parse_result or _version_from_entrypoints(config, fallback=True)
- else:
- # include fallbacks after dropping them from the main entrypoint
- version = _version_from_entrypoints(config) or _version_from_entrypoints(
- config, fallback=True
- )
-
- if version:
- return version
-
- raise LookupError(
- "setuptools-scm was unable to detect version for %r.\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" % config.absolute_root
- )
-
-
-def get_version(
- root=".",
- version_scheme=DEFAULT_VERSION_SCHEME,
- local_scheme=DEFAULT_LOCAL_SCHEME,
- write_to=None,
- write_to_template=None,
- relative_to=None,
- tag_regex=DEFAULT_TAG_REGEX,
- parentdir_prefix_version=None,
- fallback_version=None,
- fallback_root=".",
- parse=None,
- git_describe_command=None,
-):
- """
- 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__``.
- """
-
- config = Configuration(**locals())
- return _get_version(config)
-
-
-def _get_version(config):
- parsed_version = _do_parse(config)
-
- if parsed_version:
- version_string = format_version(
- parsed_version,
- version_scheme=config.version_scheme,
- local_scheme=config.local_scheme,
- )
- dump_version(
- root=config.root,
- version=version_string,
- write_to=config.write_to,
- template=config.write_to_template,
- )
-
- return version_string
+from __future__ import annotations
+
+from ._config import Configuration
+from ._config import DEFAULT_LOCAL_SCHEME # soft deprecated
+from ._config import DEFAULT_VERSION_SCHEME # soft deprecated
+from ._get_version_impl import _get_version # soft deprecated
+from ._get_version_impl import get_version # soft deprecated
+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__ = [
+ # soft deprecated imports, left for backward compatibility
+ "get_version",
+ "_get_version",
+ "dump_version",
+ "DEFAULT_VERSION_SCHEME",
+ "DEFAULT_LOCAL_SCHEME",
+ "Configuration",
+ "Version",
+ "ScmVersion",
+ "NonNormalizedVersion",
+]
-from __future__ import print_function
-import sys
-from setuptools_scm import get_version
-from setuptools_scm.integration import find_files
-from setuptools_scm.version import _warn_if_setuptools_outdated
-
-
-def main():
- _warn_if_setuptools_outdated()
- print("Guessed Version", get_version())
- if "ls" in sys.argv:
- for fname in find_files("."):
- print(fname)
+from __future__ import annotations
+from ._cli import main
if __name__ == "__main__":
main()
--- /dev/null
+from __future__ import annotations
+
+import argparse
+import os
+import sys
+
+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) -> None:
+ 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(inferred_root)
+
+ version = _get_version(config, force_write_version_files=False)
+ if version is None:
+ raise SystemExit("ERROR: no version found for", opts)
+ if opts.strip_dev:
+ version = version.partition(".dev")[0]
+ print(version)
+
+ if opts.command == "ls":
+ for fname in find_files(config.root):
+ print(fname)
+
+
+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",
+ )
+ sub = parser.add_subparsers(title="extra commands", dest="command", metavar="")
+ # We avoid `metavar` to prevent printing repetitive information
+ desc = "List files managed by the SCM"
+ sub.add_parser("ls", help=desc[0].lower() + desc[1:], description=desc)
+ return parser.parse_args(args)
+
+
+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
--- /dev/null
+""" 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 _validate_version_cls
+from ._version_cls import _VersionT
+from ._version_cls import Version as _Version
+
+log = _log.log.getChild("config")
+
+DEFAULT_TAG_REGEX = re.compile(
+ r"^(?:[\w-]+-)?(?P<version>[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,
+ version_cls=version_cls,
+ tag_regex=tag_regex,
+ **data,
+ )
--- /dev/null
+from __future__ import annotations
+
+import sys
+from typing import Any
+from typing import Callable
+from typing import cast
+from typing import Iterator
+from typing import overload
+from typing import TYPE_CHECKING
+
+from . import _log
+from . import version
+
+if TYPE_CHECKING:
+ from . import _types as _t
+ from ._config import Configuration, 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 entry_points, EntryPoints
+
+
+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()
+ else:
+ 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
--- /dev/null
+from __future__ import annotations
+
+import itertools
+import os
+from typing import Callable
+from typing import TYPE_CHECKING
+
+from .. import _log
+from .. import _types as _t
+from .._entrypoints import iter_entry_points
+
+if TYPE_CHECKING:
+ 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: http://setuptools.readthedocs.io/en/latest/setuptools.html#\
+ adding-support-for-revision-control-systems
+ """
+ realpath = os.path.normcase(os.path.realpath(path))
+ seen: set[str] = set()
+ res: list[str] = []
+ for dirpath, dirnames, filenames in os.walk(realpath, followlinks=True):
+ # dirpath with symlinks resolved
+ realdirpath = os.path.normcase(os.path.realpath(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 = os.path.normcase(os.path.realpath(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 []
--- /dev/null
+from __future__ import annotations
+
+import logging
+import os
+import subprocess
+import tarfile
+from typing import IO
+
+from . import is_toplevel_acceptable
+from . import scm_find_files
+from .. import _types as _t
+from .._run_cmd import run as _run
+from ..integration import data_from_mime
+
+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 os.path.normcase(os.path.realpath(out.strip()))
+ 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"]
+ 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 = os.path.abspath(os.path.normpath(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)
--- /dev/null
+from __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
+
+log = logging.getLogger(__name__)
+
+
+def _hg_toplevel(path: str) -> str | None:
+ try:
+ res = _run(
+ ["hg", "root"],
+ cwd=(path or "."),
+ )
+ res.check_returncode()
+ return os.path.normcase(os.path.realpath(res.stdout))
+ 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)
--- /dev/null
+from __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 format_version as _format_version
+from .version import ScmVersion
+
+_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"
+ )
+
+
+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(
+ DeprecationWarning(
+ "empty regex for tag regex is invalid, using default"
+ )
+ )
+ return _config.DEFAULT_TAG_REGEX
+ else:
+ return re.compile(tag_regex)
+ else:
+ return tag_regex
--- /dev/null
+from __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
+TYPE_CHECKING = False
+if TYPE_CHECKING:
+ from typing import Tuple, 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")
--- /dev/null
+from __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 read_toml_content
+from .toml import TOML_RESULT
+
+
+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)
+ 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}
--- /dev/null
+from __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, _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.warning(e)
+ else:
+ _assign_version(dist, config)
--- /dev/null
+from __future__ import annotations
+
+import sys
+from pathlib import Path
+from typing import Any
+from typing import Callable
+from typing import cast
+from typing import Dict
+from typing import TYPE_CHECKING
+from typing import TypedDict
+
+if sys.version_info >= (3, 11):
+ from tomllib import loads as load_toml
+else:
+ from tomli import loads as load_toml
+
+if TYPE_CHECKING:
+ 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)
--- /dev/null
+"""
+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 # type: ignore [override]
+ 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.WARN 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)
--- /dev/null
+from __future__ import annotations
+
+import re
+
+from . import _types as _t
+
+
+def strip_local(version_string: str) -> str:
+ public, sep, local = version_string.partition("+")
+ 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)
--- /dev/null
+from __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)
--- /dev/null
+from __future__ import annotations
+
+import os
+import shlex
+import subprocess
+import textwrap
+import warnings
+from typing import Callable
+from typing import Final
+from typing import Mapping
+from typing import overload
+from typing import Sequence
+from typing import TYPE_CHECKING
+from typing import TypeVar
+
+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
+
+BROKEN_TIMEOUT: Final[int] = 40
+
+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_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 "pip-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 = BROKEN_TIMEOUT,
+ 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)
+ 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,
+ 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=".", timeout=BROKEN_TIMEOUT)
+ if p.returncode != 0:
+ log.error(f"Command '{name}' returned non-zero. This is stderr:")
+ 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("%r was not found" % name, 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)
--- /dev/null
+from __future__ import annotations
+
+import os
+from typing import Callable
+from typing import List
+from typing import Sequence
+from typing import Tuple
+from typing import TYPE_CHECKING
+from typing import Union
+
+
+if TYPE_CHECKING:
+ 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"
--- /dev/null
+from __future__ import annotations
+
+from typing import cast
+from typing import Type
+from typing import Union
+
+try:
+ from packaging.version import InvalidVersion
+ from packaging.version import Version as Version
+except ImportError:
+ from setuptools.extern.packaging.version import InvalidVersion # type: ignore
+ from setuptools.extern.packaging.version import Version as Version # type: ignore
+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"<NonNormalizedVersion({self._raw_version!r})>"
+
+
+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.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
+ else:
+ # Use `version_cls` if provided, default to packaging or pkg_resources
+ if version_cls is None:
+ return Version
+ elif isinstance(version_cls, str):
+ try:
+ return cast(Type[_VersionT], import_name(version_cls))
+ except: # noqa
+ raise ValueError(
+ f"Unable to import version_cls='{version_cls}'"
+ ) from None
+ else:
+ return version_cls
+++ /dev/null
-""" configuration """
-from __future__ import print_function, unicode_literals
-import os
-import re
-import warnings
-
-from .utils import trace
-
-DEFAULT_TAG_REGEX = r"^(?:[\w-]+-)?(?P<version>[vV]?\d+(?:\.\d+){0,2}[^\+]*)(?:\+.*)?$"
-DEFAULT_VERSION_SCHEME = "guess-next-dev"
-DEFAULT_LOCAL_SCHEME = "node-and-date"
-
-
-def _check_tag_regex(value):
- if not value:
- value = DEFAULT_TAG_REGEX
- 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
-
-
-def _check_absolute_root(root, relative_to):
- if relative_to:
- if os.path.isabs(root) and not root.startswith(relative_to):
- warnings.warn(
- "absolute root path '%s' overrides relative_to '%s'"
- % (root, relative_to)
- )
- root = os.path.join(os.path.dirname(relative_to), root)
- return os.path.abspath(root)
-
-
-class Configuration(object):
- """ Global configuration model """
-
- def __init__(
- self,
- relative_to=None,
- root=".",
- version_scheme=DEFAULT_VERSION_SCHEME,
- local_scheme=DEFAULT_LOCAL_SCHEME,
- write_to=None,
- write_to_template=None,
- tag_regex=DEFAULT_TAG_REGEX,
- parentdir_prefix_version=None,
- fallback_version=None,
- fallback_root=".",
- parse=None,
- git_describe_command=None,
- ):
- # TODO:
- self._relative_to = relative_to
- self._root = "."
-
- self.root = root
- self.version_scheme = version_scheme
- self.local_scheme = local_scheme
- self.write_to = write_to
- self.write_to_template = write_to_template
- self.parentdir_prefix_version = parentdir_prefix_version
- self.fallback_version = fallback_version
- self.fallback_root = fallback_root
- self.parse = parse
- self.tag_regex = tag_regex
- self.git_describe_command = git_describe_command
-
- @property
- def fallback_root(self):
- return self._fallback_root
-
- @fallback_root.setter
- def fallback_root(self, value):
- self._fallback_root = os.path.abspath(value)
-
- @property
- def absolute_root(self):
- return self._absolute_root
-
- @property
- def relative_to(self):
- return self._relative_to
-
- @relative_to.setter
- def relative_to(self, value):
- self._absolute_root = _check_absolute_root(self._root, value)
- self._relative_to = value
- trace("root", repr(self._absolute_root))
-
- @property
- def root(self):
- return self._root
-
- @root.setter
- def root(self, value):
- self._absolute_root = _check_absolute_root(value, self._relative_to)
- self._root = value
- trace("root", repr(self._absolute_root))
-
- @property
- def tag_regex(self):
- return self._tag_regex
-
- @tag_regex.setter
- def tag_regex(self, value):
- self._tag_regex = _check_tag_regex(value)
-
- @classmethod
- def from_file(cls, name="pyproject.toml"):
- """
- 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.
- """
- with open(name) as strm:
- defn = __import__("toml").load(strm)
- section = defn.get("tool", {})["setuptools_scm"]
- return cls(**section)
+from __future__ import annotations
+
import os
-from pkg_resources import iter_entry_points
-from .utils import trace
-
-
-def iter_matching_entrypoints(path, entrypoint):
- trace("looking for ep", entrypoint, path)
- for ep in iter_entry_points(entrypoint):
- if os.path.exists(os.path.join(path, ep.name)):
- if os.path.isabs(ep.name):
- trace("ignoring bad ep", ep)
- trace("found ep", ep)
- yield ep
+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
--- /dev/null
+from __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 meta
+from .version import ScmVersion
+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
+++ /dev/null
-import os
-
-
-def scm_find_files(path, scm_files, scm_dirs):
- """ 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)
-
- scm_files and scm_dirs must be absolute with symlinks resolved (realpath),
- with normalized case (normcase)
-
- Spec here: http://setuptools.readthedocs.io/en/latest/setuptools.html#\
- adding-support-for-revision-control-systems
- """
- realpath = os.path.normcase(os.path.realpath(path))
- seen = set()
- res = []
- for dirpath, dirnames, filenames in os.walk(realpath, followlinks=True):
- # dirpath with symlinks resolved
- realdirpath = os.path.normcase(os.path.realpath(dirpath))
-
- def _link_not_in_scm(n):
- fn = os.path.join(realdirpath, os.path.normcase(n))
- return os.path.islink(fn) and fn not in scm_files
-
- if 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 not _link_not_in_scm(dn)]
- for filename in filenames:
- if _link_not_in_scm(filename):
- continue
- # dirpath + filename with symlinks preserved
- fullfilename = os.path.join(dirpath, filename)
- if os.path.normcase(os.path.realpath(fullfilename)) in scm_files:
- res.append(os.path.join(path, os.path.relpath(fullfilename, realpath)))
- seen.add(realdirpath)
- return res
+++ /dev/null
-import os
-import subprocess
-import tarfile
-import logging
-from .file_finder import scm_find_files
-from .utils import trace
-
-log = logging.getLogger(__name__)
-
-
-def _git_toplevel(path):
- try:
- with open(os.devnull, "wb") as devnull:
- out = subprocess.check_output(
- ["git", "rev-parse", "--show-toplevel"],
- cwd=(path or "."),
- universal_newlines=True,
- stderr=devnull,
- )
- trace("find files toplevel", out)
- return os.path.normcase(os.path.realpath(out.strip()))
- 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, toplevel):
- 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):
- # use git archive instead of git ls-file to honor
- # export-ignore git attribute
- cmd = ["git", "archive", "--prefix", toplevel + os.path.sep, "HEAD"]
- proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, cwd=toplevel)
- 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.exception("listing git files failed - pretending there aren't any")
- return (), ()
-
-
-def git_find_files(path=""):
- toplevel = _git_toplevel(path)
- if not toplevel:
- return []
- fullpath = os.path.abspath(os.path.normpath(path))
- if not fullpath.startswith(toplevel):
- trace("toplevel mismatch", toplevel, fullpath)
- git_files, git_dirs = _git_ls_files_and_dirs(toplevel)
- return scm_find_files(path, git_files, git_dirs)
+++ /dev/null
-import os
-import subprocess
-
-from .file_finder import scm_find_files
-
-
-def _hg_toplevel(path):
- try:
- with open(os.devnull, "wb") as devnull:
- out = subprocess.check_output(
- ["hg", "root"],
- cwd=(path or "."),
- universal_newlines=True,
- stderr=devnull,
- )
- return os.path.normcase(os.path.realpath(out.strip()))
- 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):
- hg_files = set()
- hg_dirs = {toplevel}
- out = subprocess.check_output(
- ["hg", "files"], cwd=toplevel, universal_newlines=True
- )
- for name in out.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=""):
- toplevel = _hg_toplevel(path)
- if not toplevel:
- return []
- hg_files, hg_dirs = _hg_ls_files_and_dirs(toplevel)
- return scm_find_files(path, hg_files, hg_dirs)
-from .config import Configuration
-from .utils import do_ex, trace, has_command
-from .version import meta
+from __future__ import annotations
-from os.path import isfile, join
+import dataclasses
+import logging
+import os
+import re
+import shlex
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 Callable
+from typing import Sequence
+from typing import TYPE_CHECKING
+
+from . import _types as _t
+from . import Configuration
+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 meta
+from .version import ScmVersion
+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 = 20,
+) -> _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
-try:
- from os.path import samefile
-except ImportError:
- from .win_py31_compat import samefile
-
-
-DEFAULT_DESCRIBE = "git describe --dirty --tags --long --match *.*"
-
-
-class GitWorkdir(object):
- """experimental, may change at any time"""
+ 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
- def __init__(self, path):
- self.path = path
+ return cls(Path(real_wd))
- def do_ex(self, cmd):
- return do_ex(cmd, cwd=self.path)
+ def is_dirty(self) -> bool:
+ return run_git(
+ ["status", "--porcelain", "--untracked-files=no"], self.path
+ ).parse_success(
+ parse=bool,
+ default=False,
+ )
- @classmethod
- def from_potential_worktree(cls, wd):
- real_wd, _, ret = do_ex("git rev-parse --show-toplevel", wd)
- if ret:
- return
- trace("real root", real_wd)
- if not samefile(real_wd, wd):
- return
+ 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)",
+ )
- return cls(real_wd)
+ 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
+ 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_dirty(self):
- out, _, _ = self.do_ex("git status --porcelain --untracked-files=no")
- return bool(out)
+ def is_shallow(self) -> bool:
+ return self.path.joinpath(".git/shallow").is_file()
- def get_branch(self):
- branch, err, ret = self.do_ex("git rev-parse --abbrev-ref HEAD")
- if ret:
- trace("branch err", branch, err, ret)
- return
- return branch
+ def fetch_shallow(self) -> None:
+ run_git(["fetch", "--unshallow"], self.path, check=True, timeout=240)
- def is_shallow(self):
- return isfile(join(self.path, ".git/shallow"))
+ def node(self) -> str | None:
+ def _unsafe_short_node(node: str) -> str:
+ return node[:7]
- def fetch_shallow(self):
- self.do_ex("git fetch --unshallow")
+ return run_git(
+ ["rev-parse", "--verify", "--quiet", "HEAD"], self.path
+ ).parse_success(
+ parse=_unsafe_short_node,
+ )
- def node(self):
- rev_node, _, ret = self.do_ex("git rev-parse --verify --quiet HEAD")
- if not ret:
- return rev_node[:7]
+ def count_all_nodes(self) -> int:
+ res = run_git(["rev-list", "HEAD"], self.path)
+ return res.stdout.count("\n") + 1
- def count_all_nodes(self):
- revs, _, _ = self.do_ex("git rev-list HEAD")
- return revs.count("\n") + 1
+ def default_describe(self) -> _CompletedProcess:
+ return run_git(DEFAULT_DESCRIBE[1:], self.path)
-def warn_on_shallow(wd):
+def warn_on_shallow(wd: GitWorkdir) -> None:
"""experimental, may change at any time"""
if wd.is_shallow():
- warnings.warn('"{}" is shallow and may cause errors'.format(wd.path))
+ warnings.warn(f'"{wd.path}" is shallow and may cause errors')
-def fetch_on_shallow(wd):
+def fetch_on_shallow(wd: GitWorkdir) -> None:
"""experimental, may change at any time"""
if wd.is_shallow():
- warnings.warn('"%s" was shallow, git fetch was used to rectify')
+ warnings.warn(f'"{wd.path}" was shallow, git fetch was used to rectify')
wd.fetch_shallow()
-def fail_on_shallow(wd):
+def fail_on_shallow(wd: GitWorkdir) -> None:
"""experimental, may change at any time"""
if wd.is_shallow():
raise ValueError(
- "%r is shallow, please correct with " '"git fetch --unshallow"' % wd.path
+ 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, describe_command=DEFAULT_DESCRIBE, pre_parse=warn_on_shallow, config=None
-):
+ 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
"""
- if not config:
- config = Configuration(root=root)
+ _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
- if not has_command("git"):
- return
- wd = GitWorkdir.from_potential_worktree(config.absolute_root)
- if wd is None:
- return
- if pre_parse:
- pre_parse(wd)
+def version_from_describe(
+ wd: GitWorkdir | hg_git.GitWorkdirHgClient,
+ config: Configuration,
+ describe_command: _t.CMD_TYPE | None,
+) -> ScmVersion | None:
+ pass
- if config.git_describe_command:
+ if config.git_describe_command is not None:
describe_command = config.git_describe_command
- out, unused_err, ret = wd.do_ex(describe_command)
- if ret:
- # If 'git git_describe_command' failed, try to get the information otherwise.
- rev_node = wd.node()
- dirty = wd.is_dirty()
+ 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()
- if rev_node is None:
- return meta("0.0", distance=0, dirty=dirty, config=config)
+ 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 meta(
- "0.0",
- distance=wd.count_all_nodes(),
- node="g" + rev_node,
- dirty=dirty,
- branch=wd.get_branch(),
- config=config,
- )
- else:
- tag, number, node, dirty = _git_parse_describe(out)
-
- branch = wd.get_branch()
- if number:
- return meta(
- tag,
- config=config,
- distance=number,
- node=node,
- dirty=dirty,
- branch=branch,
- )
+ return describe_res.parse_success(parse=parse_describe)
+
+
+def _git_parse_inner(
+ config: Configuration,
+ wd: GitWorkdir | hg_git.GitWorkdirHgClient,
+ pre_parse: None | (Callable[[GitWorkdir | hg_git.GitWorkdirHgClient], 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:
- return meta(tag, config=config, node=node, dirty=dirty, branch=branch)
+ 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):
+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
else:
dirty = False
- tag, number, node = describe_output.rsplit("-", 2)
- number = int(number)
+ 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)
+ else:
+ 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)
+++ /dev/null
-import os
-from .utils import data_from_mime, trace
-from .version import tag_to_version, meta
-
-
-def parse_pkginfo(root, config=None):
-
- pkginfo = os.path.join(root, "PKG-INFO")
- trace("pkginfo", pkginfo)
- data = data_from_mime(pkginfo)
- version = data.get("Version")
- if version != "UNKNOWN":
- return meta(version, preformatted=True, config=config)
-
-
-def parse_pip_egg_info(root, config=None):
- pipdir = os.path.join(root, "pip-egg-info")
- if not os.path.isdir(pipdir):
- return
- items = os.listdir(pipdir)
- trace("pip-egg-info", pipdir, items)
- if not items:
- return
- return parse_pkginfo(os.path.join(pipdir, items[0]), config=config)
-
-
-def fallback_version(root, config=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:
- return meta(config.fallback_version, preformatted=True, config=config)
+from __future__ import annotations
+
+import datetime
+import logging
import os
-from .config import Configuration
-from .utils import do, trace, data_from_mime, has_command
-from .version import meta, tags_to_versions
-
-
-def _hg_tagdist_normalize_tagcommit(config, tag, dist, node, branch):
- dirty = node.endswith("+")
- node = "h" + node.strip("+")
-
- # Detect changes since the specified tag
- revset = (
- "(branch(.)" # look for revisions in this branch only
- " and tag({tag!r})::." # after the last tag
- # ignore commits that only modify .hgtags and nothing else:
- " and (merge() or file('re:^(?!\\.hgtags).*$'))"
- " and not tag({tag!r}))" # ignore the tagged commit itself
- ).format(tag=tag)
- if tag != "0.0":
- commits = do(
- ["hg", "log", "-r", revset, "--template", "{node|short}"],
- config.absolute_root,
- )
- else:
- commits = True
- trace("normalize", locals())
- if commits or dirty:
- return meta(
- tag, distance=dist, node=node, dirty=dirty, branch=branch, config=config
+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 meta
+from .version import ScmVersion
+from .version import tag_to_version
+
+if TYPE_CHECKING:
+ from . import _types as _t
+
+from ._run_cmd import run as _run, require_command as _require_command
+
+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
+ bookmark: str
+ node_date_str: str
+ node, tags_str, bookmark, node_date_str = self.hg_log(
+ ".", "{node}\n{tag}\n{bookmark}\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 as e:
+ log.exception("error %s", e)
+ pass # unpacking failed, old hg
+
+ 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
)
- else:
- return meta(tag, config=config)
-
-
-def parse(root, config=None):
- if not config:
- config = Configuration(root=root)
-
- if not has_command("hg"):
- return
- identity_data = do("hg id -i -b -t", config.absolute_root).split()
- if not identity_data:
- return
- node = identity_data.pop(0)
- branch = identity_data.pop(0)
- if "tip" in identity_data:
- # tip is not a real tag
- identity_data.remove("tip")
- tags = tags_to_versions(identity_data)
- dirty = node[-1] == "+"
- if tags:
- return meta(tags[0], dirty=dirty, branch=branch, config=config)
-
- if node.strip("+") == "0" * 12:
- trace("initial node", config.absolute_root)
- return meta("0.0", config=config, dirty=dirty, branch=branch)
-
- try:
- tag = get_latest_normalizable_tag(config.absolute_root)
- dist = get_graph_distance(config.absolute_root, tag)
- if tag == "null":
- tag = "0.0"
- dist = int(dist) + 1
- return _hg_tagdist_normalize_tagcommit(config, tag, dist, node, branch)
- except ValueError:
- pass # unpacking failed, old hg
-
-
-def get_latest_normalizable_tag(root):
- # Gets all tags containing a '.' (see #229) from oldest to newest
- cmd = [
- "hg",
- "log",
- "-r",
- "ancestors(.) and tag('re:\\.')",
- "--template",
- "{tags}\n",
- ]
- outlines = do(cmd, root).split()
- if not outlines:
- return "null"
- tag = outlines[-1].split()[-1]
- return tag
-
-
-def get_graph_distance(root, rev1, rev2="."):
- cmd = ["hg", "log", "-q", "-r", "{}::{}".format(rev1, rev2)]
- out = do(cmd, root)
- return len(out.strip().splitlines()) - 1
-
-
-def archival_to_version(data, config=None):
- trace("data", data)
+
+ 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
elif "latesttag" in data:
return meta(
data["latesttag"],
- distance=data["latesttagdistance"],
+ distance=int(data["latesttagdistance"]),
node=node,
config=config,
)
else:
- return meta("0.0", node=node, config=config)
+ return meta(config.version_cls("0.0"), node=node, config=config)
-def parse_archival(root, config=None):
+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)
--- /dev/null
+from __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="",
+ )
-from pkg_resources import iter_entry_points
-
-from .version import _warn_if_setuptools_outdated
-from .utils import do, trace_exception
-from . import _get_version, Configuration
-
-
-def version_keyword(dist, keyword, value):
- _warn_if_setuptools_outdated()
- if not value:
- return
- if value is True:
- value = {}
- if getattr(value, "__call__", None):
- value = value()
- config = Configuration(**value)
- dist.metadata.version = _get_version(config)
-
-
-def find_files(path=""):
- for ep in iter_entry_points("setuptools_scm.files_command"):
- command = ep.load()
- if isinstance(command, str):
- # this technique is deprecated
- res = do(ep.load(), path or ".").splitlines()
- else:
- res = command(path)
- if res:
- return res
- return []
-
-
-def _args_from_toml(name="pyproject.toml"):
- # todo: more sensible config initialization
- # move this elper back to config and unify it with the code from get_config
-
- with open(name) as strm:
- defn = __import__("toml").load(strm)
- return defn.get("tool", {})["setuptools_scm"]
-
-
-def infer_version(dist):
-
- try:
- config = Configuration.from_file()
- except Exception:
- return trace_exception()
- dist.metadata.version = _get_version(config)
+from __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: None | str = 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
--- /dev/null
+from __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)
+++ /dev/null
-"""
-utils
-"""
-from __future__ import print_function, unicode_literals
-import inspect
-import warnings
-import sys
-import shlex
-import subprocess
-import os
-import io
-import platform
-import traceback
-import datetime
-
-
-DEBUG = bool(os.environ.get("SETUPTOOLS_SCM_DEBUG"))
-IS_WINDOWS = platform.system() == "Windows"
-PY2 = sys.version_info < (3,)
-PY3 = sys.version_info > (3,)
-string_types = (str,) if PY3 else (str, unicode) # noqa
-
-
-def no_git_env(env):
- # 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_"):
- trace(k, v)
- return {
- k: v
- for k, v in env.items()
- if not k.startswith("GIT_")
- or k in ("GIT_EXEC_PATH", "GIT_SSH", "GIT_SSH_COMMAND")
- }
-
-
-def trace(*k):
- if DEBUG:
- print(*k)
- sys.stdout.flush()
-
-
-def trace_exception():
- DEBUG and traceback.print_exc()
-
-
-def ensure_stripped_str(str_or_bytes):
- if isinstance(str_or_bytes, str):
- return str_or_bytes.strip()
- else:
- return str_or_bytes.decode("utf-8", "surrogateescape").strip()
-
-
-def _always_strings(env_dict):
- """
- On Windows and Python 2, environment dictionaries must be strings
- and not unicode.
- """
- if IS_WINDOWS or PY2:
- env_dict.update((key, str(value)) for (key, value) in env_dict.items())
- return env_dict
-
-
-def _popen_pipes(cmd, cwd):
- return subprocess.Popen(
- cmd,
- stdout=subprocess.PIPE,
- stderr=subprocess.PIPE,
- cwd=str(cwd),
- env=_always_strings(
- dict(
- no_git_env(os.environ),
- # os.environ,
- # try to disable i18n
- LC_ALL="C",
- LANGUAGE="",
- HGPLAIN="1",
- )
- ),
- )
-
-
-def do_ex(cmd, cwd="."):
- trace("cmd", repr(cmd))
- if os.name == "posix" and not isinstance(cmd, (list, tuple)):
- cmd = shlex.split(cmd)
-
- p = _popen_pipes(cmd, cwd)
- out, err = p.communicate()
- if out:
- trace("out", repr(out))
- if err:
- trace("err", repr(err))
- if p.returncode:
- trace("ret", p.returncode)
- return ensure_stripped_str(out), ensure_stripped_str(err), p.returncode
-
-
-def do(cmd, cwd="."):
- out, err, ret = do_ex(cmd, cwd)
- if ret:
- print(err)
- return out
-
-
-def data_from_mime(path):
- with io.open(path, encoding="utf-8") as fp:
- content = fp.read()
- trace("content", repr(content))
- # the complex conditions come from reading pseudo-mime-messages
- data = dict(x.split(": ", 1) for x in content.splitlines() if ": " in x)
- trace("data", data)
- return data
-
-
-class UTC(datetime.tzinfo):
- _ZERO = datetime.timedelta(0)
-
- def utcoffset(self, dt):
- return self._ZERO
-
- def tzname(self, dt):
- return "UTC"
-
- def dst(self, dt):
- return self._ZERO
-
-
-utc = UTC()
-
-
-def function_has_arg(fn, argname):
- assert inspect.isfunction(fn)
-
- if PY2:
- argspec = inspect.getargspec(fn).args
- else:
-
- argspec = inspect.signature(fn).parameters
-
- return argname in argspec
-
-
-def has_command(name):
- try:
- p = _popen_pipes([name, "help"], ".")
- except OSError:
- trace(*sys.exc_info())
- res = False
- else:
- p.communicate()
- res = not p.returncode
- if not res:
- warnings.warn("%r was not found" % name)
- return res
-from __future__ import print_function
-import datetime
-import warnings
-import re
-
-from .config import Configuration
-from .utils import trace, string_types, utc
+from __future__ import annotations
-from pkg_resources import iter_entry_points
-
-from pkg_resources import parse_version as pkg_parse_version
+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 Any
+from typing import Callable
+from typing import Match
+from typing import TYPE_CHECKING
-SEMVER_MINOR = 2
-SEMVER_PATCH = 3
-SEMVER_LEN = 3
+from . import _entrypoints
+from . import _modify_version
+if TYPE_CHECKING:
+ from typing_extensions import Concatenate
+ from typing_extensions import ParamSpec
-def _parse_version_tag(tag, config):
- tagstring = tag if not isinstance(tag, string_types) else str(tag)
- match = config.tag_regex.match(tagstring)
+ _P = ParamSpec("_P")
- result = None
- if match:
- if len(match.groups()) == 1:
- key = 1
- else:
- key = "version"
+from typing import TypedDict
- result = {
- "version": match.group(key),
- "prefix": match.group(0)[: match.start(key)],
- "suffix": match.group(0)[match.end(key) :],
- }
- trace("tag '{}' parsed to {}".format(tag, result))
- return result
+from ._version_cls import Version as PkgVersion, _VersionT
+from . import _version_cls as _v
+from . import _config
+log = logging.getLogger(__name__)
-def _get_version_class():
- modern_version = pkg_parse_version("1.0")
- if isinstance(modern_version, tuple):
- return None
- else:
- return type(modern_version)
+SEMVER_MINOR = 2
+SEMVER_PATCH = 3
+SEMVER_LEN = 3
-VERSION_CLASS = _get_version_class()
+class _TagDict(TypedDict):
+ version: str
+ prefix: str
+ suffix: str
-class SetuptoolsOutdatedWarning(Warning):
- pass
+def _parse_version_tag(
+ tag: str | object, config: _config.Configuration
+) -> _TagDict | None:
+ match = config.tag_regex.match(str(tag))
-# append so integrators can disable the warning
-warnings.simplefilter("error", SetuptoolsOutdatedWarning, append=True)
+ 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)
-def _warn_if_setuptools_outdated():
- if VERSION_CLASS is None:
- warnings.warn("your setuptools is too old (<12)", SetuptoolsOutdatedWarning)
+ return None
-def callable_or_entrypoint(group, callable_or_name):
- trace("ep", (group, callable_or_name))
+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):
- trace("ep found:", ep.name)
+ log.debug("ep found: %s", ep.name)
return ep.load()
-def tag_to_version(tag, config=None):
+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
- :param config: optional configuration object
"""
- trace("tag", tag)
+ log.debug("tag %s", tag)
- if not config:
- config = Configuration()
-
- tagdict = _parse_version_tag(tag, config)
- if not isinstance(tagdict, dict) or not tagdict.get("version", None):
- warnings.warn("tag {!r} no version found".format(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 = tagdict["version"]
- trace("version pre parse", version)
+ version_str = tag_dict["version"]
+ log.debug("version pre parse %s", version_str)
- if tagdict.get("suffix", ""):
- warnings.warn(
- "tag {!r} will be stripped of its suffix '{}'".format(
- tag, tagdict["suffix"]
- )
- )
+ if suffix := tag_dict.get("suffix", ""):
+ warnings.warn(f"tag {tag!r} will be stripped of its suffix {suffix!r}")
- if VERSION_CLASS is not None:
- version = pkg_parse_version(version)
- trace("version", repr(version))
+ version: _VersionT = config.version_cls(version_str)
+ log.debug("version=%r", version)
return version
-def tags_to_versions(tags, config=None):
- """
- take tags that might be prefixed with a keyword and return only the version part
- :param tags: an iterable of tags
- :param config: optional configuration object
+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`
"""
- result = []
- for tag in tags:
- tag = tag_to_version(tag, config=config)
- if tag:
- result.append(tag)
- return result
-
-
-class ScmVersion(object):
- def __init__(
- self,
- tag_version,
- distance=None,
- node=None,
- dirty=False,
- preformatted=False,
- branch=None,
- config=None,
- **kw
- ):
- if kw:
- trace("unknown args", kw)
- self.tag = tag_version
- if dirty and distance is None:
- distance = 0
- self.distance = distance
- self.node = node
- self.time = datetime.datetime.now(utc)
- self._extra = kw
- self.dirty = dirty
- self.preformatted = preformatted
- self.branch = branch
- self.config = config
@property
- def extra(self):
- warnings.warn(
- "ScmVersion.extra is deprecated and will be removed in future",
- category=DeprecationWarning,
- stacklevel=2,
+ 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"<ScmVersion {self.tag} dist={self.distance} "
+ f"node={self.node} dirty={self.dirty} branch={self.branch}>"
)
- return self._extra
- @property
- def exact(self):
- return self.distance is None
-
- def __repr__(self):
- return self.format_with(
- "<ScmVersion {tag} d={distance} n={node} d={dirty} b={branch}>"
- )
-
- def format_with(self, fmt, **kw):
+ 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,
node=self.node,
dirty=self.dirty,
branch=self.branch,
- **kw
+ node_date=self.node_date,
+ **kw,
)
- def format_choice(self, clean_format, dirty_format, **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, fmt="{guessed}.dev{distance}", **kw):
- guessed = guess_next(self.tag, **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, preformatted, config):
+def _parse_tag(
+ tag: _VersionT | str, preformatted: bool, config: _config.Configuration
+) -> _VersionT | str:
if preformatted:
return tag
- if VERSION_CLASS is None or not isinstance(tag, VERSION_CLASS):
- tag = tag_to_version(tag, config)
- 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,
- distance=None,
- dirty=False,
- node=None,
- preformatted=False,
- branch=None,
- config=None,
- **kw
-):
- if not config:
- warnings.warn(
- "meta invoked without explicit configuration,"
- " will use defaults where required."
- )
+ 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)
- trace("version", tag, "->", parsed_version)
- assert parsed_version is not None, "cant parse version %s" % tag
+ log.info("version %s -> %s", tag, parsed_version)
+ assert parsed_version is not None, "Can't parse version %s" % tag
return ScmVersion(
- parsed_version, distance, node, dirty, preformatted, branch, config, **kw
+ parsed_version,
+ distance=distance,
+ node=node,
+ dirty=dirty,
+ preformatted=preformatted,
+ branch=branch,
+ config=config,
+ node_date=node_date,
)
-def guess_next_version(tag_version):
- version = _strip_local(str(tag_version))
- return _bump_dev(version) or _bump_regex(version)
-
-
-def _strip_local(version_string):
- public, sep, local = version_string.partition("+")
- return public
-
+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 _bump_dev(version):
- if ".dev" not in version:
- return
- prefix, tail = version.rsplit(".dev", 1)
- assert tail == "0", "own dev numbers are unsupported"
- return prefix
-
-
-def _bump_regex(version):
- prefix, tail = re.match(r"(.*?)(\d+)$", version).groups()
- return "%s%d" % (prefix, int(tail) + 1)
-
-
-def guess_next_dev_version(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, retain, increment=True):
- parts = [int(i) for i in str(version).split(".")[:retain]]
+def guess_next_simple_semver(
+ version: ScmVersion, retain: int, increment: bool = True
+) -> str:
+ 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:
return ".".join(str(i) for i in parts)
-def simplified_semver_version(version):
+def simplified_semver_version(version: ScmVersion) -> str:
if version.exact:
- return guess_next_simple_semver(version.tag, retain=SEMVER_LEN, increment=False)
+ return guess_next_simple_semver(version, retain=SEMVER_LEN, increment=False)
else:
if version.branch is not None and "feature" in version.branch:
return version.format_next_version(
)
-def release_branch_semver_version(version):
+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 = _parse_version_tag(version.branch.split("/")[-1], version.config)
- if branch_ver is not None:
+ 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["version"].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)
return version.format_next_version(guess_next_simple_semver, retain=SEMVER_MINOR)
-def release_branch_semver(version):
+def release_branch_semver(version: ScmVersion) -> str:
warnings.warn(
- "release_branch_semver is deprecated and will be removed in future. "
- + "Use release_branch_semver_version instead",
+ "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 _format_local_with_time(version, time_format):
+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<date>
+ (?P<prefix>[vV]?)
+ (?P<year>\d{2}|\d{4})(?:\.\d{1,2}){2})
+ (?:\.(?P<patch>\d*))?$
+ """,
+ re.VERBOSE,
+)
+
+
+def date_ver_match(ver: str) -> Match[str] | None:
+ return _DATE_REGEX.match(ver)
+
- if version.exact or version.node is None:
- return version.format_choice(
- "", "+d{time:{time_format}}", time_format=time_format
+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:
- return version.format_choice(
- "+{node}", "+{node}.d{time:{time_format}}", time_format=time_format
+ # 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})"
+ f" 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):
- return _format_local_with_time(version, time_format="%Y%m%d")
+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, fmt="%Y%m%d%H%M%S"):
- return _format_local_with_time(version, time_format=fmt)
+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):
+def get_local_dirty_tag(version: ScmVersion) -> str:
return version.format_choice("", "+dirty")
-def get_no_local_node(_):
+def get_no_local_node(version: ScmVersion) -> str:
return ""
-def postrelease_version(version):
+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, **config):
- trace("scm version", version)
- trace("config", config)
+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
- version_scheme = callable_or_entrypoint(
- "setuptools_scm.version_scheme", config["version_scheme"]
+ main_version = _entrypoints._call_version_scheme(
+ version, "setuptools_scm.version_scheme", version.config.version_scheme, None
)
- local_scheme = callable_or_entrypoint(
- "setuptools_scm.local_scheme", config["local_scheme"]
+ log.debug("version %s", main_version)
+ assert main_version is not None
+ local_version = _entrypoints._call_version_scheme(
+ version, "setuptools_scm.local_scheme", version.config.local_scheme, "+unknown"
)
- main_version = version_scheme(version)
- trace("version", main_version)
- local_version = local_scheme(version)
- trace("local_version", local_version)
- return version_scheme(version) + local_scheme(version)
+ log.debug("local_version %s", local_version)
+ return main_version + local_version
+++ /dev/null
-"""
-Backport of os.path.samefile for Python prior to 3.2
-on Windows from jaraco.windows 3.8.
-
-DON'T EDIT THIS FILE!
-
-Instead, file tickets and PR's with `jaraco.windows
-<https://github.com/jaraco/jaraco.windows>`_ and request
-a port to setuptools_scm.
-"""
-
-import os
-import nt
-import posixpath
-import ctypes.wintypes
-import sys
-import __builtin__ as builtins
-
-
-##
-# From jaraco.windows.error
-
-def format_system_message(errno):
- """
- Call FormatMessage with a system error number to retrieve
- the descriptive error message.
- """
- # first some flags used by FormatMessageW
- ALLOCATE_BUFFER = 0x100
- FROM_SYSTEM = 0x1000
-
- # Let FormatMessageW allocate the buffer (we'll free it below)
- # Also, let it know we want a system error message.
- flags = ALLOCATE_BUFFER | FROM_SYSTEM
- source = None
- message_id = errno
- language_id = 0
- result_buffer = ctypes.wintypes.LPWSTR()
- buffer_size = 0
- arguments = None
- bytes = ctypes.windll.kernel32.FormatMessageW(
- flags,
- source,
- message_id,
- language_id,
- ctypes.byref(result_buffer),
- buffer_size,
- arguments,
- )
- # note the following will cause an infinite loop if GetLastError
- # repeatedly returns an error that cannot be formatted, although
- # this should not happen.
- handle_nonzero_success(bytes)
- message = result_buffer.value
- ctypes.windll.kernel32.LocalFree(result_buffer)
- return message
-
-
-class WindowsError(builtins.WindowsError):
- """
- More info about errors at
- http://msdn.microsoft.com/en-us/library/ms681381(VS.85).aspx
- """
-
- def __init__(self, value=None):
- if value is None:
- value = ctypes.windll.kernel32.GetLastError()
- strerror = format_system_message(value)
- if sys.version_info > (3, 3):
- args = 0, strerror, None, value
- else:
- args = value, strerror
- super(WindowsError, self).__init__(*args)
-
- @property
- def message(self):
- return self.strerror
-
- @property
- def code(self):
- return self.winerror
-
- def __str__(self):
- return self.message
-
- def __repr__(self):
- return '{self.__class__.__name__}({self.winerror})'.format(**vars())
-
-
-def handle_nonzero_success(result):
- if result == 0:
- raise WindowsError()
-
-
-##
-# From jaraco.windows.api.filesystem
-
-FILE_FLAG_OPEN_REPARSE_POINT = 0x00200000
-FILE_FLAG_BACKUP_SEMANTICS = 0x2000000
-OPEN_EXISTING = 3
-FILE_ATTRIBUTE_NORMAL = 0x80
-FILE_READ_ATTRIBUTES = 0x80
-INVALID_HANDLE_VALUE = ctypes.wintypes.HANDLE(-1).value
-
-
-class BY_HANDLE_FILE_INFORMATION(ctypes.Structure):
- _fields_ = [
- ('file_attributes', ctypes.wintypes.DWORD),
- ('creation_time', ctypes.wintypes.FILETIME),
- ('last_access_time', ctypes.wintypes.FILETIME),
- ('last_write_time', ctypes.wintypes.FILETIME),
- ('volume_serial_number', ctypes.wintypes.DWORD),
- ('file_size_high', ctypes.wintypes.DWORD),
- ('file_size_low', ctypes.wintypes.DWORD),
- ('number_of_links', ctypes.wintypes.DWORD),
- ('file_index_high', ctypes.wintypes.DWORD),
- ('file_index_low', ctypes.wintypes.DWORD),
- ]
-
- @property
- def file_size(self):
- return (self.file_size_high << 32) + self.file_size_low
-
- @property
- def file_index(self):
- return (self.file_index_high << 32) + self.file_index_low
-
-
-class SECURITY_ATTRIBUTES(ctypes.Structure):
- _fields_ = (
- ('length', ctypes.wintypes.DWORD),
- ('p_security_descriptor', ctypes.wintypes.LPVOID),
- ('inherit_handle', ctypes.wintypes.BOOLEAN),
- )
-
-
-LPSECURITY_ATTRIBUTES = ctypes.POINTER(SECURITY_ATTRIBUTES)
-
-
-CreateFile = ctypes.windll.kernel32.CreateFileW
-CreateFile.argtypes = (
- ctypes.wintypes.LPWSTR,
- ctypes.wintypes.DWORD,
- ctypes.wintypes.DWORD,
- LPSECURITY_ATTRIBUTES,
- ctypes.wintypes.DWORD,
- ctypes.wintypes.DWORD,
- ctypes.wintypes.HANDLE,
-)
-CreateFile.restype = ctypes.wintypes.HANDLE
-
-GetFileInformationByHandle = ctypes.windll.kernel32.GetFileInformationByHandle
-GetFileInformationByHandle.restype = ctypes.wintypes.BOOL
-GetFileInformationByHandle.argtypes = (
- ctypes.wintypes.HANDLE,
- ctypes.POINTER(BY_HANDLE_FILE_INFORMATION),
-)
-
-
-##
-# From jaraco.windows.filesystem
-
-def compat_stat(path):
- """
- Generate stat as found on Python 3.2 and later.
- """
- stat = os.stat(path)
- info = get_file_info(path)
- # rewrite st_ino, st_dev, and st_nlink based on file info
- return nt.stat_result(
- (stat.st_mode,) +
- (info.file_index, info.volume_serial_number, info.number_of_links) +
- stat[4:]
- )
-
-
-def samefile(f1, f2):
- """
- Backport of samefile from Python 3.2 with support for Windows.
- """
- return posixpath.samestat(compat_stat(f1), compat_stat(f2))
-
-
-def get_file_info(path):
- # open the file the same way CPython does in posixmodule.c
- desired_access = FILE_READ_ATTRIBUTES
- share_mode = 0
- security_attributes = None
- creation_disposition = OPEN_EXISTING
- flags_and_attributes = (
- FILE_ATTRIBUTE_NORMAL |
- FILE_FLAG_BACKUP_SEMANTICS |
- FILE_FLAG_OPEN_REPARSE_POINT
- )
- template_file = None
-
- handle = CreateFile(
- path,
- desired_access,
- share_mode,
- security_attributes,
- creation_disposition,
- flags_and_attributes,
- template_file,
- )
-
- if handle == INVALID_HANDLE_VALUE:
- raise WindowsError()
-
- info = BY_HANDLE_FILE_INFORMATION()
- res = GetFileInformationByHandle(handle, info)
- handle_nonzero_success(res)
-
- return info
--- /dev/null
+FROM 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
--- /dev/null
+FROM 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
+++ /dev/null
-import pkg_resources
-import setuptools_scm
-
-dist = pkg_resources.get_distribution("setuptools_scm")
-assert dist.version == setuptools_scm.get_version(), dist.version
+from __future__ import annotations
+
+import contextlib
import os
-import itertools
+from pathlib import Path
+from types import TracebackType
+from typing import Any
+from typing import Iterator
+
import pytest
-import six
+from typing_extensions import Self
+
+from .wd_wrapper import WorkDir
+from setuptools_scm._run_cmd import run
+
+
+def pytest_configure() -> None:
+ # 2009-02-13T23:31:30+00:00
+ os.environ["SOURCE_DATE_EPOCH"] = "1234567890"
+ os.environ["SETUPTOOLS_SCM_DEBUG"] = "1"
+
-os.environ["SETUPTOOLS_SCM_DEBUG"] = "1"
-VERSION_PKGS = ["setuptools", "setuptools_scm"]
+VERSION_PKGS = ["setuptools", "setuptools_scm", "packaging", "build", "wheel"]
-def pytest_report_header():
- import pkg_resources
+def pytest_report_header() -> list[str]:
+ from importlib.metadata import version
res = []
for pkg in VERSION_PKGS:
- version = pkg_resources.get_distribution(pkg).version
+ pkg_version = version(pkg)
path = __import__(pkg).__file__
- res.append("{} version {} from {!r}".format(pkg, version, path))
+ res.append(f"{pkg} version {pkg_version} from {path!r}")
return res
-class Wd(object):
- commit_command = None
- add_command = None
+def pytest_addoption(parser: Any) -> None:
+ group = parser.getgroup("setuptools_scm")
+ group.addoption(
+ "--test-legacy", dest="scm_test_virtualenv", default=False, action="store_true"
+ )
- def __repr__(self):
- return "<WD {cwd}>".format(cwd=self.cwd)
- def __init__(self, cwd):
- self.cwd = cwd
- self.__counter = itertools.count()
+class DebugMode(contextlib.AbstractContextManager): # type: ignore[type-arg]
+ from setuptools_scm import _log as __module
- def __call__(self, cmd, **kw):
- if kw:
- cmd = cmd.format(**kw)
- from setuptools_scm.utils import do
+ def __init__(self) -> None:
+ self.__stack = contextlib.ExitStack()
- return do(cmd, self.cwd)
+ def __enter__(self) -> Self:
+ self.enable()
+ return self
- def write(self, name, value, **kw):
- filename = self.cwd / name
- if kw:
- value = value.format(**kw)
- if isinstance(value, six.text_type):
- filename.write_text(value)
- else:
- filename.write_bytes(value)
- return filename
+ def __exit__(
+ self,
+ exc_type: type[BaseException] | None,
+ exc_val: BaseException | None,
+ exc_tb: TracebackType | None,
+ ) -> None:
+ self.disable()
- def _reason(self, given_reason):
- if given_reason is None:
- return "number-{c}".format(c=next(self.__counter))
- else:
- return given_reason
+ def enable(self) -> None:
+ self.__stack.enter_context(self.__module.defer_to_pytest())
- def add_and_commit(self, reason=None):
- self(self.add_command)
- self.commit(reason)
+ def disable(self) -> None:
+ self.__stack.close()
- def commit(self, reason=None):
- reason = self._reason(reason)
- self(self.commit_command, reason=reason)
- def commit_testfile(self, reason=None):
- reason = self._reason(reason)
- self.write("test.txt", "test {reason}", reason=reason)
- self(self.add_command)
- self.commit(reason=reason)
+@pytest.fixture(autouse=True)
+def debug_mode() -> Iterator[DebugMode]:
+ with DebugMode() as debug_mode:
+ yield debug_mode
- def get_version(self, **kw):
- __tracebackhide__ = True
- from setuptools_scm import get_version
- version = get_version(root=str(self.cwd), fallback_root=str(self.cwd), **kw)
- print(version)
- return version
+@pytest.fixture()
+def wd(tmp_path: Path) -> WorkDir:
+ target_wd = tmp_path.resolve() / "wd"
+ target_wd.mkdir()
+ return WorkDir(target_wd)
- @property
- def version(self):
- __tracebackhide__ = True
- return self.get_version()
+@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()
-@pytest.yield_fixture(autouse=True)
-def debug_mode():
- from setuptools_scm import utils
+ 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}"
- utils.DEBUG = True
- yield
- utils.DEBUG = False
+ 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")
-@pytest.fixture
-def wd(tmp_path):
- target_wd = tmp_path.resolve() / "wd"
- target_wd.mkdir()
- return Wd(target_wd)
+ 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
+from __future__ import annotations
+
import os
import sys
-import py
+from datetime import date
+from pathlib import Path
+
import pytest
import setuptools_scm
+from setuptools_scm import Configuration
from setuptools_scm import dump_version
-from setuptools_scm.utils import data_from_mime, do
+from setuptools_scm._run_cmd import run
+from setuptools_scm.integration import data_from_mime
+from setuptools_scm.version import meta
+from setuptools_scm.version import ScmVersion
+from testing.wd_wrapper import WorkDir
+
+
+c = Configuration()
+
+template = """\
+__version__ = version = {version!r}
+__version_tuple__ = version_tuple = {version_tuple!r}
+__sha__ = {scm_version.node!r}
+"""
-@pytest.mark.parametrize("cmd", ["ls", "dir"])
-def test_do(cmd, tmpdir):
- if not py.path.local.sysfind(cmd):
- pytest.skip(cmd + " not found")
- do(cmd, str(tmpdir))
+def test_run_plain(tmp_path: Path) -> None:
+ run([sys.executable, "-c", "print(1)"], cwd=tmp_path)
-def test_data_from_mime(tmpdir):
- tmpfile = tmpdir.join("test.archival")
- tmpfile.write("name: test\nrevision: 1")
+def test_data_from_mime(tmp_path: Path) -> None:
+ tmpfile = tmp_path.joinpath("test.archival")
+ tmpfile.write_text("name: test\nrevision: 1")
res = data_from_mime(str(tmpfile))
assert res == {"name": "test", "revision": "1"}
-def test_version_from_pkginfo(wd, monkeypatch):
+def test_version_from_pkginfo(wd: WorkDir) -> None:
wd.write("PKG-INFO", "Version: 0.1")
- assert wd.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, expected_root):
+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):
+ def assertion(config: Configuration) -> ScmVersion:
assert config.absolute_root == expected_root
+ return ScmVersion("1.0", config=config)
- monkeypatch.setattr(setuptools_scm, "_do_parse", assertion)
+ monkeypatch.setattr(setuptools_scm._get_version_impl, "parse_version", assertion)
-def test_root_parameter_creation(monkeypatch):
+def test_root_parameter_creation(monkeypatch: pytest.MonkeyPatch) -> None:
assert_root(monkeypatch, os.getcwd())
setuptools_scm.get_version()
-def test_version_from_scm(wd):
- with pytest.warns(DeprecationWarning, match=".*version_from_scm.*"):
- setuptools_scm.version_from_scm(str(wd))
+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_root_parameter_pass_by(monkeypatch, tmpdir):
- assert_root(monkeypatch, tmpdir)
- setuptools_scm.get_version(root=tmpdir.strpath)
+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-"})
+"""
+ )
+ res = run([sys.executable, "setup.py", "--version"], p)
+ assert res.stdout == "12.34"
-def test_parentdir_prefix(tmpdir, monkeypatch):
+def test_fallback(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.delenv("SETUPTOOLS_SCM_DEBUG")
- p = tmpdir.ensure("projectname-v12.34", dir=True)
- p.join("setup.py").write(
+ p = tmp_path / "sub/package"
+ p.mkdir(parents=True)
+ p.joinpath("setup.py").write_text(
"""from setuptools import setup
-setup(use_scm_version={"parentdir_prefix_version": "projectname-"})
+setup(use_scm_version={"fallback_version": "12.34"})
"""
)
- res = do((sys.executable, "setup.py", "--version"), p)
- assert res == "12.34"
+ res = run([sys.executable, "setup.py", "--version"], p)
+ assert res.stdout == "12.34"
-def test_fallback(tmpdir, monkeypatch):
+def test_empty_pretend_version(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.delenv("SETUPTOOLS_SCM_DEBUG")
- p = tmpdir.ensure("sub/package", dir=1)
- p.join("setup.py").write(
+ 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"})
"""
)
- res = do((sys.executable, "setup.py", "--version"), p)
- assert res == "12.34"
+ 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"})
+"""
+ )
+ 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, monkeypatch):
- monkeypatch.setenv(setuptools_scm.PRETEND_KEY, version)
+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, tmpdir):
- assert_root(monkeypatch, tmpdir.join("alt").strpath)
- __file__ = tmpdir.join("module/file.py").strpath
- setuptools_scm.get_version(root="../alt", relative_to=__file__)
+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()
+
+ 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
-def test_dump_version(tmpdir):
- sp = tmpdir.strpath
-
- dump_version(sp, "1.0", "first.txt")
- assert tmpdir.join("first.txt").read() == "1.0"
- dump_version(sp, "1.0", "first.py")
- content = tmpdir.join("first.py").read()
- assert repr("1.0") in content
import ast
- ast.parse(content)
+ ast.parse(read("third.py"))
-def test_parse_plain_fails(recwarn):
- def parse(root):
+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_text("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
--- /dev/null
+from __future__ import annotations
+
+import io
+from contextlib import redirect_stdout
+
+import pytest
+
+from .conftest import DebugMode
+from .test_git import wd as wd_fixture # NOQA evil fixture reuse
+from .wd_wrapper import WorkDir
+from setuptools_scm._cli import main
+
+
+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 ."
+)
+warns_absolute_root_override = pytest.warns(
+ UserWarning, match="absolute root path '.*' overrides relative_to '.*'"
+)
+
+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:
+ debug_mode.disable()
+ 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 exits_with_not_found, warns_absolute_root_override:
+ get_output(["-c", PYPROJECT_TOML, "--root=.."])
+
+ with warns_cli_root_override:
+ out = get_output(["-c", PYPROJECT_TOML, "--root=."])
+ assert out.startswith("0.1.dev1+")
-from __future__ import unicode_literals
+from __future__ import annotations
-from setuptools_scm.config import Configuration
import re
+import textwrap
+from pathlib import Path
+
import pytest
+from setuptools_scm import Configuration
+
@pytest.mark.parametrize(
- "tag, expected_version",
+ ("tag", "expected_version"),
[
("apache-arrow-0.9.0", "0.9.0"),
("arrow-0.9.0", "0.9.0"),
("V1.1", "V1.1"),
],
)
-def test_tag_regex(tag, expected_version):
+def test_tag_regex(tag: str, expected_version: str) -> None:
config = Configuration()
match = config.tag_regex.match(tag)
assert match
assert version == expected_version
-def test_config_from_pyproject(tmpdir):
- fn = tmpdir / "pyproject.toml"
- fn.write_text("[tool.setuptools_scm]\n", encoding="utf-8")
+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():
+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=".."}'
+ )
+ overriden = Configuration.from_file(fn)
+
+ assert pristine.root != overriden.root
+ assert pristine.fallback_root != overriden.fallback_root
+from __future__ import annotations
+
import os
import sys
+from typing import Iterable
import pytest
-from setuptools_scm.integration import find_files
+from .wd_wrapper import WorkDir
+from setuptools_scm._file_finders import find_files
@pytest.fixture(params=["git", "hg"])
-def inwd(request, wd, monkeypatch):
- if request.param == "git":
- if sys.platform == "win32" and sys.version_info[0] < 3:
- pytest.skip("Long/short path names supported on Windows Python 2.7")
+def inwd(
+ request: pytest.FixtureRequest, wd: WorkDir, monkeypatch: pytest.MonkeyPatch
+) -> WorkDir:
+ param: str = request.param # type: ignore
+ if param == "git":
try:
wd("git init")
except OSError:
wd('git config user.name "a test"')
wd.add_command = "git add ."
wd.commit_command = "git commit -m test-{reason}"
- elif request.param == "hg":
+ elif param == "hg":
try:
wd("hg init")
except OSError:
bdir = wd.cwd / "bdir"
bdir.mkdir()
(bdir / "fileb").touch()
- wd.add_and_commit()
+ if request.node.get_closest_marker("skip_commit") is None:
+ wd.add_and_commit()
monkeypatch.chdir(wd.cwd)
- yield wd
+ return wd
-def _sep(paths):
+def _sep(paths: Iterable[str]) -> set[str]:
return {path.replace("/", os.path.sep) for path in paths}
-def test_basic(inwd):
+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):
+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):
+def test_case(inwd: WorkDir) -> None:
(inwd.cwd / "CamelFile").touch()
(inwd.cwd / "file2").touch()
inwd.add_and_commit()
@pytest.mark.skipif(sys.platform == "win32", reason="symlinks to dir not supported")
-def test_symlink_dir(inwd):
+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):
+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):
+def test_symlink_file(inwd: WorkDir) -> None:
(inwd.cwd / "adir" / "file1link").symlink_to("../file1")
inwd.add_and_commit()
assert set(find_files("adir")) == _sep(
@pytest.mark.skipif(
sys.platform == "win32", reason="symlinks to files not supported on windows"
)
-def test_symlink_file_source_not_in_scm(inwd):
+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):
+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):
+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()
@pytest.mark.skipif(sys.platform == "win32", reason="symlinks to dir not supported")
-def test_symlink_dir_out_of_git(inwd):
+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):
+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"})
-def test_empty_root(inwd):
+@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()
assert set(find_files("cdir")) == _sep({"cdir/subdir/filec"})
-def test_empty_subdir(inwd):
+def test_empty_subdir(inwd: WorkDir) -> None:
subdir = inwd.cwd / "adir" / "emptysubdir" / "subdir"
subdir.mkdir(parents=True)
(subdir / "xfile").touch()
@pytest.mark.skipif(sys.platform == "win32", reason="symlinks not supported on windows")
-def test_double_include_through_symlink(inwd):
+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")
@pytest.mark.skipif(sys.platform == "win32", reason="symlinks not supported on windows")
-def test_symlink_not_in_scm_while_target_is(inwd):
+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()
"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"})
-import pytest
-import sys
-import pkg_resources
-from setuptools_scm import dump_version, get_version, PRETEND_KEY
-from setuptools_scm.version import (
- guess_next_version,
- meta,
- format_version,
- tag_to_version,
-)
+from __future__ import annotations
-from setuptools_scm.config import Configuration
-from setuptools_scm.utils import has_command
+import shutil
+import subprocess
+from pathlib import Path
-PY3 = sys.version_info > (2,)
+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
-class MockTime(object):
- def __format__(self, *k):
- return "time"
+c = Configuration()
@pytest.mark.parametrize(
- "tag, expected",
+ ("tag", "expected"),
[
("1.1", "1.2"),
("1.2.dev", "1.2"),
("1.1a2", "1.1a3"),
- ("23.24.post2+deadbeef", "23.24.post3"),
+ 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, expected):
- version = pkg_resources.parse_version(tag)
+def test_next_tag(tag: str, expected: str) -> None:
+ version = meta(tag, config=c)
assert guess_next_version(version) == expected
-c = Configuration()
-
VERSIONS = {
- "exact": meta("1.1", distance=None, dirty=False, config=c),
- "zerodistance": meta("1.1", distance=0, dirty=False, config=c),
- "dirty": meta("1.1", distance=None, dirty=True, config=c),
- "distance": meta("1.1", distance=3, dirty=False, config=c),
- "distancedirty": meta("1.1", distance=3, dirty=True, config=c),
+ "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,scheme,expected",
+ ("version", "version_scheme", "local_scheme", "expected"),
[
- ("exact", "guess-next-dev node-and-date", "1.1"),
- ("zerodistance", "guess-next-dev node-and-date", "1.2.dev0"),
- ("zerodistance", "guess-next-dev no-local-version", "1.2.dev0"),
- ("dirty", "guess-next-dev node-and-date", "1.2.dev0+dtime"),
- ("dirty", "guess-next-dev no-local-version", "1.2.dev0"),
- ("distance", "guess-next-dev node-and-date", "1.2.dev3"),
- ("distancedirty", "guess-next-dev node-and-date", "1.2.dev3+dtime"),
- ("distancedirty", "guess-next-dev no-local-version", "1.2.dev3"),
- ("exact", "post-release node-and-date", "1.1"),
- ("zerodistance", "post-release node-and-date", "1.1.post0"),
- ("dirty", "post-release node-and-date", "1.1.post0+dtime"),
- ("distance", "post-release node-and-date", "1.1.post3"),
- ("distancedirty", "post-release node-and-date", "1.1.post3+dtime"),
+ ("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, monkeypatch, scheme, expected):
- version = VERSIONS[version]
- monkeypatch.setattr(version, "time", MockTime())
- vs, ls = scheme.split()
- assert format_version(version, version_scheme=vs, local_scheme=ls) == expected
-
-
-def test_dump_version_doesnt_bail_on_value_error(tmpdir):
+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)
- with pytest.raises(ValueError) as exc_info:
- dump_version(tmpdir.strpath, version, write_to)
- assert str(exc_info.value).startswith("bad file format:")
+ scm_version = meta(VERSIONS["exact"].tag, config=c)
+ with pytest.raises(ValueError, match="^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, tmpdir, monkeypatch):
+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() == version
+
+
+def test_dump_version_modern(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
+ version = "1.2.3"
monkeypatch.setenv(PRETEND_KEY, version)
- get_version(write_to=str(tmpdir.join("VERSION.txt")))
- assert tmpdir.join("VERSION.txt").read() == 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() == 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_has_command() -> None:
+ with pytest.warns(RuntimeWarning, match="yadayada"):
+ assert not has_command("yadayada_setuptools_aint_ne")
-def test_has_command(recwarn):
- assert not has_command("yadayada_setuptools_aint_ne")
- msg = recwarn.pop()
- assert "yadayada" in str(msg.message)
+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",
+ ("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, expected_version):
- version = str(tag_to_version(tag))
+def test_tag_to_version(tag: str, expected_version: str) -> None:
+ version = str(tag_to_version(tag, c))
assert version == expected_version
-import sys
+from __future__ import annotations
-from setuptools_scm import integration
-from setuptools_scm.utils import do, has_command
-from setuptools_scm import git
-import pytest
+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 setuptools_scm.file_finder_git import git_find_files
-import warnings
+from pathlib import Path
+from textwrap import dedent
+from typing import Generator
+from unittest.mock import Mock
+from unittest.mock import patch
+import pytest
-skip_if_win_27 = pytest.mark.skipif(
- sys.platform == "win32" and sys.version_info[0] < 3,
- reason="Not supported on Windows + Python 2.7",
+import setuptools_scm._file_finders
+from .conftest import DebugMode
+from .wd_wrapper import WorkDir
+from setuptools_scm import Configuration
+from setuptools_scm import git
+from setuptools_scm import NonNormalizedVersion
+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
+
+pytestmark = pytest.mark.skipif(
+ not has_command("git", warn=False), reason="git executable not found"
)
-with warnings.catch_warnings():
- warnings.filterwarnings("ignore")
- if not has_command("git"):
- pytestmark = pytest.mark.skip(reason="git executable not found")
-
-
-@pytest.fixture
-def wd(wd, monkeypatch):
+@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",
+ ("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, tag, number, node, dirty):
+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(tmpdir, wd, monkeypatch):
+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(
- u"""from setuptools import setup
+ """from setuptools import setup
setup(use_scm_version={"root": "../..",
"relative_to": __file__})
"""
)
- res = do((sys.executable, "setup.py", "--version"), p)
- assert res == "0.1.dev0"
+ 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})
+"""
+ )
+ 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, caplog):
+def test_file_finder_no_history(wd: WorkDir, caplog: pytest.LogCaptureFixture) -> None:
file_list = git_find_files(str(wd.cwd))
assert file_list == []
@pytest.mark.issue("https://github.com/pypa/setuptools_scm/issues/281")
-def test_parse_call_order(wd):
- git.parse(str(wd.cwd), git.DEFAULT_DESCRIBE)
+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)
-def test_version_from_git(wd):
- assert wd.version == "0.1.dev0"
+@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.version.startswith("0.1.dev1+g")
- assert not wd.version.endswith("1-")
+ assert wd.get_version().startswith("0.1.dev1+g")
+ assert not wd.get_version().endswith("1-")
wd("git tag v0.1")
- assert wd.version == "0.1"
+ assert wd.get_version() == "0.1"
wd.write("test.txt", "test2")
- assert wd.version.startswith("0.2.dev0+g")
+ assert wd.get_version().startswith("0.2.dev0+g")
wd.commit_testfile()
- assert wd.version.startswith("0.2.dev1+g")
+ assert wd.get_version().startswith("0.2.dev1+g")
wd("git tag version-0.2")
- assert wd.version.startswith("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.version.startswith("0.2")
+ assert wd.get_version().startswith("0.2")
wd.commit_testfile()
wd("git tag 17.33.0-rc")
- assert wd.version == "17.33.0rc0"
+ 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() == "17.33.0-rc1"
@pytest.mark.issue(179)
-def test_unicode_version_scheme(wd):
+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):
+def test_git_worktree(wd: WorkDir) -> None:
wd.write("test.txt", "test2")
# untracked files dont change the state
- assert wd.version == "0.1.dev0"
+ assert wd.get_version() == "0.1.dev0+d20090213"
+
wd("git add test.txt")
- assert wd.version.startswith("0.1.dev0+d")
+ assert wd.get_version().startswith("0.1.dev0+d")
@pytest.mark.issue(86)
-def test_git_dirty_notag(wd):
+@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")
- assert wd.version.startswith("0.1.dev1")
- # the date on the tag is in UTC
- today = datetime.utcnow().date()
- # we are dirty, check for the tag
- assert today.strftime(".d%Y%m%d") in wd.version
+ 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)
-def test_git_worktree_support(wd, tmpdir):
+@pytest.mark.xfail(reason="sometimes relative path results")
+def test_git_worktree_support(wd: WorkDir, tmp_path: Path) -> None:
wd.commit_testfile()
- worktree = tmpdir.join("work_tree")
+ worktree = tmp_path / "work_tree"
wd("git worktree add -b work-tree %s" % worktree)
- res = do([sys.executable, "-m", "setuptools_scm", "ls"], cwd=worktree)
- assert str(worktree) in res
+ 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, tmpdir):
+@pytest.fixture()
+def shallow_wd(wd: WorkDir, tmp_path: Path) -> Path:
wd.commit_testfile()
wd.commit_testfile()
wd.commit_testfile()
- target = tmpdir.join("wd_shallow")
- do(["git", "clone", "file://%s" % wd.cwd, str(target), "--depth=1"])
+ 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, recwarn):
- git.parse(str(shallow_wd))
+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):
- with pytest.raises(ValueError) as einfo:
- git.parse(str(shallow_wd), pre_parse=git.fail_on_shallow)
-
- assert "git fetch" in str(einfo.value)
+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, recwarn):
- git.parse(str(shallow_wd), pre_parse=git.fetch_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), pre_parse=git.fail_on_shallow)
+ git.parse(str(shallow_wd), Configuration(), pre_parse=git.fail_on_shallow)
-def test_find_files_stop_at_root_git(wd):
+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 integration.find_files(str(project)) == []
+ assert setuptools_scm._file_finders.find_files(str(project)) == []
@pytest.mark.issue(128)
-def test_parse_no_worktree(tmpdir):
- ret = git.parse(str(tmpdir))
+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):
+def test_alphanumeric_tags_match(wd: WorkDir) -> None:
wd.commit_testfile()
wd("git tag newstyle-development-started")
- assert wd.version.startswith("0.1.dev1+g")
+ assert wd.get_version().startswith("0.1.dev1+g")
-@skip_if_win_27
-def test_git_archive_export_ignore(wd, monkeypatch):
+def test_git_archive_export_ignore(
+ wd: WorkDir, monkeypatch: pytest.MonkeyPatch
+) -> None:
wd.write("test1.txt", "test")
wd.write("test2.txt", "test")
wd.write(
wd("git add test1.txt test2.txt")
wd.commit()
monkeypatch.chdir(wd.cwd)
- assert integration.find_files(".") == [opj(".", "test1.txt")]
+ assert setuptools_scm._file_finders.find_files(".") == [opj(".", "test1.txt")]
-@skip_if_win_27
@pytest.mark.issue(228)
-def test_git_archive_subdirectory(wd, monkeypatch):
- wd("mkdir foobar")
+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 integration.find_files(".") == [opj(".", "foobar", "test1.txt")]
+ assert setuptools_scm._file_finders.find_files(".") == [
+ opj(".", "foobar", "test1.txt")
+ ]
-@skip_if_win_27
@pytest.mark.issue(251)
-def test_git_archive_run_from_subdirectory(wd, monkeypatch):
- wd("mkdir foobar")
+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 integration.find_files(".") == [opj(".", "test1.txt")]
+ assert setuptools_scm._file_finders.find_files(".") == [opj(".", "test1.txt")]
-def test_git_feature_branch_increments_major(wd):
+@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()
@pytest.mark.issue("https://github.com/pypa/setuptools_scm/issues/303")
-def test_not_matching_tags(wd):
+def test_not_matching_tags(wd: WorkDir) -> None:
wd.commit_testfile()
wd("git tag apache-arrow-0.11.1")
wd.commit_testfile()
@pytest.mark.issue("https://github.com/pypa/setuptools_scm/issues/411")
-@pytest.mark.xfail(reason="https://github.com/pypa/setuptools_scm/issues/449")
-def test_non_dotted_version(wd):
+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, wd):
- """
- """
+def test_gitdir(monkeypatch: pytest.MonkeyPatch, wd: WorkDir) -> None:
+ """ """
wd.commit_testfile()
- normal = wd.version
+ normal = wd.get_version()
# git hooks set this and break subsequent setuptools_scm unless we clean
monkeypatch.setenv("GIT_DIR", __file__)
- assert wd.version == normal
+ 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
+
+
+@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
--- /dev/null
+from __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"
+from __future__ import annotations
+
+import importlib.metadata
+import os
+import subprocess
import sys
+import textwrap
+from pathlib import Path
import pytest
-from setuptools_scm.utils import do
+import setuptools_scm._integration.setuptools
+from .wd_wrapper import WorkDir
+from setuptools_scm import Configuration
+from setuptools_scm._integration.setuptools import _warn_on_old_setuptools
+from setuptools_scm._overrides import PRETEND_KEY
+from setuptools_scm._overrides import PRETEND_KEY_NAMED
+from setuptools_scm._run_cmd import run
+c = Configuration()
-@pytest.fixture
-def wd(wd):
- try:
- wd("git init")
- except OSError:
- pytest.skip("git executable not found")
+@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 ."
return wd
-def test_pyproject_support(tmpdir, monkeypatch):
- pytest.importorskip("toml")
+def test_pyproject_support(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
+ if sys.version_info <= (3, 10):
+ pytest.importorskip("tomli")
monkeypatch.delenv("SETUPTOOLS_SCM_DEBUG")
- pkg = tmpdir.ensure("package", dir=42)
- pkg.join("pyproject.toml").write(
- """[tool.setuptools_scm]
-fallback_version = "12.34"
-"""
+ 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.join("setup.py").write("__import__('setuptools').setup()")
- res = do((sys.executable, "setup.py", "--version"), pkg)
- assert res == "12.34"
+ pkg.joinpath("setup.py").write_text("__import__('setuptools').setup()")
+ res = run([sys.executable, "setup.py", "--version"], pkg)
+ assert res.stdout == "12.34"
-def test_pyproject_support_with_git(tmpdir, monkeypatch, wd):
- monkeypatch.delenv("SETUPTOOLS_SCM_DEBUG")
- pkg = tmpdir.join("wd")
- pkg.join("pyproject.toml").write("""[tool.setuptools_scm]""")
- pkg.join("setup.py").write("__import__('setuptools').setup()")
- res = do((sys.executable, "setup.py", "--version"), pkg)
- assert res == "0.1.dev0"
+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, 10):
+ 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)
+ from setuptools_scm._integration.setuptools import version_keyword
+ import setuptools
+
+ 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")
+ 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
--- /dev/null
+from __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
+from __future__ import annotations
+
import os.path
+import sys
+import textwrap
+
+import pytest
+from .wd_wrapper import WorkDir
-def test_main():
+
+def test_main() -> None:
mainfile = os.path.join(
os.path.dirname(__file__), "..", "src", "setuptools_scm", "__main__.py"
)
+ ns = {"__package__": "setuptools_scm"}
with open(mainfile) as f:
code = compile(f.read(), "__main__.py", "exec")
- exec(code)
+ 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")
-from setuptools_scm import format_version
-from setuptools_scm.hg import archival_to_version, parse
-from setuptools_scm import integration
-from setuptools_scm.config import Configuration
-from setuptools_scm.utils import has_command
+from __future__ import annotations
+
+import os
+from pathlib import Path
+
import pytest
-import warnings
+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
-with warnings.catch_warnings():
- warnings.filterwarnings("ignore")
- if not has_command("hg"):
- pytestmark = pytest.mark.skip(reason="hg executable not found")
+pytestmark = pytest.mark.skipif(
+ not has_command("hg", warn=False), reason="hg executable not found"
+)
-@pytest.fixture
-def wd(wd):
+
+@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"'
}
-@pytest.mark.parametrize("expected,data", sorted(archival_mapping.items()))
-def test_archival_to_version(expected, data):
- config = Configuration()
- version = archival_to_version(data, config=config)
- assert (
- format_version(
- version, version_scheme="guess-next-dev", local_scheme="node-and-date"
- )
- == expected
+@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="guess-next-dev", local_scheme="node-and-date"
)
+ version = archival_to_version(data, config=config)
+ assert format_version(version) == expected
-def test_find_files_stop_at_root_hg(wd, monkeypatch):
+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 integration.find_files(str(project)) == []
+ assert setuptools_scm._file_finders.find_files(str(project)) == []
# issue 251
wd.add_and_commit()
monkeypatch.chdir(project)
- assert integration.find_files() == ["setup.cfg"]
+ assert setuptools_scm._file_finders.find_files() == ["setup.cfg"]
# XXX: better tests for tag prefixes
-def test_version_from_hg_id(wd):
- assert wd.version == "0.0"
+def test_version_from_hg_id(wd: WorkDir) -> None:
+ assert wd.get_version() == "0.0"
wd.commit_testfile()
- assert wd.version.startswith("0.1.dev2+")
+ 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.version == "0.1"
+ assert wd.get_version() == "0.1"
wd.commit_testfile()
- assert wd.version.startswith("0.2.dev2")
+ assert wd.get_version().startswith("0.2.dev2")
wd("hg up v0.1")
- assert wd.version == "0.1"
+ assert wd.get_version() == "0.1"
- # commit originating from the taged revision
- # that is not a actual tag
+ # commit originating from the tagged revision
+ # that is not an actual tag
wd.commit_testfile()
- assert wd.version.startswith("0.2.dev1+")
+ 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.version == "0.3"
+ assert wd.get_version() == "0.3"
-def test_version_from_archival(wd):
+def test_version_from_archival(wd: WorkDir) -> None:
# entrypoints are unordered,
- # cleaning the wd ensure this test wont break randomly
+ # 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\n" "tag: 0.1\n")
- assert wd.version == "0.1"
+ assert wd.get_version() == "0.1"
wd.write(
".hg_archival.txt",
"node: 000000000000\n" "latesttag: 0.1\n" "latesttagdistance: 3\n",
)
- assert wd.version == "0.2.dev3+h000000000000"
+ assert wd.get_version() == "0.2.dev3+h000000000000"
@pytest.mark.issue("#72")
-def test_version_in_merge(wd):
+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.version is not None
+ assert wd.get_version() is not None
@pytest.mark.issue(128)
-def test_parse_no_worktree(tmpdir):
- ret = parse(str(tmpdir))
+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):
+@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(wd, version_1_0):
+@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)
@pytest.mark.usefixtures("pre_merge_commit_after_tag")
-def test_version_bump_before_merge_commit(wd):
- assert wd.version.startswith("1.0.1.dev1+")
+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):
+def test_version_bump_from_merge_commit(wd: WorkDir) -> None:
wd.commit()
- assert wd.version.startswith("1.0.1.dev3+") # issue 219
+ 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):
- """ Test the case where a commit includes changes to .hgtags and other files
- """
+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.version.startswith("1.0.1.dev1+") # bump from dirty version
+ assert wd.get_version().startswith("1.0.1.dev1+") # bump from dirty version
wd.commit() # commits both the testfile _and_ .hgtags
- assert wd.version.startswith("1.0.1.dev2+")
+ 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):
- """ Tests that tags not containing a "." are ignored, the same as for git.
- Note that will be superceded by the fix for pypa/setuptools_scm/issues/235
+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.version == "1.0.0"
+ assert wd.get_version() == "1.0.0"
@pytest.mark.usefixtures("version_1_0")
-def test_feature_branch_increments_major(wd):
-
+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")
-import sys
+from __future__ import annotations
+
+import pprint
import subprocess
+import sys
+from dataclasses import replace
+from importlib.metadata import distribution
+from importlib.metadata import EntryPoint
+from pathlib import Path
+
+import pytest
-from setuptools_scm import get_version
+from setuptools_scm import Configuration
+from setuptools_scm._run_cmd import run
from setuptools_scm.git import parse
-from setuptools_scm.utils import do_ex, do
+from setuptools_scm.integration import data_from_mime
+from setuptools_scm.version import meta
-import pytest
+
+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(tmpdir, monkeypatch):
+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 = tmpdir.ensure("sub/package", dir=1)
- tmpdir.mkdir(".git")
- p.join("setup.py").write(
+ 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": ".."})'
)
- _, stderr, ret = do_ex((sys.executable, "setup.py", "--version"), p)
- assert "setuptools-scm was unable to detect version for" in stderr
- assert ret == 1
+ 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.join("PKG-INFO").write("Version: 1.0")
- res = do((sys.executable, "setup.py", "--version"), p)
- assert res == "1.0"
+ p.joinpath("PKG-INFO").write_text("Version: 1.0")
+ res = run([sys.executable, "setup.py", "--version"], p)
+ assert res.stdout == "1.0"
try:
- do("git init", p.dirpath())
+ run("git init", p.parent)
except OSError:
pass
else:
- res = do((sys.executable, "setup.py", "--version"), p)
- assert res == "0.1.dev0"
-
-
-def test_pip_egg_info(tmpdir, monkeypatch):
- """if we are indeed a sdist, the root does not apply"""
-
- # we should get the version from pkg-info if git is broken
- p = tmpdir.ensure("sub/package", dir=1)
- tmpdir.mkdir(".git")
- p.join("setup.py").write(
- "from setuptools import setup;" 'setup(use_scm_version={"root": ".."})'
- )
-
- with pytest.raises(LookupError):
- get_version(root=p.strpath, fallback_root=p.strpath)
-
- p.ensure("pip-egg-info/random.egg-info/PKG-INFO").write("Version: 1.0")
- assert get_version(root=p.strpath, fallback_root=p.strpath) == "1.0"
+ res = run([sys.executable, "setup.py", "--version"], p)
+ assert res.stdout == "0.1.dev0+d20090213"
@pytest.mark.issue(164)
-def test_pip_download(tmpdir, monkeypatch):
- monkeypatch.chdir(tmpdir)
+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(tmpdir, monkeypatch):
+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 = tmpdir.ensure("sub/package", dir=1)
- p.join("setup.py").write(
+ 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
setup(use_scm_version=vcfg)
"""
)
- p.join("PKG-INFO").write("Version: 1.0")
+ p.joinpath("PKG-INFO").write_text("Version: 1.0")
- res = do((sys.executable, "setup.py", "--version"), p)
- assert res == "1.0"
+ 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(tmpdir):
+def test_case_mismatch_on_windows_git(tmp_path: Path) -> None:
"""Case insensitive path checks on Windows"""
- p = tmpdir.ensure("CapitalizedDir", dir=1)
-
- do("git init", p)
- res = parse(str(p).lower())
+ 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=".*VERSION.py' .* .*subdir.*",
+ ):
+ write_version_files(replace(c, root=subdir), "1.0", v)
+++ /dev/null
-"""
-integration tests that check setuptools version support
-"""
-import sys
-import os
-import subprocess
-import pytest
-
-pytestmark = [
- pytest.mark.skipif(
- "sys.version_info >= (3,6,0)",
- reason="integration with old versions no longer needed on py3.6+",
- ),
- pytest.mark.xfail(
- sys.platform == "win32", reason="path behaves unexpected on windows ci"
- ),
-]
-
-
-@pytest.fixture(scope="session")
-def get_setuptools_packagedir(request):
- targets = request.config.cache.makedir("setuptools_installs")
-
- def makeinstall(version):
- target = targets.ensure(version, dir=1)
- subprocess.check_call(
- [
- sys.executable,
- "-m",
- "pip",
- "install",
- "--no-binary",
- "setuptools",
- "setuptools==" + version,
- "-t",
- str(target),
- ]
- )
- return target
-
- return makeinstall
-
-
-SCRIPT = """
-from __future__ import print_function
-import sys
-import setuptools
-print(setuptools.__version__, 'expected', sys.argv[1])
-import setuptools_scm.version
-from setuptools_scm.__main__ import main
-main()
-"""
-
-
-def check(packagedir, expected_version, **env):
-
- old_pythonpath = os.environ.get("PYTHONPATH")
- if old_pythonpath:
- pythonpath = "{}:{}".format(old_pythonpath, packagedir)
- else:
- pythonpath = str(packagedir)
- subprocess.check_call(
- [sys.executable, "-c", SCRIPT, expected_version],
- env=dict(os.environ, PYTHONPATH=pythonpath, **env),
- )
-
-
-def test_old_setuptools_fails(get_setuptools_packagedir):
- packagedir = get_setuptools_packagedir("0.9.8")
- with pytest.raises(subprocess.CalledProcessError):
- check(packagedir, "0.9.8")
-
-
-def test_old_setuptools_allows_with_warnings(get_setuptools_packagedir):
-
- packagedir = get_setuptools_packagedir("0.9.8")
- # filter using warning since in the early python startup
- check(packagedir, "0.9.8", PYTHONWARNINGS="once::Warning")
-
-
-def test_distlib_setuptools_works(get_setuptools_packagedir):
- packagedir = get_setuptools_packagedir("12.0.1")
- check(packagedir, "12.0.1")
+from __future__ import annotations
+
+from dataclasses import replace
+from datetime import date
+from datetime import timedelta
+from typing import Any
+
import pytest
-from setuptools_scm.config import Configuration
-from setuptools_scm.version import (
- meta,
- simplified_semver_version,
- release_branch_semver_version,
- tags_to_versions,
-)
+
+from setuptools_scm import Configuration
+from setuptools_scm import NonNormalizedVersion
+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 release_branch_semver_version
+from setuptools_scm.version import ScmVersion
+from setuptools_scm.version import simplified_semver_version
c = Configuration()
+c_non_normalize = Configuration(version_cls=NonNormalizedVersion)
@pytest.mark.parametrize(
- "version, expected_next",
+ ("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"),
),
],
)
-def test_next_semver(version, expected_next):
+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",
+ ("version", "expected_next"),
[
pytest.param(meta("1.0.0", config=c), "1.0.0", id="exact"),
pytest.param(
"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",
),
],
)
-def test_next_release_branch_semver(version, expected_next):
+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(
- "tag, expected",
+ ("version", "match"),
[
- pytest.param("v1.0.0", "1.0.0"),
- pytest.param("v1.0.0-rc.1", "1.0.0rc1"),
- pytest.param("v1.0.0-rc.1+-25259o4382757gjurh54", "1.0.0rc1"),
+ ("1.0.dev1", "choosing custom numbers for the `.devX` distance"),
+ ("1.0.post1", "already is a post release"),
],
)
-def test_tag_regex1(tag, expected):
- config = Configuration()
+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(
+ ("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):
- result = meta(tag, config=config)
+ result = meta(tag, config=c)
else:
- result = meta(tag, config=config)
-
+ 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/286")
-def test_tags_to_versions():
- config = Configuration()
- versions = tags_to_versions(["1.0", "2.0", "3.0"], config=config)
- assert isinstance(versions, list) # enable subscription
+@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=".*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 "Custom %s" % self.tag
+
+ def __repr__(self) -> str:
+ return "MyVersion<Custom%s>" % self.tag
+
+ 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"
--- /dev/null
+from __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"<WD {self.cwd}>"
+
+ 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)
+ 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
[tox]
-envlist=py{27,34,35,36,37,38}-test,flake8,check_readme,py{27,37}-selfcheck
-
-[pytest]
-testpaths=testing
-filterwarnings=error
-markers=
- issue(id): reference to github issue
+envlist=py{38,39,310,311},check_readme,check-dist
+requires= tox>4
[flake8]
max-complexity = 10
max-line-length = 88
ignore=E203,W503
-exclude=
- .git,
- .tox,
- .env,
- .venv,
- .pytest_cache,
- __pycache__,
- ./setuptools_scm/win_py31_compat.py
[testenv]
usedevelop=True
-skip_install=
- selfcheck: True
- test: False
deps=
pytest
- setuptools >= 42
+ setuptools >= 45
+ rich
+ build
commands=
- test: pytest []
- selfcheck: python setup.py --version
-extras =
- toml
+ pytest {posargs}
+
-[testenv:flake8]
-skip_install=True
-deps=
- flake8
- mccabe
-commands =
- flake8 src/setuptools_scm/ testing/ setup.py --exclude=setuptools_scm/win_py31_compat.py
[testenv:check_readme]
skip_install=True
-setenv = SETUPTOOLS_SCM_PRETEND_VERSION=2.0
deps=
- readme
check-manifest
+ docutils
+ pygments
+ typing_extensions
+ hatchling
+ rich
commands=
- python setup.py check -r
- rst2html.py README.rst {envlogdir}/README.html --strict []
- check-manifest
+ check-manifest --no-build-isolation
-[testenv:upload]
+[testenv:check_dist]
+skip_install = true
deps=
- wheel
+ build
twine
commands=
- python setup.py clean --all rotate -k - -m .whl,.tar.gz,.zip
- python setup.py -q egg_info
- python setup.py -q sdist --formats zip bdist_wheel register
-
+ python -m build
+ twine check dist/*
-[testenv:dist]
-deps= wheel
-whitelist_externals = rm
-commands=
- python setup.py -q clean --all
- python setup.py -q rotate -k 0 -m .egg,.zip,.whl,.tar.gz
- python setup.py -q egg_info
- python setup.py -q sdist --formats zip,bztar bdist_wheel upload
-[testenv:devpi]
-deps=
- devpi-client
-commands =
- python setup.py -q egg_info
- devpi upload --from-dir dist
#XXX: envs for hg versions