pax_global_header00006660000000000000000000000064141717642240014522gustar00rootroot0000000000000052 comment=645bd2338cb3be52a2a107054f0e68850fa9a17c setuptools_scm-6.4.2/000077500000000000000000000000001417176422400146165ustar00rootroot00000000000000setuptools_scm-6.4.2/.github/000077500000000000000000000000001417176422400161565ustar00rootroot00000000000000setuptools_scm-6.4.2/.github/FUNDING.yml000066400000000000000000000000361417176422400177720ustar00rootroot00000000000000tidelift: pypi/setuptools-scm setuptools_scm-6.4.2/.github/workflows/000077500000000000000000000000001417176422400202135ustar00rootroot00000000000000setuptools_scm-6.4.2/.github/workflows/python-tests.yml000066400000000000000000000077071417176422400234320ustar00rootroot00000000000000name: python tests+artifacts+release on: pull_request: push: branches: - main tags: - "v*" release: types: [published] jobs: test: runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: python_version: [ '3.6', '3.7', '3.8', '3.9', '3.10', 'pypy3' ] os: [windows-latest, ubuntu-latest] #, macos-latest] include: - os: windows-latest python_version: 'msys2' name: ${{ matrix.os }} - Python ${{ matrix.python_version }} steps: - uses: actions/checkout@v1 - name: Setup python uses: actions/setup-python@v2 if: matrix.python_version != 'msys2' with: python-version: ${{ matrix.python_version }} architecture: x64 - name: Setup MSYS2 uses: msys2/setup-msys2@v2 if: matrix.python_version == 'msys2' with: msystem: MINGW64 install: git mingw-w64-x86_64-python mingw-w64-x86_64-python-setuptools update: true - run: pip install -U 'setuptools>=45' if: matrix.python_version != 'msys2' - run: pip install -e .[toml,test] # pip2 is needed because Mercurial uses python2 on Ubuntu 20.04 - run: | curl https://bootstrap.pypa.io/pip/2.7/get-pip.py --output get-pip.py sudo python2 get-pip.py $(hg debuginstall --template "{pythonexe}") -m pip install hg-git --user if: matrix.os == 'ubuntu-latest' - run: pytest test_legacy_setuptools: runs-on: ubuntu-latest steps: - uses: actions/checkout@v1 - name: Setup python uses: actions/setup-python@v2 with: python-version: "3.6" architecture: x64 - run: pip install -e .[toml,test] pytest virtualenv - run: pytest --test-legacy testing/test_setuptools_support.py || true # ignore fail flaky on ci check_selfinstall: runs-on: ubuntu-latest strategy: fail-fast: false matrix: python_version: [ '3.6', '3.9', 'pypy3' ] installer: ["pip install"] name: check self install - Python ${{ matrix.python_version }} via ${{ matrix.installer }} steps: - uses: actions/checkout@v1 - name: Setup python uses: actions/setup-python@v2 with: python-version: ${{ matrix.python_version }} architecture: x64 # self install testing needs some clarity # so its being executed without any other tools running # setuptools smaller 52 is needed to do easy_install - run: pip install -U "setuptools<52" tomli packaging - run: python setup.py egg_info - run: python setup.py sdist - run: ${{ matrix.installer }} dist/* - run: python testing/check_self_install.py dist: runs-on: ubuntu-latest needs: [test] name: Python sdist/wheel steps: - uses: actions/checkout@v1 - uses: actions/setup-python@v2 with: python-version: "3.8" - name: Install dependencies run: | python -m pip install --upgrade pip pip install --upgrade wheel setuptools build - name: Build package run: python -m build -o dist/ - uses: actions/upload-artifact@v2 with: name: dist path: dist dist_check: runs-on: ubuntu-latest needs: [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 --strict dist/* dist_upload: runs-on: ubuntu-latest if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags') needs: [dist_check] steps: - uses: actions/download-artifact@v2 with: name: dist path: dist - name: Publish package to PyPI uses: pypa/gh-action-pypi-publish@master with: user: __token__ password: ${{ secrets.pypi_token }} setuptools_scm-6.4.2/.gitignore000066400000000000000000000011451417176422400166070ustar00rootroot00000000000000### 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 pip-wheel-metadata # Unit test / coverage reports htmlcov/ .tox/ .coverage .coverage.* .cache .pytest_cache nosetests.xml coverage.xml *,cover .hypothesis/ # Sphinx documentation docs/_build/ setuptools_scm-6.4.2/.pre-commit-config.yaml000066400000000000000000000020261417176422400210770ustar00rootroot00000000000000repos: - repo: https://github.com/psf/black rev: 21.12b0 hooks: - id: black args: [--safe, --quiet] - repo: https://github.com/asottile/reorder_python_imports rev: v2.6.0 hooks: - id: reorder-python-imports args: [ "--application-directories=.:src" , --py3-plus] - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.1.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer - id: check-yaml - id: debug-statements - repo: https://github.com/PyCQA/flake8 rev: 4.0.1 hooks: - id: flake8 - repo: https://github.com/asottile/pyupgrade rev: v2.31.0 hooks: - id: pyupgrade args: [--py36-plus] - repo: https://github.com/asottile/setup-cfg-fmt rev: v1.20.0 hooks: - id: setup-cfg-fmt - repo: https://github.com/pre-commit/mirrors-mypy rev: 'v0.931' hooks: - id: mypy additional_dependencies: - types-setuptools - tokenize-rt==3.2.0 - pytest == 6.2.5 setuptools_scm-6.4.2/CHANGELOG.rst000066400000000000000000000417051417176422400166460ustar00rootroot00000000000000v6.4.2 ====== * fix #671 : NoReturn is not avaliable in painfully dead python 3.6 v6.4.1 ======= * fix regression #669: restore get_version signature * fix #668: harden the selftest 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 * coorectly 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 * 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` (`project.name` or `tool.setuptools_scm.dist_name`) section or `setup.cfg` (`metadata.name`) 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 scms 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 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 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 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 misuse in surogate-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 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 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 its 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, 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 * 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 :code:`write_to` coniguration parameter v1.4.0 ====== * proper 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) setuptools_scm-6.4.2/LICENSE000066400000000000000000000017771417176422400156370ustar00rootroot00000000000000Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. setuptools_scm-6.4.2/MANIFEST.in000066400000000000000000000003501417176422400163520ustar00rootroot00000000000000exclude *.nix exclude .pre-commit-config.yaml include *.py include testing/*.py include tox.ini include *.rst include LICENSE include *.toml include mypy.ini include testing/Dockerfile.busted-buster recursive-include testing *.bash setuptools_scm-6.4.2/README.rst000066400000000000000000000535611417176422400163170ustar00rootroot00000000000000setuptools_scm ============== ``setuptools_scm`` extract 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 of the SCM-managed files to the sdist). Unwanted files must be excluded by discarding them via ``MANIFEST.in``. ``setuptools_scm`` support the following scm out of the box: * git * mercurial .. image:: https://github.com/pypa/setuptools_scm/workflows/python%20tests+artifacts+release/badge.svg :target: https://github.com/pypa/setuptools_scm/actions .. 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>=45", "wheel", "setuptools_scm>=6.2"] That will be sufficient to require ``setuptools_scm`` for projects that support PEP 518 (`pip `_ and `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 (deprecated) ------------------------------- .. warning:: ``setup_requires`` has been deprecated in favor of ``pyproject.toml`` 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 (deprecated) ------------------------------------ as ``setup_requires`` is deprecated in favour of ``pyproject.toml`` usage in ``setup.cfg`` is considered deprecated, please use ``pyproject.toml`` whenever possible. 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 (deprecated)`_ 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 (added in Python 3.8) or the `importlib_metadata`_ backport: .. code:: python from importlib.metadata import version, PackageNotFoundError try: __version__ = version("package-name") except PackageNotFoundError: # package is not installed pass Alternatively, you can use ``pkg_resources`` which is included in ``setuptools`` (but has a significant runtime cost): .. code:: python from pkg_resources import get_distribution, DistributionNotFound try: __version__ = get_distribution("package-name").version except DistributionNotFound: # package is not installed pass However, this does place a runtime dependency on ``setuptools`` and can add up to a few 100ms overhead for the package import time. .. _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 ``importlib.metadata`` after editable/real installation: .. code:: python # contents of docs/conf.py from importlib.metadata import version release = version('myproject') # 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. Usage from Docker ----------------- 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 snipped to infer the version from the host os without copying the entire ``.git`` folder to your Dockerfile. .. code:: 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: .. code:: dockerfile FROM python COPY pyproject.toml ARG PSEUDO_VERSION=1 RUN SETUPTOOLS_SCM_PRETEND_VERSION=${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] `_. 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_${UPPERCASED_DIST_NAME}`` is preferred over ``SETUPTOOLS_SCM_PRETEND_VERSION``. Notable Plugins --------------- `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 `_, 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 `_ 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 `_ 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. 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 `_). :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 `_). :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.version.NonNormalizedVersion`` :version_cls: 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. This defaults to ``packaging.version.Version`` if available. If ``packaging`` is not installed, ``pkg_resources.packaging.version.Version`` is used. Note that it is known to modify git release candidate schemes. 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 ``write_to``) BUT note that setuptools will still normalize it to create the final distribution, so as to stay compliant with the python packaging standards. 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'^(?Pv)?(?P[^\+]+)(?P.*)?$', } ) 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_PRETEND_VERSION_FOR_${UPPERCASED_DIST_NAME}: when defined and not empty, its used as the primary source for the version number in which case it will be a unparsed string it takes precedence over ``SETUPTOOLS_SCM_PRETEND_VERSION`` :SETUPTOOLS_SCM_DEBUG: when defined and not empty, a lot of debug information will be printed as part of ``setuptools_scm`` operating :SOURCE_DATE_EPOCH: when defined, 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: when defined, a ``os.pathsep`` separated list of directory names to ignore for root finding 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`. In case the tag ends with ``.dev0`` the version is not bumped and custom ``.devN`` versions will trigger a error. :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`. :no-guess-dev: Does no next version guessing, just adds :code:`.post1.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. 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 `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 `_. Tidelift will coordinate the fix and disclosure. setuptools_scm-6.4.2/mypy.ini000066400000000000000000000003351417176422400163160ustar00rootroot00000000000000[mypy] python_version = 3.6 warn_return_any = True warn_unused_configs = True mypy_path = $MYPY_CONFIG_FILE_DIR/src [mypy-setuptools_scm.*] # disabled as it will take a bit # disallow_untyped_defs = True # strict = true setuptools_scm-6.4.2/pyproject.toml000066400000000000000000000002211417176422400175250ustar00rootroot00000000000000[build-system] requires = [ "setuptools>=45", "wheel", "tomli>=1.0", "packaging>=20.0" ] build-backend = "setuptools.build_meta" setuptools_scm-6.4.2/setup.cfg000066400000000000000000000053541417176422400164460ustar00rootroot00000000000000[metadata] name = setuptools_scm description = the blessed package to manage your versions by scm tags long_description = file: README.rst long_description_content_type = text/x-rst url = https://github.com/pypa/setuptools_scm/ author = Ronny Pfannschmidt author_email = opensource@ronnypfannschmidt.de license = MIT license_file = LICENSE classifiers = Development Status :: 5 - Production/Stable Intended Audience :: Developers License :: OSI Approved :: MIT License Programming Language :: Python Programming Language :: Python :: 3 Programming Language :: Python :: 3 :: Only Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 Programming Language :: Python :: 3.10 Topic :: Software Development :: Libraries Topic :: Software Development :: Version Control Topic :: System :: Software Distribution Topic :: Utilities [options] packages = find: install_requires = packaging>=20.0 setuptools tomli>=1.0.0 # keep in sync python_requires = >=3.6 package_dir = =src zip_safe = true [options.packages.find] where = src [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.files_command = .hg = setuptools_scm.file_finder_hg:hg_find_files .git = setuptools_scm.file_finder_git:git_find_files 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 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 pyproject.toml = 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 no-guess-dev = setuptools_scm.version:no_guess_dev_version calver-by-date = setuptools_scm.version:calver_by_date setuptools_scm-6.4.2/setup.py000066400000000000000000000034121417176422400163300ustar00rootroot00000000000000"""\ 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 """ import os import sys import setuptools from setuptools.command.bdist_egg import bdist_egg as original_bdist_egg class bdist_egg(original_bdist_egg): def run(self): raise SystemExit( "%s is forbidden, " "please update to setuptools>=45 which uses pip" % type(self).__name__ ) def scm_version(): if sys.version_info < (3, 6): raise RuntimeError( "support for python < 3.6 has been removed in setuptools_scm>=6.0.0" ) here = os.path.dirname(os.path.abspath(__file__)) src = os.path.join(here, "src") sys.path.insert(0, src) from setuptools_scm import get_version from setuptools_scm.hacks import parse_pkginfo from setuptools_scm import git from setuptools_scm import hg from setuptools_scm.version import guess_next_dev_version, get_local_node_and_date def parse(root, config): try: return parse_pkginfo(root, config) except OSError: return git.parse(root, config=config) or hg.parse(root, config=config) return get_version( root=here, parse=parse, version_scheme=guess_next_dev_version, local_scheme=get_local_node_and_date, ) if __name__ == "__main__": setuptools.setup( version=scm_version(), extras_require={ "toml": [ "setuptools>=42", ], "test": ["pytest>=6.2", "virtualenv>20"], }, cmdclass={"bdist_egg": bdist_egg}, ) setuptools_scm-6.4.2/src/000077500000000000000000000000001417176422400154055ustar00rootroot00000000000000setuptools_scm-6.4.2/src/setuptools_scm/000077500000000000000000000000001417176422400204705ustar00rootroot00000000000000setuptools_scm-6.4.2/src/setuptools_scm/__init__.py000066400000000000000000000126071417176422400226070ustar00rootroot00000000000000""" :copyright: 2010-2015 by Ronny Pfannschmidt :license: MIT """ import os import warnings from typing import Optional from typing import TYPE_CHECKING from . import _types from ._entrypoints import _call_entrypoint_fn from ._entrypoints import _version_from_entrypoints from ._overrides import _read_pretended_version_for from ._overrides import PRETEND_KEY from ._overrides import PRETEND_KEY_NAMED from ._version_cls import _version_as_tuple from ._version_cls import NonNormalizedVersion from ._version_cls import Version from .config import Configuration from .config import DEFAULT_LOCAL_SCHEME from .config import DEFAULT_TAG_REGEX from .config import DEFAULT_VERSION_SCHEME from .discover import iter_matching_entrypoints from .utils import function_has_arg from .utils import trace from .version import format_version from .version import meta from .version import ScmVersion if TYPE_CHECKING: from typing import NoReturn TEMPLATES = { ".py": """\ # coding: utf-8 # file generated by setuptools_scm # don't change, don't track in version control version = {version!r} version_tuple = {version_tuple!r} """, ".txt": "{version}", } def version_from_scm(root): warnings.warn( "version_from_scm is deprecated please use get_version", category=DeprecationWarning, stacklevel=2, ) config = Configuration(root=root) return _version_from_entrypoints(config) def dump_version( root: _types.PathT, version: str, write_to: _types.PathT, template: "str | None" = None, ): assert isinstance(version, str) 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 ) ) version_tuple = _version_as_tuple(version) with open(target, "w") as fp: fp.write(template.format(version=version, version_tuple=version_tuple)) def _do_parse(config: Configuration) -> "ScmVersion|None": pretended = _read_pretended_version_for(config) if pretended is not None: return pretended if config.parse: parse_result = _call_entrypoint_fn(config.absolute_root, config, config.parse) if isinstance(parse_result, str): raise TypeError( "version parse result was a string\nplease return a parsed version" ) version: Optional[ScmVersion] if parse_result: assert isinstance(parse_result, ScmVersion) version = parse_result else: version = _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 ) return version def _version_missing(config) -> "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=".", 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, dist_name=None, version_cls=None, normalize=True, search_parent_directories=False, ): """ 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()) maybe_version = _get_version(config) if maybe_version is None: _version_missing(config) return maybe_version def _get_version(config: Configuration) -> "str|None": parsed_version = _do_parse(config) if parsed_version is None: return None version_string = format_version( parsed_version, version_scheme=config.version_scheme, local_scheme=config.local_scheme, ) if config.write_to is not None: dump_version( root=config.root, version=version_string, write_to=config.write_to, template=config.write_to_template, ) return version_string # Public API __all__ = [ "get_version", "dump_version", "version_from_scm", "Configuration", "DEFAULT_VERSION_SCHEME", "DEFAULT_LOCAL_SCHEME", "DEFAULT_TAG_REGEX", "PRETEND_KEY", "PRETEND_KEY_NAMED", "Version", "NonNormalizedVersion", # TODO: are the symbols below part of public API ? "function_has_arg", "trace", "format_version", "meta", "iter_matching_entrypoints", ] setuptools_scm-6.4.2/src/setuptools_scm/__main__.py000066400000000000000000000052001417176422400225570ustar00rootroot00000000000000import argparse import os import sys from setuptools_scm import _get_version from setuptools_scm.config import Configuration from setuptools_scm.discover import walk_potential_roots from setuptools_scm.integration import find_files def main() -> None: opts = _get_cli_opts() root = opts.root or "." try: pyproject = opts.config or _find_pyproject(root) root = opts.root or os.path.relpath(os.path.dirname(pyproject)) config = Configuration.from_file(pyproject, root=root) except (LookupError, FileNotFoundError) as ex: # no pyproject.toml OR no [tool.setuptools_scm] print( f"Warning: could not use {os.path.relpath(pyproject)}," " using default configuration.\n" f" Reason: {ex}.", file=sys.stderr, ) config = Configuration(root=root) version = _get_version(config) assert version is not None 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() -> 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() 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 if __name__ == "__main__": main() setuptools_scm-6.4.2/src/setuptools_scm/_entrypoints.py000066400000000000000000000033401417176422400235770ustar00rootroot00000000000000import warnings from typing import Optional from .config import Configuration from .discover import iter_matching_entrypoints from .utils import function_has_arg from .utils import trace from setuptools_scm.version import ScmVersion def _call_entrypoint_fn(root, config, fn): if function_has_arg(fn, "config"): return fn(root, config=config) else: warnings.warn( f"parse function {fn.__module__}.{fn.__name__}" " are required to provide a named argument" " 'config', setuptools_scm>=8.0 will remove support.", category=DeprecationWarning, stacklevel=2, ) return fn(root) def _version_from_entrypoints( config: Configuration, fallback: bool = False ) -> "ScmVersion|None": if fallback: entrypoint = "setuptools_scm.parse_scm_fallback" root = config.fallback_root else: entrypoint = "setuptools_scm.parse_scm" root = config.absolute_root trace("version_from_ep", entrypoint, root) for ep in iter_matching_entrypoints(root, entrypoint, config): version: Optional[ScmVersion] = _call_entrypoint_fn(root, config, ep.load()) trace(ep, version) if version: return version return None try: from importlib.metadata import entry_points # type: ignore except ImportError: from pkg_resources import iter_entry_points else: def iter_entry_points(group: str, name: Optional[str] = None): all_eps = entry_points() if hasattr(all_eps, "select"): eps = all_eps.select(group=group) else: eps = all_eps[group] if name is None: return iter(eps) return (ep for ep in eps if ep.name == name) setuptools_scm-6.4.2/src/setuptools_scm/_overrides.py000066400000000000000000000020651417176422400232060ustar00rootroot00000000000000import os from typing import Optional from .config import Configuration from .utils import trace from .version import meta from .version import ScmVersion PRETEND_KEY = "SETUPTOOLS_SCM_PRETEND_VERSION" PRETEND_KEY_NAMED = PRETEND_KEY + "_FOR_{name}" def _read_pretended_version_for(config: Configuration) -> Optional[ScmVersion]: """read a a overridden version from the environment tries ``SETUPTOOLS_SCM_PRETEND_VERSION`` and ``SETUPTOOLS_SCM_PRETEND_VERSION_FOR_$UPPERCASE_DIST_NAME`` """ trace("dist name:", config.dist_name) pretended: Optional[str] if config.dist_name is not None: pretended = os.environ.get( PRETEND_KEY_NAMED.format(name=config.dist_name.upper()) ) else: pretended = None if pretended is None: pretended = os.environ.get(PRETEND_KEY) if pretended is not None: # we use meta here since the pretended version # must adhere to the pep to begin with return meta(tag=pretended, preformatted=True, config=config) else: return None setuptools_scm-6.4.2/src/setuptools_scm/_types.py000066400000000000000000000011531417176422400223450ustar00rootroot00000000000000import os from typing import Callable from typing import TYPE_CHECKING from typing import TypeVar from typing import Union if TYPE_CHECKING: from typing_extensions import ParamSpec else: class ParamSpec(list): def __init__(self, _) -> None: pass PathT = Union["os.PathLike[str]", str] T = TypeVar("T") T2 = TypeVar("T2") PARAMS = ParamSpec("PARAMS") def transfer_input_args( template: "Callable[PARAMS, T]", ) -> Callable[[Callable[..., T2]], "Callable[PARAMS, T2]"]: def decorate(func: Callable[..., T2]) -> "Callable[PARAMS, T2]": return func return decorate setuptools_scm-6.4.2/src/setuptools_scm/_version_cls.py000066400000000000000000000043041417176422400235300ustar00rootroot00000000000000from logging import getLogger from typing import Tuple try: from packaging.version import Version, InvalidVersion assert hasattr( Version, "release" ), "broken installation ensure packaging>=20 is available" except ImportError: from pkg_resources._vendor.packaging.version import ( # type: ignore Version as SetuptoolsVersion, InvalidVersion, ) try: SetuptoolsVersion.release Version = SetuptoolsVersion # type: ignore except AttributeError: class Version(SetuptoolsVersion): # type: ignore @property def release(self): return self._version.release @property def dev(self): return self._version.dev @property def local(self): return self._version.local 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): # parse and validate using parent super().__init__(version) # store raw for str self._raw_version = version def __str__(self): # return the non-normalized version (parent returns the normalized) return self._raw_version def __repr__(self): # same pattern as parent return f"" def _version_as_tuple(version_str) -> Tuple["int | str", ...]: try: parsed_version = Version(version_str) except InvalidVersion: log = getLogger("setuptools_scm") log.exception("failed to parse version %s", 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 setuptools_scm-6.4.2/src/setuptools_scm/config.py000066400000000000000000000162351417176422400223160ustar00rootroot00000000000000""" configuration """ import os import re import warnings from typing import Type from typing import TypeVar from . import _types as _t from ._version_cls import NonNormalizedVersion from ._version_cls import Version from .utils import trace DEFAULT_TAG_REGEX = r"^(?:[\w-]+-)?(?P[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: _t.PathT, relative_to: _t.PathT): trace("abs root", repr(locals())) if relative_to: if ( os.path.isabs(root) and not os.path.commonpath([root, relative_to]) == relative_to ): warnings.warn( "absolute root path '%s' overrides relative_to '%s'" % (root, relative_to) ) if os.path.isdir(relative_to): warnings.warn( "relative_to is expected to be a file," " its the directory %r\n" "assuming the parent directory was passed" % (relative_to,) ) trace("dir", relative_to) root = os.path.join(relative_to, root) else: trace("file", relative_to) root = os.path.join(os.path.dirname(relative_to), root) return os.path.abspath(root) def _lazy_tomli_load(data: str): from tomli import loads return loads(data) VersionT = TypeVar("VersionT", Version, NonNormalizedVersion) class Configuration: """Global configuration model""" _root: _t.PathT _relative_to: "_t.PathT | None" version_cls: "Type[Version]|Type[NonNormalizedVersion]" def __init__( self, relative_to: "_t.PathT | None" = None, root: _t.PathT = ".", version_scheme: str = DEFAULT_VERSION_SCHEME, local_scheme=DEFAULT_LOCAL_SCHEME, write_to: "_t.PathT | None" = None, write_to_template: "str|None" = None, tag_regex=DEFAULT_TAG_REGEX, parentdir_prefix_version=None, fallback_version: "str|None" = None, fallback_root: _t.PathT = ".", parse=None, git_describe_command=None, dist_name: str = None, version_cls: "Type[Version]|Type[NonNormalizedVersion]|str|None" = None, normalize: bool = True, search_parent_directories: bool = False, ): # 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 self.dist_name = dist_name self.search_parent_directories = search_parent_directories self.parent = None if not normalize: # `normalize = False` means `version_cls = NonNormalizedVersion` if version_cls is not None: raise ValueError( "Providing a custom `version_cls` is not permitted when " "`normalize=False`" ) self.version_cls = NonNormalizedVersion else: # Use `version_cls` if provided, default to packaging or pkg_resources if version_cls is None: self.version_cls = Version elif isinstance(version_cls, str): try: # Not sure this will work in old python import importlib pkg, cls_name = version_cls.rsplit(".", 1) version_cls_host = importlib.import_module(pkg) self.version_cls = getattr(version_cls_host, cls_name) except: # noqa raise ValueError(f"Unable to import version_cls='{version_cls}'") else: self.version_cls = version_cls @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)) trace("relative_to", repr(value)) @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)) trace("relative_to", repr(self._relative_to)) @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: str = "pyproject.toml", dist_name=None, # type: str | None _load_toml=_lazy_tomli_load, **kwargs, ): """ 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, encoding="UTF-8") as strm: data = strm.read() defn = _load_toml(data) try: section = defn.get("tool", {})["setuptools_scm"] except LookupError as e: raise LookupError( f"{name} does not contain a tool.setuptools_scm section" ) from e if "dist_name" in section: if dist_name is None: dist_name = section.pop("dist_name") else: assert dist_name == section["dist_name"] del section["dist_name"] if dist_name is None: if "project" in defn: # minimal pep 621 support for figuring the pretend keys dist_name = defn["project"].get("name") if dist_name is None: dist_name = _read_dist_name_from_setup_cfg() return cls(dist_name=dist_name, **section, **kwargs) def _read_dist_name_from_setup_cfg(): # minimal effort to read dist_name off setup.cfg metadata import configparser parser = configparser.ConfigParser() parser.read(["setup.cfg"]) dist_name = parser.get("metadata", "name", fallback=None) return dist_name setuptools_scm-6.4.2/src/setuptools_scm/discover.py000066400000000000000000000030251417176422400226600ustar00rootroot00000000000000import os from .config import Configuration from .utils import iter_entry_points from .utils import trace def walk_potential_roots(root, search_parents=True): """ Iterate though a path and each of its parents. :param root: File path. :param search_parents: If ``False`` the parents are not considered. """ if not search_parents: yield root return tail = root while tail: yield root root, tail = os.path.split(root) def match_entrypoint(root, name): """ 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 trace("ignoring bad ep", name) return False def iter_matching_entrypoints(root, entrypoint, config: Configuration): """ 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``. """ trace("looking for ep", entrypoint, root) for wd in walk_potential_roots(root, config.search_parent_directories): for ep in iter_entry_points(entrypoint): if match_entrypoint(wd, ep.name): trace("found ep", ep, "in", wd) config.parent = wd yield ep setuptools_scm-6.4.2/src/setuptools_scm/file_finder.py000066400000000000000000000047721417176422400233220ustar00rootroot00000000000000import os from .utils import trace 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 def is_toplevel_acceptable(toplevel): """ """ if toplevel is None: return False ignored = os.environ.get("SETUPTOOLS_SCM_IGNORE_VCS_ROOTS", "").split(os.pathsep) ignored = [os.path.normcase(p) for p in ignored] trace(toplevel, ignored) return toplevel not in ignored setuptools_scm-6.4.2/src/setuptools_scm/file_finder_git.py000066400000000000000000000062541417176422400241620ustar00rootroot00000000000000import logging import os import subprocess import tarfile from .file_finder import is_toplevel_acceptable from .file_finder import scm_find_files from .utils import do_ex from .utils import trace log = logging.getLogger(__name__) def _git_toplevel(path): try: cwd = os.path.abspath(path or ".") out, err, ret = do_ex(["git", "rev-parse", "HEAD"], cwd=cwd) if ret != 0: # BAIL if there is no commit log.error("listing git files failed - pretending there aren't any") return None out, err, ret = do_ex( ["git", "rev-parse", "--show-prefix"], cwd=cwd, ) if ret != 0: return None out = out.strip()[:-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)] 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, stderr=subprocess.DEVNULL ) 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 (), () def git_find_files(path=""): toplevel = _git_toplevel(path) if not is_toplevel_acceptable(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) setuptools_scm-6.4.2/src/setuptools_scm/file_finder_hg.py000066400000000000000000000027241417176422400237730ustar00rootroot00000000000000import os import subprocess from .file_finder import is_toplevel_acceptable from .file_finder import scm_find_files from .utils import do_ex 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, err, ret = do_ex(["hg", "files"], cwd=toplevel) if ret: (), () 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 is_toplevel_acceptable(toplevel): return [] hg_files, hg_dirs = _hg_ls_files_and_dirs(toplevel) return scm_find_files(path, hg_files, hg_dirs) setuptools_scm-6.4.2/src/setuptools_scm/git.py000066400000000000000000000144471417176422400216370ustar00rootroot00000000000000import os import warnings from datetime import date from datetime import datetime from os.path import isfile from os.path import join from os.path import samefile from .config import Configuration from .scm_workdir import Workdir from .utils import do_ex from .utils import require_command from .utils import trace from .version import meta # 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]*", ] class GitWorkdir(Workdir): """experimental, may change at any time""" COMMAND = "git" @classmethod def from_potential_worktree(cls, wd): require_command(cls.COMMAND) wd = os.path.abspath(wd) real_wd, _, ret = do_ex("git rev-parse --show-prefix", wd) real_wd = real_wd[:-1] # remove the trailing pathsep if ret: return if not real_wd: real_wd = wd else: assert 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 = wd[: -len(real_wd)] trace("real root", real_wd) if not samefile(real_wd, wd): return return cls(real_wd) def is_dirty(self): out, _, _ = self.do_ex("git status --porcelain --untracked-files=no") return bool(out) def get_branch(self): branch, err, ret = self.do_ex("git rev-parse --abbrev-ref HEAD") if ret: trace("branch err", branch, err, ret) branch, err, ret = self.do_ex("git symbolic-ref --short HEAD") if ret: trace("branch err (symbolic-ref)", branch, err, ret) branch = None return branch def get_head_date(self): timestamp, err, ret = self.do_ex("git log -n 1 HEAD --format=%cI") if ret: trace("timestamp err", timestamp, err, ret) return # TODO, when dropping python3.6 use fromiso date_part = timestamp.split("T")[0] if "%c" in date_part: trace("git too old -> timestamp is ", timestamp) return None return datetime.strptime(date_part, r"%Y-%m-%d").date() def is_shallow(self): return isfile(join(self.path, ".git/shallow")) def fetch_shallow(self): self.do_ex("git fetch --unshallow") def node(self): node, _, ret = self.do_ex("git rev-parse --verify --quiet HEAD") if not ret: return node[:7] def count_all_nodes(self): revs, _, _ = self.do_ex("git rev-list HEAD") return revs.count("\n") + 1 def default_describe(self): return self.do_ex(DEFAULT_DESCRIBE) def warn_on_shallow(wd): """experimental, may change at any time""" if wd.is_shallow(): warnings.warn(f'"{wd.path}" is shallow and may cause errors') def fetch_on_shallow(wd): """experimental, may change at any time""" if wd.is_shallow(): warnings.warn(f'"{wd.path}" was shallow, git fetch was used to rectify') wd.fetch_shallow() def fail_on_shallow(wd): """experimental, may change at any time""" if wd.is_shallow(): raise ValueError( f'{wd.path} is shallow, please correct with "git fetch --unshallow"' ) def get_working_directory(config): """ Return the working directory (``GitWorkdir``). """ if config.parent: return GitWorkdir.from_potential_worktree(config.parent) if config.search_parent_directories: return search_parent(config.absolute_root) return GitWorkdir.from_potential_worktree(config.absolute_root) def parse(root, describe_command=None, pre_parse=warn_on_shallow, config=None): """ :param pre_parse: experimental pre_parse action, may change at any time """ if not config: config = Configuration(root=root) wd = get_working_directory(config) if wd: return _git_parse_inner( config, wd, describe_command=describe_command, pre_parse=pre_parse ) def _git_parse_inner(config, wd, pre_parse=None, describe_command=None): if pre_parse: pre_parse(wd) if config.git_describe_command is not None: describe_command = config.git_describe_command if describe_command is not None: out, _, ret = wd.do_ex(describe_command) else: out, _, ret = wd.default_describe() if ret == 0: tag, distance, node, dirty = _git_parse_describe(out) if distance == 0 and not dirty: distance = None else: # If 'git git_describe_command' failed, try to get the information otherwise. tag = "0.0" node = wd.node() if node is None: distance = 0 else: distance = wd.count_all_nodes() node = "g" + node dirty = wd.is_dirty() branch = wd.get_branch() node_date = wd.get_head_date() or date.today() return meta( tag, branch=branch, node=node, node_date=node_date, distance=distance, dirty=dirty, config=config, ) def _git_parse_describe(describe_output): # 'describe_output' looks e.g. like 'v1.5.0-0-g4060507' or # 'v1.15.1rc1-37-g9bd1298-dirty'. if describe_output.endswith("-dirty"): dirty = True describe_output = describe_output[:-6] else: dirty = False tag, number, node = describe_output.rsplit("-", 2) number = int(number) return tag, number, node, dirty def search_parent(dirname): """ Walk up the path to find the `.git` directory. :param dirname: Directory from which to start searching. """ # Code based on: # https://github.com/gitpython-developers/GitPython/blob/main/git/repo/base.py curpath = os.path.abspath(dirname) while curpath: try: wd = GitWorkdir.from_potential_worktree(curpath) except Exception: wd = None if wd is not None: return wd curpath, tail = os.path.split(curpath) if not tail: return None setuptools_scm-6.4.2/src/setuptools_scm/hacks.py000066400000000000000000000024761417176422400221440ustar00rootroot00000000000000import os from .utils import data_from_mime from .utils import trace from .version import meta from .version import tag_to_version 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: trace("FALLBACK") return meta(config.fallback_version, preformatted=True, config=config) setuptools_scm-6.4.2/src/setuptools_scm/hg.py000066400000000000000000000117221417176422400214430ustar00rootroot00000000000000import os from pathlib import Path from .config import Configuration from .scm_workdir import Workdir from .utils import data_from_mime from .utils import do_ex from .utils import require_command from .utils import trace from .version import meta from .version import tag_to_version class HgWorkdir(Workdir): COMMAND = "hg" @classmethod def from_potential_worktree(cls, wd): require_command(cls.COMMAND) root, err, ret = do_ex("hg root", wd) if ret: return return cls(root) def get_meta(self, config): node, tags, bookmark, node_date = 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, dirty_date = self.do( ["hg", "id", "-T", "{branch}\n{if(dirty, 1, 0)}\n{date|shortdate}"] ).split("\n") dirty = bool(int(dirty)) if dirty: date = dirty_date else: date = node_date if all(c == "0" for c in node): trace("initial node", self.path) return meta("0.0", config=config, dirty=dirty, branch=branch) node = "h" + node[:7] tags = tags.split() if "tip" in tags: # tip is not a real tag tags = tags.remove("tip") if tags: tag = tags[0] tag = tag_to_version(tag) if tag: return meta(tag, dirty=dirty, branch=branch, config=config) try: tag = self.get_latest_normalizable_tag() dist = self.get_distance_revs(tag) if tag == "null": tag = "0.0" dist = int(dist) + 1 if self.check_changes_since_tag(tag) or dirty: return meta( tag, distance=dist, node=node, dirty=dirty, branch=branch, config=config, node_date=date, ) else: return meta(tag, config=config) except ValueError: pass # unpacking failed, old hg def hg_log(self, revset, template): cmd = ["hg", "log", "-r", revset, "-T", template] return self.do(cmd) def get_latest_normalizable_tag(self): # 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 "null" tag = outlines[-1].split()[-1] return tag def get_distance_revs(self, rev1, rev2="."): revset = f"({rev1}::{rev2})" out = self.hg_log(revset, ".") return len(out) - 1 def check_changes_since_tag(self, tag): if tag == "0.0": return True revset = ( "(branch(.)" # look for revisions in this branch only f" and tag({tag!r})::." # after the last tag # ignore commits that only modify .hgtags and nothing else: " and (merge() or file('re:^(?!\\.hgtags).*$'))" f" and not tag({tag!r}))" # ignore the tagged commit itself ) return bool(self.hg_log(revset, ".")) def parse(root, config=None): if not config: config = Configuration(root=root) if os.path.exists(os.path.join(root, ".hg/git")): paths, _, ret = do_ex("hg path", root) if not ret: for line in paths.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 = GitWorkdirHgClient.from_potential_worktree(root) if wd: return _git_parse_inner(config, wd) wd = HgWorkdir.from_potential_worktree(config.absolute_root) if wd is None: return return wd.get_meta(config) def archival_to_version(data, config: "Configuration | None" = None): trace("data", data) node = data.get("node", "")[:12] if node: node = "h" + node if "tag" in data: return meta(data["tag"], config=config) elif "latesttag" in data: return meta( data["latesttag"], distance=data["latesttagdistance"], node=node, config=config, ) else: return meta("0.0", node=node, config=config) def parse_archival(root, config=None): archival = os.path.join(root, ".hg_archival.txt") data = data_from_mime(archival) return archival_to_version(data, config=config) setuptools_scm-6.4.2/src/setuptools_scm/hg_git.py000066400000000000000000000066521417176422400223140ustar00rootroot00000000000000import os from datetime import datetime from .git import GitWorkdir from .hg import HgWorkdir from .utils import do_ex from .utils import require_command from .utils import trace class GitWorkdirHgClient(GitWorkdir, HgWorkdir): COMMAND = "hg" @classmethod def from_potential_worktree(cls, wd): require_command(cls.COMMAND) root, err, ret = do_ex("hg root", wd) if ret: return return cls(root) def is_dirty(self): out, _, _ = self.do_ex("hg id -T '{dirty}'") return bool(out) def get_branch(self): branch, err, ret = self.do_ex("hg id -T {bookmarks}") if ret: trace("branch err", branch, err, ret) return return branch def get_head_date(self): date_part, err, ret = self.do_ex("hg log -r . -T {shortdate(date)}") if ret: trace("head date err", date_part, err, ret) return return datetime.strptime(date_part, r"%Y-%m-%d").date() def is_shallow(self): return False def fetch_shallow(self): pass def get_hg_node(self): node, _, ret = self.do_ex("hg log -r . -T {node}") if not ret: return node def _hg2git(self, hg_node): git_node = None with open(os.path.join(self.path, ".hg/git-mapfile")) as file: for line in file: if hg_node in line: git_node, hg_node = line.split() break return git_node def node(self): hg_node = self.get_hg_node() if hg_node is None: return git_node = self._hg2git(hg_node) if git_node is None: # trying again after hg -> git self.do_ex("hg gexport") git_node = self._hg2git(hg_node) if git_node is None: trace("Cannot get git node so we use hg node", 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): revs, _, _ = self.do_ex("hg log -r 'ancestors(.)' -T '.'") return len(revs) def default_describe(self): """ Tentative to reproduce the output of `git describe --dirty --tags --long --match *[0-9]*` """ hg_tags, _, ret = self.do_ex( [ "hg", "log", "-r", "(reverse(ancestors(.)) and tag(r're:[0-9]'))", "-T", "{tags}{if(tags, ' ', '')}", ] ) if ret: return None, None, None hg_tags = hg_tags.split() if not hg_tags: return None, None, None git_tags = {} with open(os.path.join(self.path, ".hg/git-tags")) as file: for line in file: node, tag = line.split() git_tags[tag] = node # find the first hg tag which is also a git tag for tag in hg_tags: if tag in git_tags: break out, _, ret = self.do_ex(["hg", "log", "-r", f"'{tag}'::.", "-T", "."]) if ret: return None, None, None distance = len(out) - 1 node = self.node() desc = f"{tag}-{distance}-g{node}" if self.is_dirty(): desc += "-dirty" return desc, None, 0 setuptools_scm-6.4.2/src/setuptools_scm/integration.py000066400000000000000000000056331417176422400233740ustar00rootroot00000000000000import os import warnings import setuptools from . import _get_version from . import _version_missing from .config import _read_dist_name_from_setup_cfg from .config import Configuration from .utils import do from .utils import iter_entry_points from .utils import trace def _warn_on_old_setuptools(_version=setuptools.__version__): if int(_version.split(".")[0]) < 45: warnings.warn( RuntimeWarning( f""" ERROR: setuptools=={_version} is used in combination with setuptools_scm>=6.x Your build configuration is incomplete and previously worked by accident! This happens as setuptools is unable to replace itself when a activated build dependency requires a more recent setuptools version (it does not respect "setuptools>X" in setup_requires). setuptools>=31 is required for setup.cfg metadata support setuptools>=42 is required for pyproject.toml configuration support Suggested workarounds if applicable: - preinstalling build dependencies like setuptools_scm before running setup.py - installing setuptools_scm using the system package manager to ensure consistency - 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 """ ) ) _warn_on_old_setuptools() def _assign_version(dist: setuptools.Distribution, config: Configuration): maybe_version = _get_version(config) if maybe_version is None: _version_missing(config) else: dist.metadata.version = maybe_version def version_keyword(dist: setuptools.Distribution, keyword, value): if not value: return if value is True: value = {} if getattr(value, "__call__", None): value = value() assert ( "dist_name" not in value ), "dist_name may not be specified in the setup keyword " trace( "version keyword", vars(dist.metadata), ) dist_name = dist.metadata.name # type: str | None if dist_name is None: dist_name = _read_dist_name_from_setup_cfg() config = Configuration(dist_name=dist_name, **value) _assign_version(dist, 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 infer_version(dist: setuptools.Distribution): trace( "finalize hook", vars(dist.metadata), ) dist_name = dist.metadata.name if not os.path.isfile("pyproject.toml"): return try: config = Configuration.from_file(dist_name=dist_name) except LookupError as e: trace(e) else: _assign_version(dist, config) setuptools_scm-6.4.2/src/setuptools_scm/scm_workdir.py000066400000000000000000000005021417176422400233620ustar00rootroot00000000000000from .utils import do from .utils import do_ex from .utils import require_command class Workdir: def __init__(self, path): require_command(self.COMMAND) self.path = path def do_ex(self, cmd): return do_ex(cmd, cwd=self.path) def do(self, cmd): return do(cmd, cwd=self.path) setuptools_scm-6.4.2/src/setuptools_scm/utils.py000066400000000000000000000070141417176422400222040ustar00rootroot00000000000000""" utils """ import inspect import os import platform import shlex import subprocess import sys import warnings DEBUG = bool(os.environ.get("SETUPTOOLS_SCM_DEBUG")) IS_WINDOWS = platform.system() == "Windows" 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) -> None: if DEBUG: print(*k, file=sys.stderr, flush=True) 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: 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)) trace(" in", cwd) 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 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 def function_has_arg(fn, argname): assert inspect.isfunction(fn) argspec = inspect.signature(fn).parameters return argname in argspec def has_command(name: str, warn: bool = True) -> bool: try: p = _popen_pipes([name, "help"], ".") except OSError: trace(*sys.exc_info()) res = False else: p.communicate() res = not p.returncode if not res and warn: warnings.warn("%r was not found" % name, category=RuntimeWarning) return res def require_command(name): if not has_command(name, warn=False): raise OSError("%r was not found" % name) def iter_entry_points(*k, **kw): from ._entrypoints import iter_entry_points return iter_entry_points(*k, **kw) setuptools_scm-6.4.2/src/setuptools_scm/version.py000066400000000000000000000353331417176422400225360ustar00rootroot00000000000000import os import re import warnings from datetime import datetime from datetime import timezone from typing import Callable from typing import Iterator from typing import List from typing import overload from typing import Tuple from .config import Configuration from .config import Version as PkgVersion from .utils import iter_entry_points from .utils import trace SEMVER_MINOR = 2 SEMVER_PATCH = 3 SEMVER_LEN = 3 def _parse_version_tag(tag, config): tagstring = tag if isinstance(tag, str) else str(tag) match = config.tag_regex.match(tagstring) result = None if match: if len(match.groups()) == 1: key = 1 else: key = "version" result = { "version": match.group(key), "prefix": match.group(0)[: match.start(key)], "suffix": match.group(0)[match.end(key) :], } trace(f"tag '{tag}' parsed to {result}") return result def callable_or_entrypoint(group, callable_or_name): trace("ep", (group, callable_or_name)) if callable(callable_or_name): return callable_or_name for ep in iter_entry_points(group, callable_or_name): trace("ep found:", ep.name) return ep.load() def tag_to_version(tag, config: "Configuration | None" = 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) if not config: config = Configuration() tagdict = _parse_version_tag(tag, config) if not isinstance(tagdict, dict) or not tagdict.get("version", None): warnings.warn(f"tag {tag!r} no version found") return None version = tagdict["version"] trace("version pre parse", version) if tagdict.get("suffix", ""): warnings.warn( "tag {!r} will be stripped of its suffix '{}'".format( tag, tagdict["suffix"] ) ) version = config.version_cls(version) trace("version", repr(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 """ result = [] for tag in tags: tag = tag_to_version(tag, config=config) if tag: result.append(tag) return result class ScmVersion: def __init__( self, tag_version: str, distance: "int|None" = None, node: "str|None" = None, dirty: bool = False, preformatted: bool = False, branch: "str|None" = None, config: "Configuration|None" = None, node_date=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.node_date = node_date if "SOURCE_DATE_EPOCH" in os.environ: date_epoch = int(os.environ["SOURCE_DATE_EPOCH"]) self.time = datetime.fromtimestamp(date_epoch, timezone.utc) else: self.time = datetime.now(timezone.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, ) return self._extra @property def exact(self): return self.distance is None def __repr__(self): return self.format_with( "" ) def format_with(self, fmt, **kw): return fmt.format( time=self.time, tag=self.tag, distance=self.distance, node=self.node, dirty=self.dirty, branch=self.branch, node_date=self.node_date, **kw, ) def format_choice(self, clean_format, dirty_format, **kw): 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) return self.format_with(fmt, guessed=guessed) def _parse_tag(tag, preformatted, config: "Configuration|None"): if preformatted: return tag if config is None or not isinstance(tag, config.version_cls): tag = tag_to_version(tag, config) return tag def meta( tag, distance: "int|None" = None, dirty: bool = False, node: "str|None" = None, preformatted: bool = False, branch: "str|None" = None, config: "Configuration|None" = None, **kw, ) -> ScmVersion: if not config: warnings.warn( "meta invoked without explicit configuration," " will use defaults where required." ) parsed_version = _parse_tag(tag, preformatted, config) trace("version", 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 ) def guess_next_version(tag_version: ScmVersion): version = _strip_local(str(tag_version)) return _bump_dev(version) or _bump_regex(version) def _dont_guess_next_version(tag_version: ScmVersion): version = _strip_local(str(tag_version)) return _bump_dev(version) or _add_post(version) def _strip_local(version_string: str) -> str: public, sep, local = version_string.partition("+") return public def _add_post(version: 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( "{version} does not end with a number to bump, " "please correct or use a custom version scheme".format(version=version) ) else: prefix, tail = match.groups() return "%s%d" % (prefix, int(tail) + 1) def guess_next_dev_version(version: ScmVersion): if version.exact: return version.format_with("{tag}") else: return version.format_next_version(guess_next_version) def guess_next_simple_semver(version: str, retain: int, increment=True): try: parts = [int(i) for i in str(version).split(".")[:retain]] except ValueError: raise ValueError(f"{version} can't be parsed as numeric version") while len(parts) < retain: parts.append(0) if increment: parts[-1] += 1 while len(parts) < SEMVER_LEN: parts.append(0) return ".".join(str(i) for i in parts) def simplified_semver_version(version): if version.exact: return guess_next_simple_semver(version.tag, retain=SEMVER_LEN, increment=False) else: if version.branch is not None and "feature" in version.branch: return version.format_next_version( guess_next_simple_semver, retain=SEMVER_MINOR ) else: return version.format_next_version( guess_next_simple_semver, retain=SEMVER_PATCH ) def release_branch_semver_version(version): 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 = branch_ver["version"] if branch_ver[0] == "v": # Allow branches that start with 'v', similar to Version. branch_ver = branch_ver[1:] # Does the branch version up to the minor part match the tag? If not it # might be like, an issue number or something and not a version number, so # we only want to use it if it matches. tag_ver_up_to_minor = str(version.tag).split(".")[:SEMVER_MINOR] branch_ver_up_to_minor = branch_ver.split(".")[:SEMVER_MINOR] if branch_ver_up_to_minor == tag_ver_up_to_minor: # We're in a release/maintenance branch, next is a patch/rc/beta bump: return version.format_next_version(guess_next_version) # We're in a development branch, next is a minor bump: return version.format_next_version(guess_next_simple_semver, retain=SEMVER_MINOR) def release_branch_semver(version): warnings.warn( "release_branch_semver is deprecated and will be removed in future. " + "Use release_branch_semver_version instead", category=DeprecationWarning, stacklevel=2, ) return release_branch_semver_version(version) def no_guess_dev_version(version: ScmVersion): if version.exact: return version.format_with("{tag}") else: return version.format_next_version(_dont_guess_next_version) def date_ver_match(ver): match = re.match( ( r"^(?P(?P\d{2}|\d{4})(?:\.\d{1,2}){2})" r"(?:\.(?P\d*)){0,1}?$" ), str(ver), ) return match def guess_next_date_ver(version, node_date=None, date_fmt=None, version_cls=None): """ same-day -> patch +1 other-day -> today distance is always added as .devX """ match = date_ver_match(version) 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" # 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" today = datetime.now(timezone.utc).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).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( "your previous tag ({}) is ahead your node date ({})".format( tag_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): 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 _format_local_with_time(version, time_format): 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 get_local_node_and_date(version): return _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_dirty_tag(version): return version.format_choice("", "+dirty") def get_no_local_node(_): return "" def postrelease_version(version): if version.exact: return version.format_with("{tag}") else: return version.format_with("{tag}.post{distance}") def _get_ep(group, name): for ep in iter_entry_points(group, name): trace("ep found:", ep.name) return ep.load() def _iter_version_schemes( entrypoint: str, scheme_value: "str|List[str]|Tuple[str, ...]", _memo=None ) -> Iterator[Callable[["ScmVersion"], str]]: if _memo is None: _memo = set() if isinstance(scheme_value, str): scheme_value = _get_ep(entrypoint, scheme_value) 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: ScmVersion, entypoint: str, given_value: str, default: str ) -> str: ... @overload def _call_version_scheme( version: ScmVersion, entypoint: str, given_value: str, default: None ) -> "str|None": ... def _call_version_scheme( version: ScmVersion, entypoint: str, given_value: str, default: "str|None" ) -> "str|None": for scheme in _iter_version_schemes(entypoint, given_value): result = scheme(version) if result is not None: return result return default def format_version(version: ScmVersion, **config) -> str: trace("scm version", version) trace("config", config) if version.preformatted: return version.tag main_version = _call_version_scheme( version, "setuptools_scm.version_scheme", config["version_scheme"], None ) trace("version", main_version) assert main_version is not None local_version = _call_version_scheme( version, "setuptools_scm.local_scheme", config["local_scheme"], "+unknown" ) trace("local_version", local_version) return main_version + local_version setuptools_scm-6.4.2/testing/000077500000000000000000000000001417176422400162735ustar00rootroot00000000000000setuptools_scm-6.4.2/testing/Dockerfile.busted-buster000066400000000000000000000002771417176422400230620ustar00rootroot00000000000000FROM debian:buster RUN apt-get update -q && apt-get install -yq python3-pip python3-setuptools RUN printf "[easy_install]\nallow_hosts=localhost\nfind_links=/dist\n" > /root/.pydistutils.cfg setuptools_scm-6.4.2/testing/check_self_install.py000066400000000000000000000002471417176422400224640ustar00rootroot00000000000000import pkg_resources import setuptools_scm dist = pkg_resources.get_distribution("setuptools_scm") assert dist.version == setuptools_scm.get_version(), dist.version setuptools_scm-6.4.2/testing/conftest.py000066400000000000000000000064341417176422400205010ustar00rootroot00000000000000import itertools import os import pytest # 2009-02-13T23:31:30+00:00 os.environ["SOURCE_DATE_EPOCH"] = "1234567890" os.environ["SETUPTOOLS_SCM_DEBUG"] = "1" VERSION_PKGS = ["setuptools", "setuptools_scm"] def pytest_report_header(): import pkg_resources res = [] for pkg in VERSION_PKGS: version = pkg_resources.get_distribution(pkg).version path = __import__(pkg).__file__ res.append(f"{pkg} version {version} from {path!r}") return res def pytest_addoption(parser): group = parser.getgroup("setuptools_scm") group.addoption( "--test-legacy", dest="scm_test_virtualenv", default=False, action="store_true" ) class Wd: commit_command = None add_command = None def __repr__(self): return f"" def __init__(self, cwd): self.cwd = cwd self.__counter = itertools.count() def __call__(self, cmd, **kw): if kw: cmd = cmd.format(**kw) from setuptools_scm.utils import do return do(cmd, self.cwd) def write(self, name, value, **kw): filename = self.cwd / name if kw: value = value.format(**kw) if isinstance(value, bytes): filename.write_bytes(value) else: filename.write_text(value) return filename def _reason(self, given_reason): if given_reason is None: return f"number-{next(self.__counter)}" else: return given_reason def add_and_commit(self, reason=None): self(self.add_command) self.commit(reason) 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) 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 @property def version(self): __tracebackhide__ = True return self.get_version() @pytest.fixture(autouse=True) def debug_mode(): from setuptools_scm import utils utils.DEBUG = True yield utils.DEBUG = False @pytest.fixture def wd(tmp_path): target_wd = tmp_path.resolve() / "wd" target_wd.mkdir() return Wd(target_wd) @pytest.fixture def repositories_hg_git(tmp_path): from setuptools_scm.utils import do tmp_path = tmp_path.resolve() path_git = tmp_path / "repo_git" path_git.mkdir() wd = Wd(path_git) wd("git init") wd("git config user.email test@example.com") wd('git config user.name "a test"') wd.add_command = "git add ." wd.commit_command = "git commit -m test-{reason}" path_hg = tmp_path / "repo_hg" do(f"hg clone {path_git} {path_hg} --config extensions.hggit=") assert path_hg.exists() with open(path_hg / ".hg/hgrc", "a") as file: file.write("[extensions]\nhggit =\n") wd_hg = Wd(path_hg) wd_hg.add_command = "hg add ." wd_hg.commit_command = 'hg commit -m test-{reason} -u test -d "0 0"' return wd_hg, wd setuptools_scm-6.4.2/testing/play_out_381.bash000077500000000000000000000011011417176422400213550ustar00rootroot00000000000000#!/usr/bin/env bash set -euxo pipefail rm -rf y z home venv tmp [ ! -d black ] && git clone https://github.com/psf/black export SETUPTOOLS_SCM_DEBUG=1 export PRE_COMMIT_HOME="$PWD/home" export TMPDIR="$PWD/tmp" git init y git init z git -C z commit --allow-empty -m 'commit!' git -C y submodule add "$PWD/z" cat > "$PWD/y/.git/modules/z/hooks/pre-commit" < str: return tmp_path.joinpath(name).read_text() assert read("first.txt") == "1.0" dump_version(tmp_path, "1.0.dev42", "first.py") lines = read("first.py").splitlines() assert "version = '1.0.dev42'" in lines assert "version_tuple = (1, 0, 'dev42')" in lines dump_version(tmp_path, "1.0.1+g4ac9d2c", "second.py") lines = read("second.py").splitlines() assert "version = '1.0.1+g4ac9d2c'" in lines assert "version_tuple = (1, 0, 1, 'g4ac9d2c')" in lines dump_version(tmp_path, "1.2.3.dev18+gb366d8b.d20210415", "third.py") lines = read("third.py").splitlines() assert "version = '1.2.3.dev18+gb366d8b.d20210415'" in lines assert "version_tuple = (1, 2, 3, 'dev18', 'gb366d8b.d20210415')" in lines import ast ast.parse(read("third.py")) def test_parse_plain_fails(recwarn): def parse(root): return "tricked you" with pytest.raises(TypeError): setuptools_scm.get_version(parse=parse) def test_custom_version_cls(): """Test that `normalize` and `version_cls` work as expected""" class MyVersion: def __init__(self, tag_str: str): self.version = tag_str def __repr__(self): return f"hello,{self.version}" # you can not use normalize=False and version_cls at the same time with pytest.raises(ValueError): 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" setuptools_scm-6.4.2/testing/test_config.py000066400000000000000000000022751417176422400211570ustar00rootroot00000000000000import re import textwrap import pytest from setuptools_scm.config import Configuration @pytest.mark.parametrize( "tag, expected_version", [ ("apache-arrow-0.9.0", "0.9.0"), ("arrow-0.9.0", "0.9.0"), ("arrow-0.9.0-rc", "0.9.0-rc"), ("arrow-1", "1"), ("arrow-1+", "1"), ("arrow-1+foo", "1"), ("arrow-1.1+foo", "1.1"), ("v1.1", "v1.1"), ("V1.1", "V1.1"), ], ) def test_tag_regex(tag, expected_version): config = Configuration() match = config.tag_regex.match(tag) assert match version = match.group("version") assert version == expected_version def test_config_from_pyproject(tmpdir): fn = tmpdir / "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(): tag_regex = re.compile(r"v(\d+)") conf = Configuration(tag_regex=tag_regex) assert conf.tag_regex is tag_regex setuptools_scm-6.4.2/testing/test_file_finder.py000066400000000000000000000142271417176422400221600ustar00rootroot00000000000000import os import sys import pytest from setuptools_scm.integration import find_files @pytest.fixture(params=["git", "hg"]) def inwd(request, wd, monkeypatch): if request.param == "git": try: wd("git init") except OSError: pytest.skip("git executable not found") wd("git config user.email test@example.com") wd('git config user.name "a test"') wd.add_command = "git add ." wd.commit_command = "git commit -m test-{reason}" elif request.param == "hg": try: wd("hg init") except OSError: pytest.skip("hg executable not found") wd.add_command = "hg add ." wd.commit_command = 'hg commit -m test-{reason} -u test -d "0 0"' (wd.cwd / "file1").touch() adir = wd.cwd / "adir" adir.mkdir() (adir / "filea").touch() bdir = wd.cwd / "bdir" bdir.mkdir() (bdir / "fileb").touch() if request.node.get_closest_marker("skip_commit") is None: wd.add_and_commit() monkeypatch.chdir(wd.cwd) yield wd def _sep(paths): return {path.replace("/", os.path.sep) for path in paths} def test_basic(inwd): 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): (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): (inwd.cwd / "CamelFile").touch() (inwd.cwd / "file2").touch() inwd.add_and_commit() assert set(find_files()) == _sep( {"CamelFile", "file2", "file1", "adir/filea", "bdir/fileb"} ) @pytest.mark.skipif(sys.platform == "win32", reason="symlinks to dir not supported") def test_symlink_dir(inwd): (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): (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): (inwd.cwd / "adir" / "file1link").symlink_to("../file1") inwd.add_and_commit() assert set(find_files("adir")) == _sep( {"adir/filea", "adir/file1link"} ) # -> ../file1 @pytest.mark.skipif( sys.platform == "win32", reason="symlinks to files not supported on windows" ) def test_symlink_file_source_not_in_scm(inwd): (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): (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): (inwd.cwd / "bdir" / "loop").symlink_to("../bdir") (inwd.cwd / "adir" / "bdirlink").symlink_to("../bdir") inwd.add_and_commit() assert set(find_files("adir")) == _sep({"adir/filea", "adir/bdirlink/fileb"}) @pytest.mark.skipif(sys.platform == "win32", reason="symlinks to dir not supported") def test_symlink_dir_out_of_git(inwd): (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): (inwd.cwd / "adir" / "outsidefilelink").symlink_to(__file__) inwd.add_and_commit() assert set(find_files("adir")) == _sep({"adir/filea"}) @pytest.mark.parametrize("path_add", ["{cwd}", "{cwd}" + os.pathsep + "broken"]) def test_ignore_root(inwd, monkeypatch, path_add): monkeypatch.setenv("SETUPTOOLS_SCM_IGNORE_VCS_ROOTS", path_add.format(cwd=inwd.cwd)) assert find_files() == [] def test_empty_root(inwd): subdir = inwd.cwd / "cdir" / "subdir" subdir.mkdir(parents=True) (subdir / "filec").touch() inwd.add_and_commit() assert set(find_files("cdir")) == _sep({"cdir/subdir/filec"}) def test_empty_subdir(inwd): subdir = inwd.cwd / "adir" / "emptysubdir" / "subdir" subdir.mkdir(parents=True) (subdir / "xfile").touch() inwd.add_and_commit() assert set(find_files("adir")) == _sep( {"adir/filea", "adir/emptysubdir/subdir/xfile"} ) @pytest.mark.skipif(sys.platform == "win32", reason="symlinks not supported on windows") def test_double_include_through_symlink(inwd): (inwd.cwd / "data").mkdir() (inwd.cwd / "data" / "datafile").touch() (inwd.cwd / "adir" / "datalink").symlink_to("../data") (inwd.cwd / "adir" / "filealink").symlink_to("filea") inwd.add_and_commit() assert set(find_files()) == _sep( { "file1", "adir/datalink", # -> ../data "adir/filealink", # -> filea "adir/filea", "bdir/fileb", "data/datafile", } ) @pytest.mark.skipif(sys.platform == "win32", reason="symlinks not supported on windows") def test_symlink_not_in_scm_while_target_is(inwd): (inwd.cwd / "data").mkdir() (inwd.cwd / "data" / "datafile").touch() inwd.add_and_commit() (inwd.cwd / "adir" / "datalink").symlink_to("../data") (inwd.cwd / "adir" / "filealink").symlink_to("filea") assert set(find_files()) == _sep( { "file1", "adir/filea", # adir/datalink and adir/afilelink not included # because the symlink_to themselves are not in scm "bdir/fileb", "data/datafile", } ) @pytest.mark.issue(587) @pytest.mark.skip_commit def test_not_commited(inwd): assert find_files() == [] setuptools_scm-6.4.2/testing/test_functions.py000066400000000000000000000066141417176422400217230ustar00rootroot00000000000000from pathlib import Path import pkg_resources import pytest from setuptools_scm import dump_version from setuptools_scm import get_version from setuptools_scm import PRETEND_KEY from setuptools_scm.config import Configuration from setuptools_scm.utils 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 @pytest.mark.parametrize( "tag, expected", [ ("1.1", "1.2"), ("1.2.dev", "1.2"), ("1.1a2", "1.1a3"), ("23.24.post2+deadbeef", "23.24.post3"), ], ) def test_next_tag(tag, expected): version = pkg_resources.parse_version(tag) 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), } @pytest.mark.parametrize( "version,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+d20090213"), ("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+d20090213"), ("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+d20090213"), ("distance", "post-release node-and-date", "1.1.post3"), ("distancedirty", "post-release node-and-date", "1.1.post3+d20090213"), ], ) def test_format_version(version, scheme, expected): version = VERSIONS[version] vs, ls = scheme.split() assert format_version(version, version_scheme=vs, local_scheme=ls) == expected def test_dump_version_doesnt_bail_on_value_error(tmp_path): write_to = "VERSION" version = str(VERSIONS["exact"].tag) with pytest.raises(ValueError, match="^bad file format:"): dump_version(tmp_path, version, write_to) @pytest.mark.parametrize( "version", ["1.0", "1.2.3.dev1+ge871260", "1.2.3.dev15+ge871260.d20180625"] ) def test_dump_version_works_with_pretend( version: str, tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: monkeypatch.setenv(PRETEND_KEY, version) target = tmp_path.joinpath("VERSION.txt") get_version(write_to=target) assert target.read_text() == version def test_has_command() -> None: with pytest.warns(RuntimeWarning, match="yadayada"): assert not has_command("yadayada_setuptools_aint_ne") @pytest.mark.parametrize( "tag, expected_version", [ ("1.1", "1.1"), ("release-1.1", "1.1"), pytest.param("3.3.1-rc26", "3.3.1rc26", marks=pytest.mark.issue(266)), ], ) def test_tag_to_version(tag: str, expected_version: str) -> None: version = str(tag_to_version(tag)) assert version == expected_version setuptools_scm-6.4.2/testing/test_git.py000066400000000000000000000276221417176422400205000ustar00rootroot00000000000000import os import sys from datetime import date from datetime import datetime from datetime import timezone from os.path import join as opj from unittest.mock import Mock from unittest.mock import patch import pytest from setuptools_scm import git from setuptools_scm import integration from setuptools_scm import NonNormalizedVersion from setuptools_scm.file_finder_git import git_find_files from setuptools_scm.utils import do from setuptools_scm.utils import has_command pytestmark = pytest.mark.skipif( not has_command("git", warn=False), reason="git executable not found" ) @pytest.fixture def wd(wd, monkeypatch): 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}" return wd @pytest.mark.parametrize( "given, tag, number, node, dirty", [ ("3.3.1-rc26-0-g9df187b", "3.3.1-rc26", 0, "g9df187b", False), ("17.33.0-rc-17-g38c3047c0", "17.33.0-rc", 17, "g38c3047c0", False), ], ) def test_parse_describe_output(given, tag, number, node, dirty): parsed = git._git_parse_describe(given) assert parsed == (tag, number, node, dirty) def test_root_relative_to(tmpdir, wd, monkeypatch): monkeypatch.delenv("SETUPTOOLS_SCM_DEBUG") p = wd.cwd.joinpath("sub/package") p.mkdir(parents=True) p.joinpath("setup.py").write_text( """from setuptools import setup setup(use_scm_version={"root": "../..", "relative_to": __file__}) """ ) res = do((sys.executable, "setup.py", "--version"), p) assert res == "0.1.dev0" def test_root_search_parent_directories(tmpdir, wd, monkeypatch): 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 = do((sys.executable, "setup.py", "--version"), p) assert res == "0.1.dev0" def test_git_gone(wd, monkeypatch): monkeypatch.setenv("PATH", str(wd.cwd / "not-existing")) with pytest.raises(EnvironmentError, match="'git' was not found"): git.parse(str(wd.cwd), git.DEFAULT_DESCRIBE) @pytest.mark.issue("https://github.com/pypa/setuptools_scm/issues/298") @pytest.mark.issue(403) def test_file_finder_no_history(wd, caplog): file_list = git_find_files(str(wd.cwd)) assert file_list == [] assert "listing git files failed - pretending there aren't any" in caplog.text @pytest.mark.issue("https://github.com/pypa/setuptools_scm/issues/281") def test_parse_call_order(wd): git.parse(str(wd.cwd), git.DEFAULT_DESCRIBE) def test_version_from_git(wd): assert wd.version == "0.1.dev0" assert git.parse(str(wd.cwd), git.DEFAULT_DESCRIBE).branch == "master" wd.commit_testfile() assert wd.version.startswith("0.1.dev1+g") assert not wd.version.endswith("1-") wd("git tag v0.1") assert wd.version == "0.1" wd.write("test.txt", "test2") assert wd.version.startswith("0.2.dev0+g") wd.commit_testfile() assert wd.version.startswith("0.2.dev1+g") wd("git tag version-0.2") assert wd.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") wd.commit_testfile() wd("git tag 17.33.0-rc") assert wd.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" ) @pytest.mark.parametrize("with_class", [False, type, str]) def test_git_version_unnormalized_setuptools(with_class, tmpdir, wd, monkeypatch): """ 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") p = wd.cwd # create a setup.py dest_file = str(tmpdir.join("VERSION.txt")).replace("\\", "/") if with_class is False: # try normalize = False setup_py = """ from setuptools import setup setup(use_scm_version={'normalize': False, 'write_to': '%s'}) """ elif with_class is type: # custom non-normalizing class setup_py = """ 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': '%s'}) """ elif with_class is str: # non-normalizing class referenced by name setup_py = """from setuptools import setup setup(use_scm_version={ 'version_cls': 'setuptools_scm.NonNormalizedVersion', 'write_to': '%s' }) """ # finally write the setup.py file p.joinpath("setup.py").write_text(setup_py % dest_file) # 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 = do((sys.executable, "setup.py", "--version"), p) assert res == "17.33.0rc1" # but the version tag in the file is non-normalized (with the dash) assert tmpdir.join("VERSION.txt").read() == "17.33.0-rc1" @pytest.mark.issue(179) def test_unicode_version_scheme(wd): 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): wd.write("test.txt", "test2") # untracked files dont change the state assert wd.version == "0.1.dev0" wd("git add test.txt") assert wd.version.startswith("0.1.dev0+d") @pytest.mark.issue(86) @pytest.mark.parametrize("today", [False, True]) def test_git_dirty_notag(today, wd, monkeypatch): 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") if today: # the date on the tag is in UTC tag = datetime.now(timezone.utc).date().strftime(".d%Y%m%d") else: tag = ".d20090213" # we are dirty, check for the tag assert tag in wd.version @pytest.mark.issue(193) @pytest.mark.xfail(reason="sometimes relative path results") def test_git_worktree_support(wd, tmp_path): wd.commit_testfile() 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 "test.txt" in res assert str(worktree) in res @pytest.fixture def shallow_wd(wd, tmpdir): 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"]) return target def test_git_parse_shallow_warns(shallow_wd, recwarn): git.parse(str(shallow_wd)) 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_shallow_autocorrect(shallow_wd, recwarn): git.parse(str(shallow_wd), 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) def test_find_files_stop_at_root_git(wd): wd.commit_testfile() project = wd.cwd / "project" project.mkdir() project.joinpath("setup.cfg").touch() assert integration.find_files(str(project)) == [] @pytest.mark.issue(128) def test_parse_no_worktree(tmpdir): ret = git.parse(str(tmpdir)) assert ret is None def test_alphanumeric_tags_match(wd): wd.commit_testfile() wd("git tag newstyle-development-started") assert wd.version.startswith("0.1.dev1+g") def test_git_archive_export_ignore(wd, monkeypatch): wd.write("test1.txt", "test") wd.write("test2.txt", "test") wd.write( ".git/info/attributes", # Explicitly include test1.txt so that the test is not affected by # a potentially global gitattributes file on the test machine. "/test1.txt -export-ignore\n/test2.txt export-ignore", ) wd("git add test1.txt test2.txt") wd.commit() monkeypatch.chdir(wd.cwd) assert integration.find_files(".") == [opj(".", "test1.txt")] @pytest.mark.issue(228) def test_git_archive_subdirectory(wd, monkeypatch): 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")] @pytest.mark.issue(251) def test_git_archive_run_from_subdirectory(wd, monkeypatch): 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")] def test_git_feature_branch_increments_major(wd): wd.commit_testfile() wd("git tag 1.0.0") wd.commit_testfile() assert wd.get_version(version_scheme="python-simplified-semver").startswith("1.0.1") wd("git checkout -b feature/fun") wd.commit_testfile() assert wd.get_version(version_scheme="python-simplified-semver").startswith("1.1.0") @pytest.mark.issue("https://github.com/pypa/setuptools_scm/issues/303") def test_not_matching_tags(wd): wd.commit_testfile() wd("git tag apache-arrow-0.11.1") wd.commit_testfile() wd("git tag apache-arrow-js-0.9.9") wd.commit_testfile() assert wd.get_version( tag_regex=r"^apache-arrow-([\.0-9]+)$", git_describe_command="git describe --dirty --tags --long --exclude *js* ", ).startswith("0.11.2") @pytest.mark.issue("https://github.com/pypa/setuptools_scm/issues/411") @pytest.mark.xfail(reason="https://github.com/pypa/setuptools_scm/issues/449") def test_non_dotted_version(wd): 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): 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): 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): """ """ wd.commit_testfile() normal = wd.version # git hooks set this and break subsequent setuptools_scm unless we clean monkeypatch.setenv("GIT_DIR", __file__) assert wd.version == normal def test_git_getdate(wd): # TODO: case coverage for git wd parse today = date.today() def parse_date(): return git.parse(os.fspath(wd.cwd)).node_date git_wd = git.GitWorkdir(os.fspath(wd.cwd)) assert git_wd.get_head_date() is None assert parse_date() == today wd.commit_testfile() assert git_wd.get_head_date() == today meta = git.parse(os.fspath(wd.cwd)) assert meta.node_date == today def test_git_getdate_badgit( wd, ): wd.commit_testfile() git_wd = git.GitWorkdir(os.fspath(wd.cwd)) with patch.object(git_wd, "do_ex", Mock(return_value=("%cI", "", 0))): assert git_wd.get_head_date() is None setuptools_scm-6.4.2/testing/test_hg_git.py000066400000000000000000000041611417176422400211470ustar00rootroot00000000000000import pytest from setuptools_scm.utils import do_ex from setuptools_scm.utils import has_command @pytest.fixture(scope="module", autouse=True) def _check_hg_git(): if not has_command("hg", warn=False): pytest.skip("hg executable not found") python_hg, err, ret = do_ex("hg debuginstall --template {pythonexe}") if ret: skip_no_hggit = True else: out, err, ret = do_ex([python_hg.strip(), "-c", "import hggit"]) skip_no_hggit = bool(ret) if skip_no_hggit: pytest.skip("hg-git not installed") def test_base(repositories_hg_git): wd, wd_git = repositories_hg_git assert wd_git.version == "0.1.dev0" assert wd.version == "0.1.dev0" wd_git.commit_testfile() wd("hg pull -u") version_git = wd_git.version version = wd.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.version == "0.1" assert wd.version == "0.1" wd_git.write("test.txt", "test2") wd.write("test.txt", "test2") assert wd_git.version.startswith("0.2.dev0+g") assert wd.version.startswith("0.2.dev0+g") wd_git.commit_testfile() wd("hg pull") wd("hg up -C") assert wd_git.version.startswith("0.2.dev1+g") assert wd.version.startswith("0.2.dev1+g") wd_git("git tag version-0.2") wd("hg pull -u") assert wd_git.version.startswith("0.2") assert wd.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.version.startswith("0.2") with pytest.warns( UserWarning, match="tag '.*' will be stripped of its suffix '.*'" ): assert wd.version.startswith("0.2") wd_git.commit_testfile() wd_git("git tag 17.33.0-rc") wd("hg pull -u") assert wd_git.version == "17.33.0rc0" assert wd.version == "17.33.0rc0" setuptools_scm-6.4.2/testing/test_integration.py000066400000000000000000000112301417176422400222240ustar00rootroot00000000000000import os import sys import textwrap import pytest from setuptools_scm import PRETEND_KEY from setuptools_scm import PRETEND_KEY_NAMED from setuptools_scm.integration import _warn_on_old_setuptools from setuptools_scm.utils import do @pytest.fixture def wd(wd): wd("git init") wd("git config user.email test@example.com") wd('git config user.name "a test"') wd.add_command = "git add ." wd.commit_command = "git commit -m test-{reason}" return wd def test_pyproject_support(tmpdir, monkeypatch): pytest.importorskip("toml") monkeypatch.delenv("SETUPTOOLS_SCM_DEBUG") pkg = tmpdir.ensure("package", dir=42) pkg.join("pyproject.toml").write_text( textwrap.dedent( """ [tool.setuptools_scm] fallback_version = "12.34" [project] description = "Factory ⸻ A code generator 🏭" authors = [{name = "Łukasz Langa"}] """ ), encoding="utf-8", ) pkg.join("setup.py").write("__import__('setuptools').setup()") res = do((sys.executable, "setup.py", "--version"), pkg) assert res == "12.34" PYPROJECT_FILES = { "setup.py": "[tool.setuptools_scm]", "setup.cfg": "[tool.setuptools_scm]", "pyproject tool.setuptools_scm": ( "[tool.setuptools_scm]\ndist_name='setuptools_scm_example'" ), "pyproject.project": ( "[project]\nname='setuptools_scm_example'\n[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, metadata_in): 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") def test_pretend_version(monkeypatch, wd): 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, wd, metadata_in): 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(tmpdir, monkeypatch, wd): 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(tmpdir, monkeypatch, wd): 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, wd): monkeypatch.setenv(PRETEND_KEY, "dummy") wd.write("setup.py", SETUP_PY_PLAIN) assert wd.get_version(write_to="test.py") == "dummy" assert wd("python setup.py --version") == "0.0.0" def test_own_setup_fails_on_old_python(monkeypatch): monkeypatch.setattr("sys.version_info", (3, 5)) monkeypatch.syspath_prepend(os.path.dirname(os.path.dirname(__file__))) import setup with pytest.raises( RuntimeError, match="support for python < 3.6 has been removed in setuptools_scm>=6.0.0", ): setup.scm_version() def testwarn_on_broken_setuptools(): _warn_on_old_setuptools("45") with pytest.warns(RuntimeWarning, match="ERROR: setuptools==44"): _warn_on_old_setuptools("44") @pytest.mark.issue(611) def test_distribution_procides_extras(): try: from importlib.metadata import distribution except ImportError: from importlib_metadata import distribution dist = distribution("setuptools_scm") assert sorted(dist.metadata.get_all("Provides-Extra")) == ["test", "toml"] setuptools_scm-6.4.2/testing/test_main.py000066400000000000000000000027041417176422400206330ustar00rootroot00000000000000import os.path import sys import textwrap import pytest def test_main(): mainfile = os.path.join( os.path.dirname(__file__), "..", "src", "setuptools_scm", "__main__.py" ) with open(mainfile) as f: code = compile(f.read(), "__main__.py", "exec") exec(code) @pytest.fixture def repo(wd): 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): 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): res = repo((sys.executable, "-m", "setuptools_scm")) assert res.startswith("0.1.1.dev1") def test_repo_with_pyproject_missing_setuptools_scm(repo): pyproject = """\ [project] name = "example" """ repo.write("pyproject.toml", textwrap.dedent(pyproject)) repo.add_and_commit() res = repo((sys.executable, "-m", "setuptools_scm")) assert res.startswith("0.1.1.dev2") setuptools_scm-6.4.2/testing/test_mercurial.py000066400000000000000000000122211417176422400216650ustar00rootroot00000000000000import pytest from setuptools_scm import format_version from setuptools_scm import integration from setuptools_scm.config import Configuration from setuptools_scm.hg import archival_to_version from setuptools_scm.hg import parse from setuptools_scm.utils import has_command pytestmark = pytest.mark.skipif( not has_command("hg", warn=False), reason="hg executable not found" ) @pytest.fixture def wd(wd): wd("hg init") wd.add_command = "hg add ." wd.commit_command = 'hg commit -m test-{reason} -u test -d "0 0"' return wd archival_mapping = { "1.0": {"tag": "1.0"}, "1.1.dev3+h000000000000": { "latesttag": "1.0", "latesttagdistance": "3", "node": "0" * 20, }, "0.0": {"node": "0" * 20}, "1.2.2": {"tag": "release-1.2.2"}, "1.2.2.dev0": {"tag": "release-1.2.2.dev"}, } @pytest.mark.parametrize("expected,data", sorted(archival_mapping.items())) def test_archival_to_version(expected, 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 ) def test_hg_gone(wd, monkeypatch): monkeypatch.setenv("PATH", str(wd.cwd / "not-existing")) with pytest.raises(EnvironmentError, match="'hg' was not found"): parse(str(wd.cwd)) def test_find_files_stop_at_root_hg(wd, monkeypatch): 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)) == [] # issue 251 wd.add_and_commit() monkeypatch.chdir(project) assert integration.find_files() == ["setup.cfg"] # XXX: better tests for tag prefixes def test_version_from_hg_id(wd): assert wd.version == "0.0" wd.commit_testfile() assert wd.version.startswith("0.1.dev2+") # tagging commit is considered the tag wd('hg tag v0.1 -u test -d "0 0"') assert wd.version == "0.1" wd.commit_testfile() assert wd.version.startswith("0.2.dev2") wd("hg up v0.1") assert wd.version == "0.1" # commit originating from the tagged revision # that is not a actual tag wd.commit_testfile() assert wd.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" def test_version_from_archival(wd): # entrypoints are unordered, # cleaning the wd ensure this test won't break randomly wd.cwd.joinpath(".hg").rename(wd.cwd / ".nothg") wd.write(".hg_archival.txt", "node: 000000000000\n" "tag: 0.1\n") assert wd.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" @pytest.mark.issue("#72") def test_version_in_merge(wd): wd.commit_testfile() wd.commit_testfile() wd("hg up 0") wd.commit_testfile() wd("hg merge --tool :merge") assert wd.version is not None @pytest.mark.issue(128) def test_parse_no_worktree(tmpdir): ret = parse(str(tmpdir)) assert ret is None @pytest.fixture def version_1_0(wd): 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): wd("hg branch testbranch") wd.write("branchfile", "branchtext") wd(wd.add_command) wd.commit() wd("hg update default") wd("hg merge testbranch") return wd @pytest.mark.usefixtures("pre_merge_commit_after_tag") def test_version_bump_before_merge_commit(wd): assert wd.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): wd.commit() assert wd.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""" 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 wd.commit() # commits both the testfile _and_ .hgtags assert wd.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 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" @pytest.mark.usefixtures("version_1_0") def test_feature_branch_increments_major(wd) -> None: wd.commit_testfile() assert wd.get_version(version_scheme="python-simplified-semver").startswith("1.0.1") wd("hg branch feature/fun") assert wd.get_version(version_scheme="python-simplified-semver").startswith("1.1.0") setuptools_scm-6.4.2/testing/test_regressions.py000066400000000000000000000053471417176422400222600ustar00rootroot00000000000000import subprocess import sys import pytest from setuptools_scm import get_version from setuptools_scm.git import parse from setuptools_scm.utils import do from setuptools_scm.utils import do_ex def test_pkginfo_noscmroot(tmpdir, monkeypatch): """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( "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 p.join("PKG-INFO").write("Version: 1.0") res = do((sys.executable, "setup.py", "--version"), p) assert res == "1.0" try: do("git init", p.dirpath()) 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" @pytest.mark.issue(164) def test_pip_download(tmpdir, monkeypatch): monkeypatch.chdir(tmpdir) subprocess.check_call([sys.executable, "-m", "pip", "download", "lz4==0.9.0"]) def test_use_scm_version_callable(tmpdir, monkeypatch): """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( """from setuptools import setup def vcfg(): from setuptools_scm.version import guess_next_dev_version def vs(v): return guess_next_dev_version(v) return {"version_scheme": vs} setup(use_scm_version=vcfg) """ ) p.join("PKG-INFO").write("Version: 1.0") res = do((sys.executable, "setup.py", "--version"), p) assert res == "1.0" @pytest.mark.skipif(sys.platform != "win32", reason="this bug is only valid on windows") def test_case_mismatch_on_windows_git(tmpdir): """Case insensitive path checks on Windows""" p = tmpdir.ensure("CapitalizedDir", dir=1) do("git init", p) res = parse(str(p).lower()) assert res is not None setuptools_scm-6.4.2/testing/test_setuptools_support.py000066400000000000000000000122151417176422400237220ustar00rootroot00000000000000""" integration tests that check setuptools version support """ import os import pathlib import subprocess import sys import pytest def cli_run(*k, **kw): """this defers the virtualenv import it helps to avoid warnings from the furthermore imported setuptools """ global cli_run from virtualenv.run import cli_run return cli_run(*k, **kw) pytestmark = pytest.mark.filterwarnings( r"ignore:.*tool\.setuptools_scm.*", r"always:.*setup.py install is deprecated.*" ) ROOT = pathlib.Path(__file__).parent.parent class Venv: def __init__(self, location: pathlib.Path): self.location = location @property def python(self): return self.location / "bin/python" class VenvMaker: def __init__(self, base: pathlib.Path): self.base = base def __repr__(self): return f"" def get_venv(self, python, pip, setuptools, prefix="scm"): name = f"{prefix}-py={python}-pip={pip}-setuptools={setuptools}" path = self.base / name if not path.is_dir(): cli_run( [ str(path), "--python", python, "--pip", pip, "--setuptools", setuptools, ], setup_logging=False, ) venv = Venv(path) subprocess.run([venv.python, "-m", "pip", "install", "-e", str(ROOT)]) # fixup pip subprocess.check_call([venv.python, "-m", "pip", "install", f"pip=={pip}"]) subprocess.check_call( [venv.python, "-m", "pip", "install", f"setuptools~={setuptools}"] ) return venv @pytest.fixture def venv_maker(pytestconfig): if not pytestconfig.getoption("--test-legacy"): pytest.skip( "testing on legacy setuptools disabled, pass --test-legacy to run them" ) dir = pytestconfig.cache.makedir("setuptools_scm_venvs") path = pathlib.Path(str(dir)) return VenvMaker(path) 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(venv, expected_version, **env): subprocess.check_call( [venv.python, "-c", SCRIPT, expected_version], env=dict(os.environ, **env), ) @pytest.mark.skipif( sys.version_info[:2] >= (3, 10), reason="old setuptools won't work on python 3.10" ) def test_distlib_setuptools_works(venv_maker): venv = venv_maker.get_venv(setuptools="45.0.0", pip="9.0", python="3.6") subprocess.run([venv.python, "-m", "pip", "install", "-e", str(ROOT)]) check(venv, "45.0.0") SETUP_PY_NAME = """ from setuptools import setup setup(name='setuptools_scm_test_package') """ SETUP_PY_KEYWORD = """ from setuptools import setup setup(use_scm_version={"write_to": "pkg_version.py"}) """ PYPROJECT_TOML_WITH_KEY = """ [build-system] # Minimum requirements for the build system to execute. requires = ["setuptools>45", "wheel"] # PEP 508 specifications. [tool.setuptools_scm] write_to = "pkg_version.py" """ SETUP_CFG_NAME = """ [metadata] name = setuptools_scm_test_package """ def prepare_expecting_pyproject_support(pkg: pathlib.Path): pkg.mkdir() pkg.joinpath("setup.py").write_text(SETUP_PY_NAME) pkg.joinpath("pyproject.toml").write_text(PYPROJECT_TOML_WITH_KEY) pkg.joinpath("PKG-INFO").write_text("Version: 1.0.0") def prepare_setup_py_config(pkg: pathlib.Path): pkg.mkdir() pkg.joinpath("setup.py").write_text(SETUP_PY_KEYWORD) pkg.joinpath("setup.cfg").write_text(SETUP_CFG_NAME) pkg.joinpath("PKG-INFO").write_text("Version: 1.0.0") @pytest.mark.skipif( sys.version_info[:2] >= (3, 10), reason="old setuptools won't work on python 3.10" ) @pytest.mark.parametrize("setuptools", [f"{v}.0" for v in range(31, 45)]) @pytest.mark.parametrize( "project_create", [ pytest.param( prepare_expecting_pyproject_support, marks=pytest.mark.xfail(reason="pyproject requires setuptools > 42"), ), prepare_setup_py_config, ], ) def test_on_old_setuptools( venv_maker, tmp_path, setuptools, project_create, monkeypatch ): pkg = tmp_path.joinpath("pkg") project_create(pkg) venv = venv_maker.get_venv(setuptools=setuptools, pip="9.0", python="3.6") # monkeypatch.delenv("SETUPTOOLS_SCM_DEBUG", raising=False) def run_and_output(cmd): res = subprocess.run(cmd, cwd=str(pkg), stdout=subprocess.PIPE) if not res.returncode: return res.stdout.strip() else: print(res.stdout) pytest.fail(str(cmd), pytrace=False) version = run_and_output([venv.python, "setup.py", "--version"]) name = run_and_output([venv.python, "setup.py", "--name"]) assert (name, version) == (b"setuptools_scm_test_package", b"1.0.0") # monkeypatch.setenv( # "SETUPTOOLS_SCM_PRETEND_VERSION_FOR_setuptools_scm_test_package", "2.0,0") # version_pretend = run_and_output([venv.python, "setup.py", "--version"]) # assert version_pretend == b"2.0.0" setuptools_scm-6.4.2/testing/test_version.py000066400000000000000000000234511417176422400213760ustar00rootroot00000000000000from datetime import date from datetime import timedelta import pytest from setuptools_scm.config import Configuration from setuptools_scm.version import calver_by_date 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 no_guess_dev_version from setuptools_scm.version import release_branch_semver_version from setuptools_scm.version import simplified_semver_version from setuptools_scm.version import tags_to_versions c = Configuration() @pytest.mark.parametrize( "version, expected_next", [ pytest.param(meta("1.0.0", config=c), "1.0.0", id="exact"), pytest.param(meta("1.0", config=c), "1.0.0", id="short_tag"), pytest.param( meta("1.0.0", distance=2, branch="default", config=c), "1.0.1.dev2", id="normal_branch", ), pytest.param( meta("1.0", distance=2, branch="default", config=c), "1.0.1.dev2", id="normal_branch_short_tag", ), pytest.param( meta("1.0.0", distance=2, branch="feature", config=c), "1.1.0.dev2", id="feature_branch", ), pytest.param( meta("1.0", distance=2, branch="feature", config=c), "1.1.0.dev2", id="feature_branch_short_tag", ), pytest.param( meta("1.0.0", distance=2, branch="features/test", config=c), "1.1.0.dev2", id="feature_in_branch", ), ], ) def test_next_semver(version, expected_next): computed = simplified_semver_version(version) assert computed == expected_next def test_next_semver_bad_tag(): version = meta("1.0.0-foo", preformatted=True, config=c) with pytest.raises( ValueError, match="1.0.0-foo can't be parsed as numeric version" ): simplified_semver_version(version) @pytest.mark.parametrize( "version, expected_next", [ pytest.param(meta("1.0.0", config=c), "1.0.0", id="exact"), pytest.param( meta("1.0.0", distance=2, branch="master", config=c), "1.1.0.dev2", id="development_branch", ), pytest.param( meta("1.0.0rc1", distance=2, branch="master", config=c), "1.1.0.dev2", id="development_branch_release_candidate", ), pytest.param( meta("1.0.0", distance=2, branch="maintenance/1.0.x", config=c), "1.0.1.dev2", id="release_branch_legacy_version", ), pytest.param( meta("1.0.0", distance=2, branch="v1.0.x", config=c), "1.0.1.dev2", id="release_branch_with_v_prefix", ), pytest.param( meta("1.0.0", distance=2, branch="release-1.0", config=c), "1.0.1.dev2", id="release_branch_with_prefix", ), pytest.param( meta("1.0.0", distance=2, branch="bugfix/3434", config=c), "1.1.0.dev2", id="false_positive_release_branch", ), ], ) def test_next_release_branch_semver(version, expected_next): computed = release_branch_semver_version(version) assert computed == expected_next def m(tag, **kw): 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, expected_next): computed = no_guess_dev_version(version) assert computed == expected_next @pytest.mark.parametrize( "version, match", [ ("1.0.dev1", "choosing custom numbers for the `.devX` distance"), ("1.0.post1", "already is a post release"), ], ) def test_no_guess_version_bad(version, match): with pytest.raises(ValueError, match=match): no_guess_dev_version(m(version, distance=1)) def test_bump_dev_version_zero(): assert guess_next_version("1.0.dev0") == "1.0" def test_bump_dev_version_nonzero_raises(): with pytest.raises(ValueError) as excinfo: guess_next_version("1.0.dev1") assert str(excinfo.value) == ( "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" ) @pytest.mark.parametrize( "tag, expected", [ 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"), ], ) def test_tag_regex1(tag, expected): if "+" in tag: # pytest bug wrt cardinality with pytest.warns(UserWarning): result = meta(tag, config=c) else: result = meta(tag, config=c) assert result.tag.public == expected @pytest.mark.issue("https://github.com/pypa/setuptools_scm/issues/286") def test_tags_to_versions(): versions = tags_to_versions(["1.0", "2.0", "3.0"], config=c) assert isinstance(versions, list) # enable subscription @pytest.mark.issue("https://github.com/pypa/setuptools_scm/issues/471") def test_version_bump_bad(): 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="2.0.0-alpha.5-PMC") def test_format_version_schemes(): version = meta("1.0", config=c) format_version( version, local_scheme="no-local-version", version_scheme=[lambda v: None, "guess-next-dev"], ) def date_to_str(date_=None, days_offset=0, fmt="{dt:%y}.{dt.month}.{dt.day}"): date_ = date_ or date.today() date_ = date_ - timedelta(days=days_offset) return fmt.format(dt=date_) @pytest.mark.parametrize( "version, expected_next", [ pytest.param( meta(date_to_str(days_offset=3), config=c), date_to_str(days_offset=3), id="exact", ), pytest.param( meta(date_to_str() + ".1", config=c), 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, dirty=True), date_to_str() + ".0.dev0", id="dirty other day", ), pytest.param( meta(date_to_str(), config=c, distance=2, branch="default"), date_to_str() + ".1.dev2", id="normal branch", ), pytest.param( meta(date_to_str(fmt="{dt:%Y}.{dt.month}.{dt.day}"), config=c), date_to_str(fmt="{dt:%Y}.{dt.month}.{dt.day}"), id="4 digits year", ), pytest.param( meta(date_to_str(), config=c, distance=2, branch="release-2021.05.06"), "2021.05.06", id="release branch", ), pytest.param( meta(date_to_str() + ".2", config=c, 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, node_date=date.today() - timedelta(days=2), ), date_to_str(days_offset=3) + ".2", id="node date clean", ), pytest.param( meta( date_to_str(days_offset=2) + ".2", config=c, distance=2, node_date=date.today() - timedelta(days=2), ), date_to_str(date.today() - timedelta(days=2)) + ".3.dev2", id="node date distance", ), pytest.param( meta( "1.2.0", config=c, distance=2, node_date=date.today() - timedelta(days=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, expected_next): 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"), pytest.param( meta("1.0.0", config=c, dirty=True), "1.0.0", id="SemVer dirty", marks=pytest.mark.xfail, ), ], ) def test_calver_by_date_semver(version, expected_next): computed = calver_by_date(version) assert computed == expected_next def test_calver_by_date_future_warning(): with pytest.warns(UserWarning, match="your previous tag*"): calver_by_date(meta(date_to_str(days_offset=-2), config=c, distance=2)) def test_custom_version_cls(): """Test that we can pass our own version class instead of pkg_resources""" class MyVersion: def __init__(self, tag_str: str): self.tag = tag_str def __repr__(self): return "Custom %s" % self.tag scm_version = meta("1.0.0-foo", config=Configuration(version_cls=MyVersion)) assert isinstance(scm_version.tag, MyVersion) assert repr(scm_version.tag) == "Custom 1.0.0-foo" setuptools_scm-6.4.2/tox.ini000066400000000000000000000017421417176422400161350ustar00rootroot00000000000000[tox] envlist=py{36,37,38,39,310}-{test,selfcheck},check_readme,check-dist [pytest] testpaths=testing filterwarnings= error ignore:.*tool\.setuptools_scm.* markers= issue(id): reference to github issue skip_commit: allows to skip committing in the helpers # disable unraisable until investigated addopts = -p no:unraisableexception [flake8] max-complexity = 10 max-line-length = 88 ignore=E203,W503 [testenv] usedevelop=True skip_install= selfcheck: True test: False deps= pytest setuptools >= 45 tomli virtualenv>20 commands= test: pytest [] selfcheck: python setup.py --version [testenv:check_readme] skip_install=True setenv = SETUPTOOLS_SCM_PRETEND_VERSION=2.0 deps= check-manifest docutils pygments commands= rst2html.py README.rst {envlogdir}/README.html --strict [] check-manifest [testenv:check_dist] deps= build twine commands= python -m build twine check dist/* #XXX: envs for hg versions