From 6eb844e03f8fea43635a645026f6495f32a83f33 Mon Sep 17 00:00:00 2001 From: DongHun Kwak Date: Mon, 5 Apr 2021 16:03:29 +0900 Subject: [PATCH] Imported Upstream version 4.1.2 --- .github/FUNDING.yml | 1 + .github/workflows/pre-commit.yml | 20 + .github/workflows/python-tests.yml | 155 +++++ .gitignore | 48 ++ CHANGELOG.rst | 504 ++++++++++++++ LICENSE | 17 + MANIFEST.in | 10 + PKG-INFO | 615 ++++++++++++++++++ README.rst | 589 +++++++++++++++++ pyproject.toml | 3 + setup.cfg | 76 +++ setup.py | 53 ++ src/setuptools_scm.egg-info/PKG-INFO | 615 ++++++++++++++++++ src/setuptools_scm.egg-info/SOURCES.txt | 47 ++ .../dependency_links.txt | 1 + src/setuptools_scm.egg-info/entry_points.txt | 35 + src/setuptools_scm.egg-info/requires.txt | 4 + src/setuptools_scm.egg-info/top_level.txt | 1 + src/setuptools_scm.egg-info/zip-safe | 1 + src/setuptools_scm/__init__.py | 163 +++++ src/setuptools_scm/__main__.py | 17 + src/setuptools_scm/config.py | 125 ++++ src/setuptools_scm/discover.py | 13 + src/setuptools_scm/file_finder.py | 55 ++ src/setuptools_scm/file_finder_git.py | 69 ++ src/setuptools_scm/file_finder_hg.py | 47 ++ src/setuptools_scm/git.py | 153 +++++ src/setuptools_scm/hacks.py | 37 ++ src/setuptools_scm/hg.py | 113 ++++ src/setuptools_scm/integration.py | 48 ++ src/setuptools_scm/utils.py | 163 +++++ src/setuptools_scm/version.py | 349 ++++++++++ src/setuptools_scm/win_py31_compat.py | 214 ++++++ testing/check_self_install.py | 5 + testing/conftest.py | 96 +++ testing/play_out_381.bash | 23 + testing/test_basic_api.py | 117 ++++ testing/test_config.py | 39 ++ testing/test_file_finder.py | 182 ++++++ testing/test_functions.py | 106 +++ testing/test_git.py | 273 ++++++++ testing/test_integration.py | 42 ++ testing/test_main.py | 10 + testing/test_mercurial.py | 182 ++++++ testing/test_regressions.py | 90 +++ testing/test_setuptools_support.py | 83 +++ testing/test_version.py | 111 ++++ tox.ini | 83 +++ 48 files changed, 5803 insertions(+) create mode 100644 .github/FUNDING.yml create mode 100644 .github/workflows/pre-commit.yml create mode 100644 .github/workflows/python-tests.yml create mode 100644 .gitignore create mode 100644 CHANGELOG.rst create mode 100644 LICENSE create mode 100644 MANIFEST.in create mode 100644 PKG-INFO create mode 100644 README.rst create mode 100644 pyproject.toml create mode 100644 setup.cfg create mode 100644 setup.py create mode 100644 src/setuptools_scm.egg-info/PKG-INFO create mode 100644 src/setuptools_scm.egg-info/SOURCES.txt create mode 100644 src/setuptools_scm.egg-info/dependency_links.txt create mode 100644 src/setuptools_scm.egg-info/entry_points.txt create mode 100644 src/setuptools_scm.egg-info/requires.txt create mode 100644 src/setuptools_scm.egg-info/top_level.txt create mode 100644 src/setuptools_scm.egg-info/zip-safe create mode 100644 src/setuptools_scm/__init__.py create mode 100644 src/setuptools_scm/__main__.py create mode 100644 src/setuptools_scm/config.py create mode 100644 src/setuptools_scm/discover.py create mode 100644 src/setuptools_scm/file_finder.py create mode 100644 src/setuptools_scm/file_finder_git.py create mode 100644 src/setuptools_scm/file_finder_hg.py create mode 100644 src/setuptools_scm/git.py create mode 100644 src/setuptools_scm/hacks.py create mode 100644 src/setuptools_scm/hg.py create mode 100644 src/setuptools_scm/integration.py create mode 100644 src/setuptools_scm/utils.py create mode 100644 src/setuptools_scm/version.py create mode 100644 src/setuptools_scm/win_py31_compat.py create mode 100644 testing/check_self_install.py create mode 100644 testing/conftest.py create mode 100755 testing/play_out_381.bash create mode 100644 testing/test_basic_api.py create mode 100644 testing/test_config.py create mode 100644 testing/test_file_finder.py create mode 100644 testing/test_functions.py create mode 100644 testing/test_git.py create mode 100644 testing/test_integration.py create mode 100644 testing/test_main.py create mode 100644 testing/test_mercurial.py create mode 100644 testing/test_regressions.py create mode 100644 testing/test_setuptools_support.py create mode 100644 testing/test_version.py create mode 100644 tox.ini diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..ac779ab --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +tidelift: pypi/setuptools-scm diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml new file mode 100644 index 0000000..c154a12 --- /dev/null +++ b/.github/workflows/pre-commit.yml @@ -0,0 +1,20 @@ +name: pre-commit + +on: + pull_request: + push: + branches: [master] + +jobs: + pre-commit: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - uses: actions/setup-python@v1 + - name: set PY + run: echo "::set-env name=PY::$(python --version --version | sha256sum | cut -d' ' -f1)" + - uses: actions/cache@v1 + with: + path: ~/.cache/pre-commit + key: pre-commit|${{ env.PY }}|${{ hashFiles('.pre-commit-config.yaml') }} + - uses: pre-commit/action@v1.0.0 diff --git a/.github/workflows/python-tests.yml b/.github/workflows/python-tests.yml new file mode 100644 index 0000000..ca8df3f --- /dev/null +++ b/.github/workflows/python-tests.yml @@ -0,0 +1,155 @@ +name: python tests+artifacts+release + +on: + pull_request: + push: + branches: + - master + tags: + - "v*" + release: + +jobs: + test: + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + python_version: [ '2.7', '3.5', '3.6', '3.7', '3.8', 'pypy2', 'pypy3' ] + os: [windows-latest, ubuntu-latest] #, macos-latest] + exclude: + - os: windows-latest + python_version: "pypy2" + include: + - os: ubuntu-latest + python_version: '3.9-dev' + + name: ${{ matrix.os }} - Python ${{ matrix.python_version }} + steps: + - uses: actions/checkout@v1 + - name: Setup python + uses: actions/setup-python@v2 + if: matrix.python_version != '3.9-dev' + with: + python-version: ${{ matrix.python_version }} + architecture: x64 + - name: Set up Python ${{ matrix.python_version }} (deadsnakes) + uses: deadsnakes/action@v1.0.0 + if: matrix.python_version == '3.9-dev' + with: + python-version: ${{ matrix.python_version }} + architecture: x64 + - run: pip install -U setuptools + - run: pip install -e .[toml] pytest + - run: pytest + + check_selfinstall: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python_version: [ '2.7', '3.5', '3.6', '3.7', '3.8', 'pypy2', 'pypy3' ] + name: check self install - Python ${{ matrix.python_version }} + steps: + - uses: actions/checkout@v1 + - name: Setup python + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python_version }} + architecture: x64 + # self install testing needs some clarity + # so its being executed without any other tools running + - run: pip install -U setuptools + - run: python setup.py egg_info + - run: python setup.py sdist + - run: easy_install dist/* + - run: python testing/check_self_install.py + + + eggs: + runs-on: ubuntu-latest + + needs: [test] + name: Python ${{ matrix.python_version }} eggs + strategy: + matrix: + python_version: ['2.7', '3.5', '3.6', '3.7', '3.8', '3.9-dev'] + steps: + - uses: actions/checkout@v1 + - name: Setup python + uses: actions/setup-python@v2 + if: matrix.python_version != '3.9-dev' + with: + python-version: ${{ matrix.python_version }} + architecture: x64 + - name: Set up Python ${{ matrix.python_version }} (deadsnakes) + uses: deadsnakes/action@v1.0.0 + if: matrix.python_version == '3.9-dev' + with: + python-version: ${{ matrix.python_version }} + architecture: x64 + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install --upgrade wheel setuptools + - run: python setup.py egg_info + - name: Build package + run: python setup.py bdist_egg + - uses: actions/upload-artifact@v2 + with: + name: dist + path: dist + + dist: + runs-on: ubuntu-latest + + needs: [test] + name: Python bdist/wheel + steps: + - uses: actions/checkout@v1 + - uses: actions/setup-python@v1 + with: + python-version: "3.8" + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install --upgrade wheel setuptools + - run: python setup.py egg_info + - name: Build package + run: python setup.py bdist_wheel sdist + - uses: actions/upload-artifact@v2 + with: + name: dist + path: dist + + + dist_check: + runs-on: ubuntu-latest + needs: [eggs, dist] + steps: + - uses: actions/setup-python@v2 + with: + python-version: "3.8" + - name: Install dependencies + run: pip install twine + - uses: actions/download-artifact@v2 + with: + name: dist + path: dist + - run: twine check dist/* + + dist_upload: + + runs-on: ubuntu-latest + if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags') + needs: [dist_check] + 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 }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c51b132 --- /dev/null +++ b/.gitignore @@ -0,0 +1,48 @@ +### JetBrains template +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion + +*.iml + +## Directory-based project format: +.idea/ + +### Other editors +.*.swp + + +### Python template +# Byte-compiled / optimized +__pycache__/ +*.py[cod] +*$py.class + + +# Distribution / packaging +.env/ +env/ +.venv/ +venv/ +build/ +dist/ +.eggs/ +lib/ +lib64/ +*.egg-info/ + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +.pytest_cache +nosetests.xml +coverage.xml +*,cover + +# Sphinx documentation +docs/_build/ diff --git a/CHANGELOG.rst b/CHANGELOG.rst new file mode 100644 index 0000000..251ee8d --- /dev/null +++ b/CHANGELOG.rst @@ -0,0 +1,504 @@ +v4.1.2 +======= + +* disallow git tags without dots by default again - #449 + +v4.1.1 +======= + +* drop jaraco.windows from pyproject.toml, allows for wheel builds on python2 + + +v4.1.0 +======= + +* include python 3.9 via the deadsnakes action +* return release_branch_semver scheme (it got dropped in a bad rebase) +* undo the devendoring of the samefile backport for python2.7 on windows +* re-enable the building of universal wheels +* fix handling of missing git/hg on python2.7 (python 3 exceptions where used) +* correct the tox flake8 invocation +* trigger builds on tags again + +v4.0.0 +====== + +* Add ``parentdir_project_version`` to support installs from GitHub release + tarballs. +* use Coordinated Universal Time (UTC) +* switch to github actions for ci +* fix documentation for ``tag_regex`` and add support for single digit versions +* document handling of enterprise distros with unsupported setuptools versions #312 +* switch to declarative metadata +* drop the internal copy of samefile and use a dependency on jaraco.windows on legacy systems +* select git tags based on the presence of numbers instead of dots +* enable getting a version form a parent folder prefix +* add release-branch-semver version scheme +* make global configuration available to version metadata +* drop official support for python 3.4 + +v3.5.0 +====== + +* add ``no-local-version`` local scheme and improve documentation for schemes + +v3.4.4 +====== + +* fix #403: also sort out resource warnings when dealing with git file finding + +v3.4.3 +====== + +* fix #399: ensure the git file finder terminates subprocess after reading archive + +v3.4.2 +====== + +* fix #395: correctly transfer tag regex in the Configuration constructor +* rollback --first-parent for git describe as it turns out to be a regression for some users + +v3.4.1 +====== + +* pull in #377 to fix #374: correctly set up the default version scheme for pyproject usage. + this bugfix got missed when ruushing the release. + +v3.4.0 +====== + +* fix #181 - add support for projects built under setuptools declarative config + by way of the setuptools.finalize_distribution_options hook in Setuptools 42. + +* fix #305 - ensure the git file finder closes filedescriptors even when errors happen + +* fix #381 - clean out env vars from the git hook system to ensure correct function from within + +* modernize docs wrt importlib.metadata + +*edited* + +* use --first-parent for git describe + +v3.3.3 +====== + +* add eggs for python3.7 and 3.8 to the deploy + +v3.3.2 +====== + + +* fix #335 - fix python3.8 support and add builds for up to python3.8 + +v3.3.1 +====== + +* fix #333 (regression from #198) - use a specific fallback root when calling fallbacks. Remove old + hack that resets the root when fallback entrypoints are present. + +v3.3.0 +====== + +* fix #198 by adding the ``fallback_version`` option, which sets the version to be used when everything else fails. + +v3.2.0 +====== + +* fix #303 and #283 by adding the option ``git_describe_command`` to allow the user to control the +way that `git describe` is called. + +v3.1.0 +======= + +* fix #297 - correct the invocation in version_from_scm and deprecate it as its exposed by accident +* fix #298 - handle git file listing on empty repositories +* fix #268 - deprecate ScmVersion.extra + + +v3.0.6 +====== +* fix #295 - correctly handle selfinstall from tarballs + +v3.0.5 +====== + +* fix #292 - match leading ``V`` character as well + + https://www.python.org/dev/peps/pep-0440/#preceding-v-character + +v3.0.4 +======= + +* rerelease of 3.0.3 after fixing the release process + +v3.0.3 (pulled from pypi due to a packaging issue) +====== + +* fix #286 - duo an oversight a helper functio nwas returning a generator instead of a list + + +v3.0.2 +====== + +* fix a regression from tag parsing - support for multi-dashed prefixes - #284 + + +v3.0.1 +======= + +* fix a regression in setuptools_scm.git.parse - reorder arguments so the positional invocation from before works as expected #281 + +v3.0.0 +======= + +* introduce pre-commit and use black +* print the origin module to help testing +* switch to src layout (breaking change) +* no longer alias tag and parsed_version in order to support understanding a version parse failure +* require parse results to be ScmVersion or None (breaking change) +* fix #266 by requiring the prefix word to be a word again + (breaking change as the bug allowed arbitrary prefixes while the original feature only allowed words") +* introduce a internal config object to allow the configruation fo tag parsing and prefixes + (thanks to @punkadiddle for introducing it and passing it trough) + +v2.1.0 +====== + +* enhance docs for sphinx usage +* add symlink support to file finder for git #247 + (thanks Stéphane Bidoul) +* enhance tests handling win32 + (thanks Stéphane Bidoul) + +v2.0.0 +======== + +* fix #237 - correct imports in code examples +* improve mercurial commit detection (thanks Aaron) +* breaking change: remove support for setuptools before parsed versions +* reintroduce manifest as the travis deploy cant use the file finder +* reconfigure flake8 for future compatibility with black +* introduce support for branch name in version metadata and support a opt-in simplified semver version scheme + +v1.17.0 +======== + +* fix regression in git support - use a function to ensure it works in egg isntalled mode +* actually fail if file finding fails in order to see broken setups instead of generating broken dists + + (thanks Mehdi ABAAKOUK for both) + + +v1.16.2 +======== + +* fix regression in handling git export ignores + (thanks Mehdi ABAAKOUK) + +v1.16.1 +======= + +* fix regression in support for old setuptools versions + (thanks Marco Clemencic) + + +v1.16.0 +======= + +* drop support for eol python versions +* #214 - fix missuse in surogate-escape api +* add the node-and-timestamp local version sheme +* respect git export ignores +* avoid shlex.split on windows +* fix #218 - better handling of mercurial edge-cases with tag commits + being considered as the tagged commit +* fix #223 - remove the dependency on the interal SetupttoolsVersion + as it was removed after long-standing deprecation + +v1.15.7 +====== + +* Fix #174 with #207: Re-use samefile backport as developed in + jaraco.windows, and only use the backport where samefile is + not available. + +v1.15.6 +======= + +* fix #171 by unpinning the py version to allow a fixed one to get installed + +v1.15.5 +======= + +* fix #167 by correctly respecting preformatted version metadata + from PKG-INFO/EGG-INFO + +v1.15.4 +======= + +* fix issue #164: iterate all found entry points to avoid erros when pip remakes egg-info +* enhance self-use to enable pip install from github again + +v1.15.3 +======= + +* bring back correctly getting our version in the own sdist, finalizes #114 +* fix issue #150: strip local components of tags + +v1.15.2 +======= + +* fix issue #128: return None when a scm specific parse fails in a worktree to ease parse reuse + + +v1.15.1 +======= + +* fix issue #126: the local part of any tags is discarded + when guessing new versions +* minor performance optimization by doing fewer git calls + in the usual cases + + +v1.15.0 +======= + +* more sophisticated ignoring of mercurial tag commits + when considering distance in commits + (thanks Petre Mierlutiu) +* fix issue #114: stop trying to be smart for the sdist + and ensure its always correctly usign itself +* update trove classifiers +* fix issue #84: document using the installed package metadata for sphinx +* fix issue #81: fail more gracious when git/hg are missing +* address issue #93: provide an experimental api to customize behaviour on shallow git repos + a custom parse function may pick pre parse actions to do when using git + + +v1.14.1 +======= + +* fix #109: when detecting a dirty git workdir + don't consider untracked file + (this was a regression due to #86 in v1.13.1) +* consider the distance 0 when the git node is unknown + (happens when you haven't commited anything) + +v1.14.0 +======= + +* publish bdist_egg for python 2.6, 2.7 and 3.3-3.5 +* fix issue #107 - dont use node if it is None + +v1.13.1 +======= + +* fix issue #86 - detect dirty git workdir without tags + +v1.13.0 +======= + +* fix regression caused by the fix of #101 + * assert types for version dumping + * strictly pass all versions trough parsed version metadata + +v1.12.0 +======= + +* fix issue #97 - add support for mercurial plugins +* fix issue #101 - write version cache even for pretend version + (thanks anarcat for reporting and fixing) + +v1.11.1 +======== + +* fix issue #88 - better docs for sphinx usage (thanks Jason) +* fix issue #89 - use normpath to deal with windows + (thanks Te-jé Rodgers for reporting and fixing) + +v1.11.0 +======= + +* always run tag_to_version so in order to handle prefixes on old setuptools + (thanks to Brian May) +* drop support for python 3.2 +* extend the error message on missing scm metadata + (thanks Markus Unterwaditzer) +* fix bug when using callable version_scheme + (thanks Esben Haabendal) + +v1.10.1 +======= + +* fix issue #73 - in hg pre commit merge, consider parent1 instead of failing + +v1.10.0 +======= + +* add support for overriding the version number via the + environment variable SETUPTOOLS_SCM_PRETEND_VERSION + +* fix isssue #63 by adding the --match parameter to the git describe call + and prepare the possibility of passing more options to scm backends + +* fix issue #70 and #71 by introducing the parse keyword + to specify custom scm parsing, its an expert feature, + use with caution + + this change also introduces the setuptools_scm.parse_scm_fallback + entrypoint which can be used to register custom archive fallbacks + + +v1.9.0 +====== + +* Add :code:`relative_to` parameter to :code:`get_version` function; + fixes #44 per #45. + +v1.8.0 +====== + +* fix issue with setuptools wrong version warnings being printed to standard + out. User is informed now by distutils-warnings. +* restructure root finding, we now reliably ignore outer scm + and prefer PKG-INFO over scm, fixes #43 and #45 + +v1.7.0 +====== + +* correct the url to github + thanks David Szotten +* enhance scm not found errors with a note on git tarballs + thanks Markus +* add support for :code:`write_to_template` + +v1.6.0 +====== + +* bail out early if the scm is missing + + this brings issues with git tarballs and + older devpi-client releases to light, + before we would let the setup stay at version 0.0, + now there is a ValueError + +* propperly raise errors on write_to missuse (thanks Te-jé Rodgers) + +v1.5.5 +====== + +* Fix bug on Python 2 on Windows when environment has unicode fields. + +v1.5.4 +====== + +* Fix bug on Python 2 when version is loaded from existing metadata. + +v1.5.3 +====== + +* #28: Fix decoding error when PKG-INFO contains non-ASCII. + +v1.5.2 +====== + +* add zip_safe flag + +v1.5.1 +====== + +* fix file access bug i missed in 1.5 + +v1.5.0 +====== + +* moved setuptools integration related code to own file +* support storing version strings into a module/text file + using the :code:`write_to` coniguration parameter + +v1.4.0 +====== + +* propper handling for sdist +* fix file-finder failure from windows +* resuffle docs + +v1.3.0 +====== + +* support setuptools easy_install egg creation details + by hardwireing the version in the sdist + +v1.2.0 +====== + +* enhance self-use + +v1.1.0 +====== + +* enable self-use + +v1.0.0 +====== + +* documentation enhancements + +v0.26 +===== + +* rename to setuptools_scm +* split into package, add lots of entry points for extension +* pluggable version schemes + +v0.25 +===== + +* fix pep440 support + this reshuffles the complete code for version guessing + +v0.24 +===== + +* dont drop dirty flag on node finding +* fix distance for dirty flagged versions +* use dashes for time again, + its normalisation with setuptools +* remove the own version attribute, + it was too fragile to test for +* include file finding +* handle edge cases around dirty tagged versions + +v0.23 +===== + +* windows compatibility fix (thanks stefan) + drop samefile since its missing in + some python2 versions on windows +* add tests to the source tarballs + + +v0.22 +===== + +* windows compatibility fix (thanks stefan) + use samefile since it does path normalisation + +v0.21 +===== + +* fix the own version attribute (thanks stefan) + +v0.20 +===== + +* fix issue 11: always take git describe long format + to avoid the source of the ambiguity +* fix issue 12: add a __version__ attribute via pkginfo + +v0.19 +===== + +* configurable next version guessing +* fix distance guessing (thanks stefan) diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..89de354 --- /dev/null +++ b/LICENSE @@ -0,0 +1,17 @@ +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..4bbd88d --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,10 @@ +exclude *.nix +exclude .travis.yaml +exclude .pre-commit-config.yaml +include *.py +include testing/*.py +include tox.ini +include *.rst +include LICENSE +include *.toml +recursive-include testing *.bash diff --git a/PKG-INFO b/PKG-INFO new file mode 100644 index 0000000..1f3b082 --- /dev/null +++ b/PKG-INFO @@ -0,0 +1,615 @@ +Metadata-Version: 2.1 +Name: setuptools_scm +Version: 4.1.2 +Summary: the blessed package to manage your versions by scm tags +Home-page: https://github.com/pypa/setuptools_scm/ +Author: Ronny Pfannschmidt +Author-email: opensource@ronnypfannschmidt.de +License: MIT +Description: setuptools_scm + =============== + + ``setuptools_scm`` handles managing your Python package versions + in SCM metadata instead of declaring them as the version argument + or in a SCM managed file. + + Additionally ``setuptools_scm`` provides setuptools with a list of files that are managed by the SCM + (i.e. it automatically adds all of the SCM-managed files to the sdist). + Unwanted files must be excluded by discarding them via ``MANIFEST.in``. + + .. image:: https://travis-ci.org/pypa/setuptools_scm.svg?branch=master + :target: https://travis-ci.org/pypa/setuptools_scm + + .. image:: https://tidelift.com/badges/package/pypi/setuptools-scm + :target: https://tidelift.com/subscription/pkg/pypi-setuptools-scm?utm_source=pypi-setuptools-scm&utm_medium=readme + + + ``pyproject.toml`` usage + ------------------------ + + The preferred way to configure ``setuptools_scm`` is to author + settings in a ``tool.setuptools_scm`` section of ``pyproject.toml``. + + This feature requires Setuptools 42 or later, released in Nov, 2019. + If your project needs to support build from sdist on older versions + of Setuptools, you will need to also implement the ``setup.py usage`` + for those legacy environments. + + First, ensure that ``setuptools_scm`` is present during the project's + built step by specifying it as one of the build requirements. + + .. code:: toml + + # pyproject.toml + [build-system] + requires = ["setuptools>=42", "wheel", "setuptools_scm[toml]>=3.4"] + + Note that the ``toml`` extra must be supplied. + + That will be sufficient to require ``setuptools_scm`` for projects + that support PEP 518 (`pip `_ 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 + ------------------ + + The following settings are considered legacy behavior and + superseded by the ``pyproject.toml`` usage, but for maximal + compatibility, projects may also supply the configuration in + this older form. + + To use ``setuptools_scm`` just modify your project's ``setup.py`` file + like this: + + * Add ``setuptools_scm`` to the ``setup_requires`` parameter. + * Add the ``use_scm_version`` parameter and set it to ``True``. + + For example: + + .. code:: python + + from setuptools import setup + setup( + ..., + use_scm_version=True, + setup_requires=['setuptools_scm'], + ..., + ) + + Arguments to ``get_version()`` (see below) may be passed as a dictionary to + ``use_scm_version``. For example: + + .. code:: python + + from setuptools import setup + setup( + ..., + use_scm_version = { + "root": "..", + "relative_to": __file__, + "local_scheme": "node-and-timestamp" + }, + setup_requires=['setuptools_scm'], + ..., + ) + + You can confirm the version number locally via ``setup.py``: + + .. code-block:: shell + + $ python setup.py --version + + .. note:: + + If you see unusual version numbers for packages but ``python setup.py + --version`` reports the expected version number, ensure ``[egg_info]`` is + not defined in ``setup.cfg``. + + + ``setup.cfg`` usage + ------------------- + + If using `setuptools 30.3.0 + `_ + or greater, you can store ``setup_requires`` configuration in ``setup.cfg``. + However, ``use_scm_version`` must still be placed in ``setup.py``. For example: + + .. code:: python + + # setup.py + from setuptools import setup + setup( + use_scm_version=True, + ) + + .. code:: ini + + # setup.cfg + [metadata] + ... + + [options] + setup_requires = + setuptools_scm + ... + + .. important:: + + Ensure neither the ``[metadata]`` ``version`` option nor the ``[egg_info]`` + section are defined, as these will interfere with ``setuptools_scm``. + + You may also need to define a ``pyproject.toml`` file (`PEP-0518 + `_) to ensure you have the required + version of ``setuptools``: + + .. code:: ini + + # pyproject.toml + [build-system] + requires = ["setuptools>=30.3.0", "wheel", "setuptools_scm"] + + For more information, refer to the `setuptools issue #1002 + `_. + + + Programmatic usage + ------------------ + + In order to use ``setuptools_scm`` from code that is one directory deeper + than the project's root, you can use: + + .. code:: python + + from setuptools_scm import get_version + version = get_version(root='..', relative_to=__file__) + + See `setup.py Usage`_ above for how to use this within ``setup.py``. + + + Retrieving package version at runtime + ------------------------------------- + + If you have opted not to hardcode the version number inside the package, + you can retrieve it at runtime from PEP-0566_ metadata using + ``importlib.metadata`` from the standard library + or the `importlib_metadata`_ backport: + + .. code:: python + + from importlib.metadata import version, PackageNotFoundError + + try: + __version__ = version(__name__) + except PackageNotFoundError: + # package is not installed + pass + + Alternatively, you can use ``pkg_resources`` which is included in + ``setuptools``: + + .. code:: python + + from pkg_resources import get_distribution, DistributionNotFound + + try: + __version__ = get_distribution(__name__).version + except DistributionNotFound: + # package is not installed + pass + + This does place a runtime dependency on ``setuptools``. + + .. _PEP-0566: https://www.python.org/dev/peps/pep-0566/ + .. _importlib_metadata: https://pypi.org/project/importlib-metadata/ + + + Usage from Sphinx + ----------------- + + It is discouraged to use ``setuptools_scm`` from Sphinx itself, + instead use ``pkg_resources`` after editable/real installation: + + .. code:: python + + # contents of docs/conf.py + from pkg_resources import get_distribution + release = get_distribution('myproject').version + # for example take major/minor + version = '.'.join(release.split('.')[:2]) + + The underlying reason is, that services like *Read the Docs* sometimes change + the working directory for good reasons and using the installed metadata + prevents using needless volatile data there. + + Notable Plugins + ---------------- + + `setuptools_scm_git_archive `_ + 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 `_). + + 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_DEBUG: + when defined and not empty, + a lot of debug information will be printed as part of ``setuptools_scm`` + operating + + Extending setuptools_scm + ------------------------ + + ``setuptools_scm`` ships with a few ``setuptools`` entrypoints based hooks to + extend its default capabilities. + + Adding a new SCM + ~~~~~~~~~~~~~~~~ + + ``setuptools_scm`` provides two entrypoints for adding new SCMs: + + ``setuptools_scm.parse_scm`` + A function used to parse the metadata of the current workdir + using the name of the control directory/file of your SCM as the + entrypoint's name. E.g. for the built-in entrypoint for git the + entrypoint is named ``.git`` and references ``setuptools_scm.git:parse`` + + The return value MUST be a ``setuptools_scm.version.ScmVersion`` instance + created by the function ``setuptools_scm.version:meta``. + + ``setuptools_scm.files_command`` + Either a string containing a shell command that prints all SCM managed + files in its current working directory or a callable, that given a + pathname will return that list. + + Also use then name of your SCM control directory as name of the entrypoint. + + Version number construction + ~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + ``setuptools_scm.version_scheme`` + Configures how the version number is constructed given a + ``setuptools_scm.version.ScmVersion`` instance and should return a string + representing the version. + + Available implementations: + + :guess-next-dev: Automatically guesses the next development version (default). + Guesses the upcoming release by incrementing the pre-release segment if present, + otherwise by incrementing the micro segment. Then appends :code:`.devN`. + :post-release: generates post release versions (adds :code:`.postN`) + :python-simplified-semver: Basic semantic versioning. Guesses the upcoming release + by incrementing the minor segment and setting the micro segment to zero if the + current branch contains the string ``'feature'``, otherwise by incrementing the + micro version. Then appends :code:`.devN`. Not compatible with pre-releases. + :release-branch-semver: Semantic versioning for projects with release branches. The + same as ``guess-next-dev`` (incrementing the pre-release or micro segment) if on + a release branch: a branch whose name (ignoring namespace) parses as a version + that matches the most recent tag up to the minor segment. Otherwise if on a + non-release branch, increments the minor segment and sets the micro segment to + zero, then appends :code:`.devN`. + + ``setuptools_scm.local_scheme`` + Configures how the local part of a version is rendered given a + ``setuptools_scm.version.ScmVersion`` instance and should return a string + representing the local version. + Dates and times are in Coordinated Universal Time (UTC), because as part + of the version, they should be location independent. + + Available implementations: + + :node-and-date: adds the node on dev versions and the date on dirty + workdir (default) + :node-and-timestamp: like ``node-and-date`` but with a timestamp of + the form ``{:%Y%m%d%H%M%S}`` instead + :dirty-tag: adds ``+dirty`` if the current workdir has changes + :no-local-version: omits local version, useful e.g. because pypi does + not support it + + + Importing in ``setup.py`` + ~~~~~~~~~~~~~~~~~~~~~~~~~ + + To support usage in ``setup.py`` passing a callable into ``use_scm_version`` + is supported. + + Within that callable, ``setuptools_scm`` is available for import. + The callable must return the configuration. + + + .. code:: python + + # content of setup.py + import setuptools + + def myversion(): + from setuptools_scm.version import get_local_dirty_tag + def clean_scheme(version): + return get_local_dirty_tag(version) if version.dirty else '+clean' + + return {'local_scheme': clean_scheme} + + setup( + ..., + use_scm_version=myversion, + ... + ) + + + Note on testing non-installed versions + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + While the general advice is to test against a installed version, + some environments require a test prior to install, + + .. code:: + + $ python setup.py egg_info + $ PYTHONPATH=$PWD:$PWD/src pytest + + + Interaction with Enterprise Distributions + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + Some enterprise distributions like RHEL7 and others + ship rather old setuptools versions due to various release management details. + + On such distributions one might observe errors like: + + :code:``setuptools_scm.version.SetuptoolsOutdatedWarning: your setuptools is too old (<12)`` + + In those case its typically possible to build by using a sdist against ``setuptools_scm<2.0``. + As those old setuptools versions lack sensible types for versions, + modern setuptools_scm is unable to support them sensibly. + + In case the project you need to build can not be patched to either use old setuptools_scm, + its still possible to install a more recent version of setuptools in order to handle the build + and/or install the package by using wheels or eggs. + + + + + Code of Conduct + --------------- + + Everyone interacting in the ``setuptools_scm`` project's codebases, issue + trackers, chat rooms, and mailing lists is expected to follow the + `PyPA Code of Conduct`_. + + .. _PyPA Code of Conduct: https://www.pypa.io/en/latest/code-of-conduct/ + + Security Contact + ================ + + To report a security vulnerability, please use the + `Tidelift security contact `_. + Tidelift will coordinate the fix and disclosure. + +Platform: UNKNOWN +Classifier: Development Status :: 5 - Production/Stable +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: MIT License +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 2.7 +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.5 +Classifier: Programming Language :: Python :: 3.6 +Classifier: Programming Language :: Python :: 3.7 +Classifier: Programming Language :: Python :: 3.8 +Classifier: Topic :: Software Development :: Libraries +Classifier: Topic :: Software Development :: Version Control +Classifier: Topic :: System :: Software Distribution +Classifier: Topic :: Utilities +Requires-Python: !=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7 +Provides-Extra: toml diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..074eb6c --- /dev/null +++ b/README.rst @@ -0,0 +1,589 @@ +setuptools_scm +=============== + +``setuptools_scm`` handles managing your Python package versions +in SCM metadata instead of declaring them as the version argument +or in a SCM managed file. + +Additionally ``setuptools_scm`` provides setuptools with a list of files that are managed by the SCM +(i.e. it automatically adds all of the SCM-managed files to the sdist). +Unwanted files must be excluded by discarding them via ``MANIFEST.in``. + +.. image:: https://travis-ci.org/pypa/setuptools_scm.svg?branch=master + :target: https://travis-ci.org/pypa/setuptools_scm + +.. image:: https://tidelift.com/badges/package/pypi/setuptools-scm + :target: https://tidelift.com/subscription/pkg/pypi-setuptools-scm?utm_source=pypi-setuptools-scm&utm_medium=readme + + +``pyproject.toml`` usage +------------------------ + +The preferred way to configure ``setuptools_scm`` is to author +settings in a ``tool.setuptools_scm`` section of ``pyproject.toml``. + +This feature requires Setuptools 42 or later, released in Nov, 2019. +If your project needs to support build from sdist on older versions +of Setuptools, you will need to also implement the ``setup.py usage`` +for those legacy environments. + +First, ensure that ``setuptools_scm`` is present during the project's +built step by specifying it as one of the build requirements. + +.. code:: toml + + # pyproject.toml + [build-system] + requires = ["setuptools>=42", "wheel", "setuptools_scm[toml]>=3.4"] + +Note that the ``toml`` extra must be supplied. + +That will be sufficient to require ``setuptools_scm`` for projects +that support PEP 518 (`pip `_ 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 +------------------ + +The following settings are considered legacy behavior and +superseded by the ``pyproject.toml`` usage, but for maximal +compatibility, projects may also supply the configuration in +this older form. + +To use ``setuptools_scm`` just modify your project's ``setup.py`` file +like this: + +* Add ``setuptools_scm`` to the ``setup_requires`` parameter. +* Add the ``use_scm_version`` parameter and set it to ``True``. + +For example: + +.. code:: python + + from setuptools import setup + setup( + ..., + use_scm_version=True, + setup_requires=['setuptools_scm'], + ..., + ) + +Arguments to ``get_version()`` (see below) may be passed as a dictionary to +``use_scm_version``. For example: + +.. code:: python + + from setuptools import setup + setup( + ..., + use_scm_version = { + "root": "..", + "relative_to": __file__, + "local_scheme": "node-and-timestamp" + }, + setup_requires=['setuptools_scm'], + ..., + ) + +You can confirm the version number locally via ``setup.py``: + +.. code-block:: shell + + $ python setup.py --version + +.. note:: + + If you see unusual version numbers for packages but ``python setup.py + --version`` reports the expected version number, ensure ``[egg_info]`` is + not defined in ``setup.cfg``. + + +``setup.cfg`` usage +------------------- + +If using `setuptools 30.3.0 +`_ +or greater, you can store ``setup_requires`` configuration in ``setup.cfg``. +However, ``use_scm_version`` must still be placed in ``setup.py``. For example: + +.. code:: python + + # setup.py + from setuptools import setup + setup( + use_scm_version=True, + ) + +.. code:: ini + + # setup.cfg + [metadata] + ... + + [options] + setup_requires = + setuptools_scm + ... + +.. important:: + + Ensure neither the ``[metadata]`` ``version`` option nor the ``[egg_info]`` + section are defined, as these will interfere with ``setuptools_scm``. + +You may also need to define a ``pyproject.toml`` file (`PEP-0518 +`_) to ensure you have the required +version of ``setuptools``: + +.. code:: ini + + # pyproject.toml + [build-system] + requires = ["setuptools>=30.3.0", "wheel", "setuptools_scm"] + +For more information, refer to the `setuptools issue #1002 +`_. + + +Programmatic usage +------------------ + +In order to use ``setuptools_scm`` from code that is one directory deeper +than the project's root, you can use: + +.. code:: python + + from setuptools_scm import get_version + version = get_version(root='..', relative_to=__file__) + +See `setup.py Usage`_ above for how to use this within ``setup.py``. + + +Retrieving package version at runtime +------------------------------------- + +If you have opted not to hardcode the version number inside the package, +you can retrieve it at runtime from PEP-0566_ metadata using +``importlib.metadata`` from the standard library +or the `importlib_metadata`_ backport: + +.. code:: python + + from importlib.metadata import version, PackageNotFoundError + + try: + __version__ = version(__name__) + except PackageNotFoundError: + # package is not installed + pass + +Alternatively, you can use ``pkg_resources`` which is included in +``setuptools``: + +.. code:: python + + from pkg_resources import get_distribution, DistributionNotFound + + try: + __version__ = get_distribution(__name__).version + except DistributionNotFound: + # package is not installed + pass + +This does place a runtime dependency on ``setuptools``. + +.. _PEP-0566: https://www.python.org/dev/peps/pep-0566/ +.. _importlib_metadata: https://pypi.org/project/importlib-metadata/ + + +Usage from Sphinx +----------------- + +It is discouraged to use ``setuptools_scm`` from Sphinx itself, +instead use ``pkg_resources`` after editable/real installation: + +.. code:: python + + # contents of docs/conf.py + from pkg_resources import get_distribution + release = get_distribution('myproject').version + # for example take major/minor + version = '.'.join(release.split('.')[:2]) + +The underlying reason is, that services like *Read the Docs* sometimes change +the working directory for good reasons and using the installed metadata +prevents using needless volatile data there. + +Notable Plugins +---------------- + +`setuptools_scm_git_archive `_ + 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 `_). + +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_DEBUG: + when defined and not empty, + a lot of debug information will be printed as part of ``setuptools_scm`` + operating + +Extending setuptools_scm +------------------------ + +``setuptools_scm`` ships with a few ``setuptools`` entrypoints based hooks to +extend its default capabilities. + +Adding a new SCM +~~~~~~~~~~~~~~~~ + +``setuptools_scm`` provides two entrypoints for adding new SCMs: + +``setuptools_scm.parse_scm`` + A function used to parse the metadata of the current workdir + using the name of the control directory/file of your SCM as the + entrypoint's name. E.g. for the built-in entrypoint for git the + entrypoint is named ``.git`` and references ``setuptools_scm.git:parse`` + + The return value MUST be a ``setuptools_scm.version.ScmVersion`` instance + created by the function ``setuptools_scm.version:meta``. + +``setuptools_scm.files_command`` + Either a string containing a shell command that prints all SCM managed + files in its current working directory or a callable, that given a + pathname will return that list. + + Also use then name of your SCM control directory as name of the entrypoint. + +Version number construction +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +``setuptools_scm.version_scheme`` + Configures how the version number is constructed given a + ``setuptools_scm.version.ScmVersion`` instance and should return a string + representing the version. + + Available implementations: + + :guess-next-dev: Automatically guesses the next development version (default). + Guesses the upcoming release by incrementing the pre-release segment if present, + otherwise by incrementing the micro segment. Then appends :code:`.devN`. + :post-release: generates post release versions (adds :code:`.postN`) + :python-simplified-semver: Basic semantic versioning. Guesses the upcoming release + by incrementing the minor segment and setting the micro segment to zero if the + current branch contains the string ``'feature'``, otherwise by incrementing the + micro version. Then appends :code:`.devN`. Not compatible with pre-releases. + :release-branch-semver: Semantic versioning for projects with release branches. The + same as ``guess-next-dev`` (incrementing the pre-release or micro segment) if on + a release branch: a branch whose name (ignoring namespace) parses as a version + that matches the most recent tag up to the minor segment. Otherwise if on a + non-release branch, increments the minor segment and sets the micro segment to + zero, then appends :code:`.devN`. + +``setuptools_scm.local_scheme`` + Configures how the local part of a version is rendered given a + ``setuptools_scm.version.ScmVersion`` instance and should return a string + representing the local version. + Dates and times are in Coordinated Universal Time (UTC), because as part + of the version, they should be location independent. + + Available implementations: + + :node-and-date: adds the node on dev versions and the date on dirty + workdir (default) + :node-and-timestamp: like ``node-and-date`` but with a timestamp of + the form ``{:%Y%m%d%H%M%S}`` instead + :dirty-tag: adds ``+dirty`` if the current workdir has changes + :no-local-version: omits local version, useful e.g. because pypi does + not support it + + +Importing in ``setup.py`` +~~~~~~~~~~~~~~~~~~~~~~~~~ + +To support usage in ``setup.py`` passing a callable into ``use_scm_version`` +is supported. + +Within that callable, ``setuptools_scm`` is available for import. +The callable must return the configuration. + + +.. code:: python + + # content of setup.py + import setuptools + + def myversion(): + from setuptools_scm.version import get_local_dirty_tag + def clean_scheme(version): + return get_local_dirty_tag(version) if version.dirty else '+clean' + + return {'local_scheme': clean_scheme} + + setup( + ..., + use_scm_version=myversion, + ... + ) + + +Note on testing non-installed versions +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +While the general advice is to test against a installed version, +some environments require a test prior to install, + +.. code:: + + $ python setup.py egg_info + $ PYTHONPATH=$PWD:$PWD/src pytest + + +Interaction with Enterprise Distributions +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Some enterprise distributions like RHEL7 and others +ship rather old setuptools versions due to various release management details. + +On such distributions one might observe errors like: + +:code:``setuptools_scm.version.SetuptoolsOutdatedWarning: your setuptools is too old (<12)`` + +In those case its typically possible to build by using a sdist against ``setuptools_scm<2.0``. +As those old setuptools versions lack sensible types for versions, +modern setuptools_scm is unable to support them sensibly. + +In case the project you need to build can not be patched to either use old setuptools_scm, +its still possible to install a more recent version of setuptools in order to handle the build +and/or install the package by using wheels or eggs. + + + + +Code of Conduct +--------------- + +Everyone interacting in the ``setuptools_scm`` project's codebases, issue +trackers, chat rooms, and mailing lists is expected to follow the +`PyPA Code of Conduct`_. + +.. _PyPA Code of Conduct: https://www.pypa.io/en/latest/code-of-conduct/ + +Security Contact +================ + +To report a security vulnerability, please use the +`Tidelift security contact `_. +Tidelift will coordinate the fix and disclosure. diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..f90d4d1 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["setuptools>=34.4", "wheel"] +build-backend = "setuptools.build_meta" diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..110f700 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,76 @@ +[metadata] +license_file = LICENSE +license = MIT +name = setuptools_scm +url = https://github.com/pypa/setuptools_scm/ +author = Ronny Pfannschmidt +author_email = opensource@ronnypfannschmidt.de +description = the blessed package to manage your versions by scm tags +long_description = file:README.rst +classifiers = + Development Status :: 5 - Production/Stable + Intended Audience :: Developers + License :: OSI Approved :: MIT License + Programming Language :: Python + Programming Language :: Python :: 2.7 + Programming Language :: Python :: 3 + Programming Language :: Python :: 3.5 + Programming Language :: Python :: 3.6 + Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3.8 + Topic :: Software Development :: Libraries + Topic :: Software Development :: Version Control + Topic :: System :: Software Distribution + Topic :: Utilities + +[options] +zip_safe = true +python_requires = >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.* +install_requires = + setuptools +packages = find: +package_dir = + =src + +[options.packages.find] +where = src + +[options.extras_require] +toml = toml + +[options.entry_points] +distutils.setup_keywords = + use_scm_version = setuptools_scm.integration:version_keyword +setuptools.file_finders = + setuptools_scm = setuptools_scm.integration:find_files +setuptools.finalize_distribution_options = + setuptools_scm = setuptools_scm.integration:infer_version +setuptools_scm.parse_scm = + .hg = setuptools_scm.hg:parse + .git = setuptools_scm.git:parse +setuptools_scm.parse_scm_fallback = + .hg_archival.txt = setuptools_scm.hg:parse_archival + PKG-INFO = setuptools_scm.hacks:parse_pkginfo + pip-egg-info = setuptools_scm.hacks:parse_pip_egg_info + setup.py = setuptools_scm.hacks:fallback_version +setuptools_scm.files_command = + .hg = setuptools_scm.file_finder_hg:hg_find_files + .git = setuptools_scm.file_finder_git:git_find_files +setuptools_scm.version_scheme = + guess-next-dev = setuptools_scm.version:guess_next_dev_version + post-release = setuptools_scm.version:postrelease_version + python-simplified-semver = setuptools_scm.version:simplified_semver_version + release-branch-semver = setuptools_scm.version:release_branch_semver_version +setuptools_scm.local_scheme = + node-and-date = setuptools_scm.version:get_local_node_and_date + node-and-timestamp = setuptools_scm.version:get_local_node_and_timestamp + dirty-tag = setuptools_scm.version:get_local_dirty_tag + no-local-version = setuptools_scm.version:get_no_local_node + +[bdist_wheel] +universal = 1 + +[egg_info] +tag_build = +tag_date = 0 + diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..0b3e0c7 --- /dev/null +++ b/setup.py @@ -0,0 +1,53 @@ +"""\ +important note: + +the setup of setuptools_scm is self-using, +the first execution of `python setup.py egg_info` +will generate partial data +its critical to run `python setup.py egg_info` +once before running sdist or easy_install on a fresh checkouts + +pip usage is recommended +""" +from __future__ import print_function +import os +import sys +import setuptools + + +def scm_config(): + here = os.path.dirname(os.path.abspath(__file__)) + src = os.path.join(here, "src") + egg_info = os.path.join(src, "setuptools_scm.egg-info") + has_entrypoints = os.path.isdir(egg_info) + import pkg_resources + + sys.path.insert(0, src) + pkg_resources.working_set.add_entry(src) + # FIXME: remove debug + print(src) + print(pkg_resources.working_set) + from setuptools_scm.hacks import parse_pkginfo + from setuptools_scm.git import parse as parse_git + from setuptools_scm.version import guess_next_dev_version, get_local_node_and_date + + def parse(root): + try: + return parse_pkginfo(root) + except IOError: + return parse_git(root) + + config = dict( + version_scheme=guess_next_dev_version, local_scheme=get_local_node_and_date + ) + + if has_entrypoints: + return dict(use_scm_version=config) + else: + from setuptools_scm import get_version + + return dict(version=get_version(root=here, parse=parse, **config)) + + +if __name__ == "__main__": + setuptools.setup(**scm_config()) diff --git a/src/setuptools_scm.egg-info/PKG-INFO b/src/setuptools_scm.egg-info/PKG-INFO new file mode 100644 index 0000000..ea5bbd2 --- /dev/null +++ b/src/setuptools_scm.egg-info/PKG-INFO @@ -0,0 +1,615 @@ +Metadata-Version: 2.1 +Name: setuptools-scm +Version: 4.1.2 +Summary: the blessed package to manage your versions by scm tags +Home-page: https://github.com/pypa/setuptools_scm/ +Author: Ronny Pfannschmidt +Author-email: opensource@ronnypfannschmidt.de +License: MIT +Description: setuptools_scm + =============== + + ``setuptools_scm`` handles managing your Python package versions + in SCM metadata instead of declaring them as the version argument + or in a SCM managed file. + + Additionally ``setuptools_scm`` provides setuptools with a list of files that are managed by the SCM + (i.e. it automatically adds all of the SCM-managed files to the sdist). + Unwanted files must be excluded by discarding them via ``MANIFEST.in``. + + .. image:: https://travis-ci.org/pypa/setuptools_scm.svg?branch=master + :target: https://travis-ci.org/pypa/setuptools_scm + + .. image:: https://tidelift.com/badges/package/pypi/setuptools-scm + :target: https://tidelift.com/subscription/pkg/pypi-setuptools-scm?utm_source=pypi-setuptools-scm&utm_medium=readme + + + ``pyproject.toml`` usage + ------------------------ + + The preferred way to configure ``setuptools_scm`` is to author + settings in a ``tool.setuptools_scm`` section of ``pyproject.toml``. + + This feature requires Setuptools 42 or later, released in Nov, 2019. + If your project needs to support build from sdist on older versions + of Setuptools, you will need to also implement the ``setup.py usage`` + for those legacy environments. + + First, ensure that ``setuptools_scm`` is present during the project's + built step by specifying it as one of the build requirements. + + .. code:: toml + + # pyproject.toml + [build-system] + requires = ["setuptools>=42", "wheel", "setuptools_scm[toml]>=3.4"] + + Note that the ``toml`` extra must be supplied. + + That will be sufficient to require ``setuptools_scm`` for projects + that support PEP 518 (`pip `_ 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 + ------------------ + + The following settings are considered legacy behavior and + superseded by the ``pyproject.toml`` usage, but for maximal + compatibility, projects may also supply the configuration in + this older form. + + To use ``setuptools_scm`` just modify your project's ``setup.py`` file + like this: + + * Add ``setuptools_scm`` to the ``setup_requires`` parameter. + * Add the ``use_scm_version`` parameter and set it to ``True``. + + For example: + + .. code:: python + + from setuptools import setup + setup( + ..., + use_scm_version=True, + setup_requires=['setuptools_scm'], + ..., + ) + + Arguments to ``get_version()`` (see below) may be passed as a dictionary to + ``use_scm_version``. For example: + + .. code:: python + + from setuptools import setup + setup( + ..., + use_scm_version = { + "root": "..", + "relative_to": __file__, + "local_scheme": "node-and-timestamp" + }, + setup_requires=['setuptools_scm'], + ..., + ) + + You can confirm the version number locally via ``setup.py``: + + .. code-block:: shell + + $ python setup.py --version + + .. note:: + + If you see unusual version numbers for packages but ``python setup.py + --version`` reports the expected version number, ensure ``[egg_info]`` is + not defined in ``setup.cfg``. + + + ``setup.cfg`` usage + ------------------- + + If using `setuptools 30.3.0 + `_ + or greater, you can store ``setup_requires`` configuration in ``setup.cfg``. + However, ``use_scm_version`` must still be placed in ``setup.py``. For example: + + .. code:: python + + # setup.py + from setuptools import setup + setup( + use_scm_version=True, + ) + + .. code:: ini + + # setup.cfg + [metadata] + ... + + [options] + setup_requires = + setuptools_scm + ... + + .. important:: + + Ensure neither the ``[metadata]`` ``version`` option nor the ``[egg_info]`` + section are defined, as these will interfere with ``setuptools_scm``. + + You may also need to define a ``pyproject.toml`` file (`PEP-0518 + `_) to ensure you have the required + version of ``setuptools``: + + .. code:: ini + + # pyproject.toml + [build-system] + requires = ["setuptools>=30.3.0", "wheel", "setuptools_scm"] + + For more information, refer to the `setuptools issue #1002 + `_. + + + Programmatic usage + ------------------ + + In order to use ``setuptools_scm`` from code that is one directory deeper + than the project's root, you can use: + + .. code:: python + + from setuptools_scm import get_version + version = get_version(root='..', relative_to=__file__) + + See `setup.py Usage`_ above for how to use this within ``setup.py``. + + + Retrieving package version at runtime + ------------------------------------- + + If you have opted not to hardcode the version number inside the package, + you can retrieve it at runtime from PEP-0566_ metadata using + ``importlib.metadata`` from the standard library + or the `importlib_metadata`_ backport: + + .. code:: python + + from importlib.metadata import version, PackageNotFoundError + + try: + __version__ = version(__name__) + except PackageNotFoundError: + # package is not installed + pass + + Alternatively, you can use ``pkg_resources`` which is included in + ``setuptools``: + + .. code:: python + + from pkg_resources import get_distribution, DistributionNotFound + + try: + __version__ = get_distribution(__name__).version + except DistributionNotFound: + # package is not installed + pass + + This does place a runtime dependency on ``setuptools``. + + .. _PEP-0566: https://www.python.org/dev/peps/pep-0566/ + .. _importlib_metadata: https://pypi.org/project/importlib-metadata/ + + + Usage from Sphinx + ----------------- + + It is discouraged to use ``setuptools_scm`` from Sphinx itself, + instead use ``pkg_resources`` after editable/real installation: + + .. code:: python + + # contents of docs/conf.py + from pkg_resources import get_distribution + release = get_distribution('myproject').version + # for example take major/minor + version = '.'.join(release.split('.')[:2]) + + The underlying reason is, that services like *Read the Docs* sometimes change + the working directory for good reasons and using the installed metadata + prevents using needless volatile data there. + + Notable Plugins + ---------------- + + `setuptools_scm_git_archive `_ + 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 `_). + + 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_DEBUG: + when defined and not empty, + a lot of debug information will be printed as part of ``setuptools_scm`` + operating + + Extending setuptools_scm + ------------------------ + + ``setuptools_scm`` ships with a few ``setuptools`` entrypoints based hooks to + extend its default capabilities. + + Adding a new SCM + ~~~~~~~~~~~~~~~~ + + ``setuptools_scm`` provides two entrypoints for adding new SCMs: + + ``setuptools_scm.parse_scm`` + A function used to parse the metadata of the current workdir + using the name of the control directory/file of your SCM as the + entrypoint's name. E.g. for the built-in entrypoint for git the + entrypoint is named ``.git`` and references ``setuptools_scm.git:parse`` + + The return value MUST be a ``setuptools_scm.version.ScmVersion`` instance + created by the function ``setuptools_scm.version:meta``. + + ``setuptools_scm.files_command`` + Either a string containing a shell command that prints all SCM managed + files in its current working directory or a callable, that given a + pathname will return that list. + + Also use then name of your SCM control directory as name of the entrypoint. + + Version number construction + ~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + ``setuptools_scm.version_scheme`` + Configures how the version number is constructed given a + ``setuptools_scm.version.ScmVersion`` instance and should return a string + representing the version. + + Available implementations: + + :guess-next-dev: Automatically guesses the next development version (default). + Guesses the upcoming release by incrementing the pre-release segment if present, + otherwise by incrementing the micro segment. Then appends :code:`.devN`. + :post-release: generates post release versions (adds :code:`.postN`) + :python-simplified-semver: Basic semantic versioning. Guesses the upcoming release + by incrementing the minor segment and setting the micro segment to zero if the + current branch contains the string ``'feature'``, otherwise by incrementing the + micro version. Then appends :code:`.devN`. Not compatible with pre-releases. + :release-branch-semver: Semantic versioning for projects with release branches. The + same as ``guess-next-dev`` (incrementing the pre-release or micro segment) if on + a release branch: a branch whose name (ignoring namespace) parses as a version + that matches the most recent tag up to the minor segment. Otherwise if on a + non-release branch, increments the minor segment and sets the micro segment to + zero, then appends :code:`.devN`. + + ``setuptools_scm.local_scheme`` + Configures how the local part of a version is rendered given a + ``setuptools_scm.version.ScmVersion`` instance and should return a string + representing the local version. + Dates and times are in Coordinated Universal Time (UTC), because as part + of the version, they should be location independent. + + Available implementations: + + :node-and-date: adds the node on dev versions and the date on dirty + workdir (default) + :node-and-timestamp: like ``node-and-date`` but with a timestamp of + the form ``{:%Y%m%d%H%M%S}`` instead + :dirty-tag: adds ``+dirty`` if the current workdir has changes + :no-local-version: omits local version, useful e.g. because pypi does + not support it + + + Importing in ``setup.py`` + ~~~~~~~~~~~~~~~~~~~~~~~~~ + + To support usage in ``setup.py`` passing a callable into ``use_scm_version`` + is supported. + + Within that callable, ``setuptools_scm`` is available for import. + The callable must return the configuration. + + + .. code:: python + + # content of setup.py + import setuptools + + def myversion(): + from setuptools_scm.version import get_local_dirty_tag + def clean_scheme(version): + return get_local_dirty_tag(version) if version.dirty else '+clean' + + return {'local_scheme': clean_scheme} + + setup( + ..., + use_scm_version=myversion, + ... + ) + + + Note on testing non-installed versions + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + While the general advice is to test against a installed version, + some environments require a test prior to install, + + .. code:: + + $ python setup.py egg_info + $ PYTHONPATH=$PWD:$PWD/src pytest + + + Interaction with Enterprise Distributions + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + Some enterprise distributions like RHEL7 and others + ship rather old setuptools versions due to various release management details. + + On such distributions one might observe errors like: + + :code:``setuptools_scm.version.SetuptoolsOutdatedWarning: your setuptools is too old (<12)`` + + In those case its typically possible to build by using a sdist against ``setuptools_scm<2.0``. + As those old setuptools versions lack sensible types for versions, + modern setuptools_scm is unable to support them sensibly. + + In case the project you need to build can not be patched to either use old setuptools_scm, + its still possible to install a more recent version of setuptools in order to handle the build + and/or install the package by using wheels or eggs. + + + + + Code of Conduct + --------------- + + Everyone interacting in the ``setuptools_scm`` project's codebases, issue + trackers, chat rooms, and mailing lists is expected to follow the + `PyPA Code of Conduct`_. + + .. _PyPA Code of Conduct: https://www.pypa.io/en/latest/code-of-conduct/ + + Security Contact + ================ + + To report a security vulnerability, please use the + `Tidelift security contact `_. + Tidelift will coordinate the fix and disclosure. + +Platform: UNKNOWN +Classifier: Development Status :: 5 - Production/Stable +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: MIT License +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 2.7 +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.5 +Classifier: Programming Language :: Python :: 3.6 +Classifier: Programming Language :: Python :: 3.7 +Classifier: Programming Language :: Python :: 3.8 +Classifier: Topic :: Software Development :: Libraries +Classifier: Topic :: Software Development :: Version Control +Classifier: Topic :: System :: Software Distribution +Classifier: Topic :: Utilities +Requires-Python: !=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7 +Provides-Extra: toml diff --git a/src/setuptools_scm.egg-info/SOURCES.txt b/src/setuptools_scm.egg-info/SOURCES.txt new file mode 100644 index 0000000..3953c8e --- /dev/null +++ b/src/setuptools_scm.egg-info/SOURCES.txt @@ -0,0 +1,47 @@ +.gitignore +CHANGELOG.rst +LICENSE +MANIFEST.in +README.rst +pyproject.toml +setup.cfg +setup.py +tox.ini +.github/FUNDING.yml +.github/workflows/pre-commit.yml +.github/workflows/python-tests.yml +src/setuptools_scm/__init__.py +src/setuptools_scm/__main__.py +src/setuptools_scm/config.py +src/setuptools_scm/discover.py +src/setuptools_scm/file_finder.py +src/setuptools_scm/file_finder_git.py +src/setuptools_scm/file_finder_hg.py +src/setuptools_scm/git.py +src/setuptools_scm/hacks.py +src/setuptools_scm/hg.py +src/setuptools_scm/integration.py +src/setuptools_scm/utils.py +src/setuptools_scm/version.py +src/setuptools_scm/win_py31_compat.py +src/setuptools_scm.egg-info/PKG-INFO +src/setuptools_scm.egg-info/SOURCES.txt +src/setuptools_scm.egg-info/dependency_links.txt +src/setuptools_scm.egg-info/entry_points.txt +src/setuptools_scm.egg-info/requires.txt +src/setuptools_scm.egg-info/top_level.txt +src/setuptools_scm.egg-info/zip-safe +testing/check_self_install.py +testing/conftest.py +testing/play_out_381.bash +testing/test_basic_api.py +testing/test_config.py +testing/test_file_finder.py +testing/test_functions.py +testing/test_git.py +testing/test_integration.py +testing/test_main.py +testing/test_mercurial.py +testing/test_regressions.py +testing/test_setuptools_support.py +testing/test_version.py \ No newline at end of file diff --git a/src/setuptools_scm.egg-info/dependency_links.txt b/src/setuptools_scm.egg-info/dependency_links.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/setuptools_scm.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/src/setuptools_scm.egg-info/entry_points.txt b/src/setuptools_scm.egg-info/entry_points.txt new file mode 100644 index 0000000..7e46afc --- /dev/null +++ b/src/setuptools_scm.egg-info/entry_points.txt @@ -0,0 +1,35 @@ +[distutils.setup_keywords] +use_scm_version = setuptools_scm.integration:version_keyword + +[setuptools.file_finders] +setuptools_scm = setuptools_scm.integration:find_files + +[setuptools.finalize_distribution_options] +setuptools_scm = setuptools_scm.integration:infer_version + +[setuptools_scm.files_command] +.git = setuptools_scm.file_finder_git:git_find_files +.hg = setuptools_scm.file_finder_hg:hg_find_files + +[setuptools_scm.local_scheme] +dirty-tag = setuptools_scm.version:get_local_dirty_tag +no-local-version = setuptools_scm.version:get_no_local_node +node-and-date = setuptools_scm.version:get_local_node_and_date +node-and-timestamp = setuptools_scm.version:get_local_node_and_timestamp + +[setuptools_scm.parse_scm] +.git = setuptools_scm.git:parse +.hg = setuptools_scm.hg:parse + +[setuptools_scm.parse_scm_fallback] +.hg_archival.txt = setuptools_scm.hg:parse_archival +PKG-INFO = setuptools_scm.hacks:parse_pkginfo +pip-egg-info = setuptools_scm.hacks:parse_pip_egg_info +setup.py = setuptools_scm.hacks:fallback_version + +[setuptools_scm.version_scheme] +guess-next-dev = setuptools_scm.version:guess_next_dev_version +post-release = setuptools_scm.version:postrelease_version +python-simplified-semver = setuptools_scm.version:simplified_semver_version +release-branch-semver = setuptools_scm.version:release_branch_semver_version + diff --git a/src/setuptools_scm.egg-info/requires.txt b/src/setuptools_scm.egg-info/requires.txt new file mode 100644 index 0000000..1fad750 --- /dev/null +++ b/src/setuptools_scm.egg-info/requires.txt @@ -0,0 +1,4 @@ +setuptools + +[toml] +toml diff --git a/src/setuptools_scm.egg-info/top_level.txt b/src/setuptools_scm.egg-info/top_level.txt new file mode 100644 index 0000000..cba8d88 --- /dev/null +++ b/src/setuptools_scm.egg-info/top_level.txt @@ -0,0 +1 @@ +setuptools_scm diff --git a/src/setuptools_scm.egg-info/zip-safe b/src/setuptools_scm.egg-info/zip-safe new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/setuptools_scm.egg-info/zip-safe @@ -0,0 +1 @@ + diff --git a/src/setuptools_scm/__init__.py b/src/setuptools_scm/__init__.py new file mode 100644 index 0000000..6b22b28 --- /dev/null +++ b/src/setuptools_scm/__init__.py @@ -0,0 +1,163 @@ +""" +:copyright: 2010-2015 by Ronny Pfannschmidt +:license: MIT +""" +import os +import warnings + +from .config import ( + Configuration, + DEFAULT_VERSION_SCHEME, + DEFAULT_LOCAL_SCHEME, + DEFAULT_TAG_REGEX, +) +from .utils import function_has_arg, string_types +from .version import format_version, meta +from .discover import iter_matching_entrypoints + +PRETEND_KEY = "SETUPTOOLS_SCM_PRETEND_VERSION" + +TEMPLATES = { + ".py": """\ +# coding: utf-8 +# file generated by setuptools_scm +# don't change, don't track in version control +version = {version!r} +""", + ".txt": "{version}", +} + + +def version_from_scm(root): + warnings.warn( + "version_from_scm is deprecated please use get_version", + category=DeprecationWarning, + ) + config = Configuration() + config.root = root + # TODO: Is it API? + return _version_from_entrypoints(config) + + +def _call_entrypoint_fn(root, config, fn): + if function_has_arg(fn, "config"): + return fn(root, config=config) + else: + warnings.warn( + "parse functions are required to provide a named argument" + " 'config' in the future.", + category=PendingDeprecationWarning, + stacklevel=2, + ) + return fn(root) + + +def _version_from_entrypoints(config, fallback=False): + if fallback: + entrypoint = "setuptools_scm.parse_scm_fallback" + root = config.fallback_root + else: + entrypoint = "setuptools_scm.parse_scm" + root = config.absolute_root + for ep in iter_matching_entrypoints(root, entrypoint): + version = _call_entrypoint_fn(root, config, ep.load()) + + if version: + return version + + +def dump_version(root, version, write_to, template=None): + assert isinstance(version, string_types) + if not write_to: + return + target = os.path.normpath(os.path.join(root, write_to)) + ext = os.path.splitext(target)[1] + template = template or TEMPLATES.get(ext) + + if template is None: + raise ValueError( + "bad file format: '{}' (of {}) \nonly *.txt and *.py are supported".format( + os.path.splitext(target)[1], target + ) + ) + with open(target, "w") as fp: + fp.write(template.format(version=version)) + + +def _do_parse(config): + pretended = os.environ.get(PRETEND_KEY) + if pretended: + # we use meta here since the pretended version + # must adhere to the pep to begin with + return meta(tag=pretended, preformatted=True, config=config) + + if config.parse: + parse_result = _call_entrypoint_fn(config.absolute_root, config, config.parse) + if isinstance(parse_result, string_types): + raise TypeError( + "version parse result was a string\nplease return a parsed version" + ) + version = parse_result or _version_from_entrypoints(config, fallback=True) + else: + # include fallbacks after dropping them from the main entrypoint + version = _version_from_entrypoints(config) or _version_from_entrypoints( + config, fallback=True + ) + + if version: + return version + + raise LookupError( + "setuptools-scm was unable to detect version for %r.\n\n" + "Make sure you're either building from a fully intact git repository " + "or PyPI tarballs. Most other sources (such as GitHub's tarballs, a " + "git checkout without the .git folder) don't contain the necessary " + "metadata and will not work.\n\n" + "For example, if you're using pip, instead of " + "https://github.com/user/proj/archive/master.zip " + "use git+https://github.com/user/proj.git#egg=proj" % config.absolute_root + ) + + +def get_version( + root=".", + version_scheme=DEFAULT_VERSION_SCHEME, + local_scheme=DEFAULT_LOCAL_SCHEME, + write_to=None, + write_to_template=None, + relative_to=None, + tag_regex=DEFAULT_TAG_REGEX, + parentdir_prefix_version=None, + fallback_version=None, + fallback_root=".", + parse=None, + git_describe_command=None, +): + """ + If supplied, relative_to should be a file from which root may + be resolved. Typically called by a script or module that is not + in the root of the repository to direct setuptools_scm to the + root of the repository by supplying ``__file__``. + """ + + config = Configuration(**locals()) + return _get_version(config) + + +def _get_version(config): + parsed_version = _do_parse(config) + + if parsed_version: + version_string = format_version( + parsed_version, + version_scheme=config.version_scheme, + local_scheme=config.local_scheme, + ) + dump_version( + root=config.root, + version=version_string, + write_to=config.write_to, + template=config.write_to_template, + ) + + return version_string diff --git a/src/setuptools_scm/__main__.py b/src/setuptools_scm/__main__.py new file mode 100644 index 0000000..a464c51 --- /dev/null +++ b/src/setuptools_scm/__main__.py @@ -0,0 +1,17 @@ +from __future__ import print_function +import sys +from setuptools_scm import get_version +from setuptools_scm.integration import find_files +from setuptools_scm.version import _warn_if_setuptools_outdated + + +def main(): + _warn_if_setuptools_outdated() + print("Guessed Version", get_version()) + if "ls" in sys.argv: + for fname in find_files("."): + print(fname) + + +if __name__ == "__main__": + main() diff --git a/src/setuptools_scm/config.py b/src/setuptools_scm/config.py new file mode 100644 index 0000000..e7f4d72 --- /dev/null +++ b/src/setuptools_scm/config.py @@ -0,0 +1,125 @@ +""" configuration """ +from __future__ import print_function, unicode_literals +import os +import re +import warnings + +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, relative_to): + if relative_to: + if os.path.isabs(root) and not root.startswith(relative_to): + warnings.warn( + "absolute root path '%s' overrides relative_to '%s'" + % (root, relative_to) + ) + root = os.path.join(os.path.dirname(relative_to), root) + return os.path.abspath(root) + + +class Configuration(object): + """ Global configuration model """ + + def __init__( + self, + relative_to=None, + root=".", + version_scheme=DEFAULT_VERSION_SCHEME, + local_scheme=DEFAULT_LOCAL_SCHEME, + write_to=None, + write_to_template=None, + tag_regex=DEFAULT_TAG_REGEX, + parentdir_prefix_version=None, + fallback_version=None, + fallback_root=".", + parse=None, + git_describe_command=None, + ): + # TODO: + self._relative_to = relative_to + self._root = "." + + self.root = root + self.version_scheme = version_scheme + self.local_scheme = local_scheme + self.write_to = write_to + self.write_to_template = write_to_template + self.parentdir_prefix_version = parentdir_prefix_version + self.fallback_version = fallback_version + self.fallback_root = fallback_root + self.parse = parse + self.tag_regex = tag_regex + self.git_describe_command = git_describe_command + + @property + def fallback_root(self): + return self._fallback_root + + @fallback_root.setter + def fallback_root(self, value): + self._fallback_root = os.path.abspath(value) + + @property + def absolute_root(self): + return self._absolute_root + + @property + def relative_to(self): + return self._relative_to + + @relative_to.setter + def relative_to(self, value): + self._absolute_root = _check_absolute_root(self._root, value) + self._relative_to = value + trace("root", repr(self._absolute_root)) + + @property + def root(self): + return self._root + + @root.setter + def root(self, value): + self._absolute_root = _check_absolute_root(value, self._relative_to) + self._root = value + trace("root", repr(self._absolute_root)) + + @property + def tag_regex(self): + return self._tag_regex + + @tag_regex.setter + def tag_regex(self, value): + self._tag_regex = _check_tag_regex(value) + + @classmethod + def from_file(cls, name="pyproject.toml"): + """ + Read Configuration from pyproject.toml (or similar). + Raises exceptions when file is not found or toml is + not installed or the file has invalid format or does + not contain the [tool.setuptools_scm] section. + """ + with open(name) as strm: + defn = __import__("toml").load(strm) + section = defn.get("tool", {})["setuptools_scm"] + return cls(**section) diff --git a/src/setuptools_scm/discover.py b/src/setuptools_scm/discover.py new file mode 100644 index 0000000..019f1c5 --- /dev/null +++ b/src/setuptools_scm/discover.py @@ -0,0 +1,13 @@ +import os +from pkg_resources import iter_entry_points +from .utils import trace + + +def iter_matching_entrypoints(path, entrypoint): + trace("looking for ep", entrypoint, path) + for ep in iter_entry_points(entrypoint): + if os.path.exists(os.path.join(path, ep.name)): + if os.path.isabs(ep.name): + trace("ignoring bad ep", ep) + trace("found ep", ep) + yield ep diff --git a/src/setuptools_scm/file_finder.py b/src/setuptools_scm/file_finder.py new file mode 100644 index 0000000..77ec146 --- /dev/null +++ b/src/setuptools_scm/file_finder.py @@ -0,0 +1,55 @@ +import os + + +def scm_find_files(path, scm_files, scm_dirs): + """ setuptools compatible file finder that follows symlinks + + - path: the root directory from which to search + - scm_files: set of scm controlled files and symlinks + (including symlinks to directories) + - scm_dirs: set of scm controlled directories + (including directories containing no scm controlled files) + + scm_files and scm_dirs must be absolute with symlinks resolved (realpath), + with normalized case (normcase) + + Spec here: http://setuptools.readthedocs.io/en/latest/setuptools.html#\ + adding-support-for-revision-control-systems + """ + realpath = os.path.normcase(os.path.realpath(path)) + seen = set() + res = [] + for dirpath, dirnames, filenames in os.walk(realpath, followlinks=True): + # dirpath with symlinks resolved + realdirpath = os.path.normcase(os.path.realpath(dirpath)) + + def _link_not_in_scm(n): + fn = os.path.join(realdirpath, os.path.normcase(n)) + return os.path.islink(fn) and fn not in scm_files + + if realdirpath not in scm_dirs: + # directory not in scm, don't walk it's content + dirnames[:] = [] + continue + if os.path.islink(dirpath) and not os.path.relpath( + realdirpath, realpath + ).startswith(os.pardir): + # a symlink to a directory not outside path: + # we keep it in the result and don't walk its content + res.append(os.path.join(path, os.path.relpath(dirpath, path))) + dirnames[:] = [] + continue + if realdirpath in seen: + # symlink loop protection + dirnames[:] = [] + continue + dirnames[:] = [dn for dn in dirnames if not _link_not_in_scm(dn)] + for filename in filenames: + if _link_not_in_scm(filename): + continue + # dirpath + filename with symlinks preserved + fullfilename = os.path.join(dirpath, filename) + if os.path.normcase(os.path.realpath(fullfilename)) in scm_files: + res.append(os.path.join(path, os.path.relpath(fullfilename, realpath))) + seen.add(realdirpath) + return res diff --git a/src/setuptools_scm/file_finder_git.py b/src/setuptools_scm/file_finder_git.py new file mode 100644 index 0000000..9aa6245 --- /dev/null +++ b/src/setuptools_scm/file_finder_git.py @@ -0,0 +1,69 @@ +import os +import subprocess +import tarfile +import logging +from .file_finder import scm_find_files +from .utils import trace + +log = logging.getLogger(__name__) + + +def _git_toplevel(path): + try: + with open(os.devnull, "wb") as devnull: + out = subprocess.check_output( + ["git", "rev-parse", "--show-toplevel"], + cwd=(path or "."), + universal_newlines=True, + stderr=devnull, + ) + trace("find files toplevel", out) + return os.path.normcase(os.path.realpath(out.strip())) + except subprocess.CalledProcessError: + # git returned error, we are not in a git repo + return None + except OSError: + # git command not found, probably + return None + + +def _git_interpret_archive(fd, toplevel): + with tarfile.open(fileobj=fd, mode="r|*") as tf: + git_files = set() + git_dirs = {toplevel} + for member in tf.getmembers(): + name = os.path.normcase(member.name).replace("/", os.path.sep) + if member.type == tarfile.DIRTYPE: + git_dirs.add(name) + else: + git_files.add(name) + return git_files, git_dirs + + +def _git_ls_files_and_dirs(toplevel): + # use git archive instead of git ls-file to honor + # export-ignore git attribute + cmd = ["git", "archive", "--prefix", toplevel + os.path.sep, "HEAD"] + proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, cwd=toplevel) + try: + try: + return _git_interpret_archive(proc.stdout, toplevel) + finally: + # ensure we avoid resource warnings by cleaning up the process + proc.stdout.close() + proc.terminate() + except Exception: + if proc.wait() != 0: + log.exception("listing git files failed - pretending there aren't any") + return (), () + + +def git_find_files(path=""): + toplevel = _git_toplevel(path) + if not toplevel: + return [] + fullpath = os.path.abspath(os.path.normpath(path)) + if not fullpath.startswith(toplevel): + trace("toplevel mismatch", toplevel, fullpath) + git_files, git_dirs = _git_ls_files_and_dirs(toplevel) + return scm_find_files(path, git_files, git_dirs) diff --git a/src/setuptools_scm/file_finder_hg.py b/src/setuptools_scm/file_finder_hg.py new file mode 100644 index 0000000..2aa1e16 --- /dev/null +++ b/src/setuptools_scm/file_finder_hg.py @@ -0,0 +1,47 @@ +import os +import subprocess + +from .file_finder import scm_find_files + + +def _hg_toplevel(path): + try: + with open(os.devnull, "wb") as devnull: + out = subprocess.check_output( + ["hg", "root"], + cwd=(path or "."), + universal_newlines=True, + stderr=devnull, + ) + return os.path.normcase(os.path.realpath(out.strip())) + except subprocess.CalledProcessError: + # hg returned error, we are not in a mercurial repo + return None + except OSError: + # hg command not found, probably + return None + + +def _hg_ls_files_and_dirs(toplevel): + hg_files = set() + hg_dirs = {toplevel} + out = subprocess.check_output( + ["hg", "files"], cwd=toplevel, universal_newlines=True + ) + for name in out.splitlines(): + name = os.path.normcase(name).replace("/", os.path.sep) + fullname = os.path.join(toplevel, name) + hg_files.add(fullname) + dirname = os.path.dirname(fullname) + while len(dirname) > len(toplevel) and dirname not in hg_dirs: + hg_dirs.add(dirname) + dirname = os.path.dirname(dirname) + return hg_files, hg_dirs + + +def hg_find_files(path=""): + toplevel = _hg_toplevel(path) + if not toplevel: + return [] + hg_files, hg_dirs = _hg_ls_files_and_dirs(toplevel) + return scm_find_files(path, hg_files, hg_dirs) diff --git a/src/setuptools_scm/git.py b/src/setuptools_scm/git.py new file mode 100644 index 0000000..afefa34 --- /dev/null +++ b/src/setuptools_scm/git.py @@ -0,0 +1,153 @@ +from .config import Configuration +from .utils import do_ex, trace, has_command +from .version import meta + +from os.path import isfile, join +import warnings + + +try: + from os.path import samefile +except ImportError: + from .win_py31_compat import samefile + + +DEFAULT_DESCRIBE = "git describe --dirty --tags --long --match *.*" + + +class GitWorkdir(object): + """experimental, may change at any time""" + + def __init__(self, path): + self.path = path + + def do_ex(self, cmd): + return do_ex(cmd, cwd=self.path) + + @classmethod + def from_potential_worktree(cls, wd): + real_wd, _, ret = do_ex("git rev-parse --show-toplevel", wd) + if ret: + return + trace("real root", real_wd) + if not samefile(real_wd, wd): + return + + 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) + return + return branch + + def is_shallow(self): + return isfile(join(self.path, ".git/shallow")) + + def fetch_shallow(self): + self.do_ex("git fetch --unshallow") + + def node(self): + rev_node, _, ret = self.do_ex("git rev-parse --verify --quiet HEAD") + if not ret: + return rev_node[:7] + + def count_all_nodes(self): + revs, _, _ = self.do_ex("git rev-list HEAD") + return revs.count("\n") + 1 + + +def warn_on_shallow(wd): + """experimental, may change at any time""" + if wd.is_shallow(): + warnings.warn('"{}" is shallow and may cause errors'.format(wd.path)) + + +def fetch_on_shallow(wd): + """experimental, may change at any time""" + if wd.is_shallow(): + warnings.warn('"%s" 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( + "%r is shallow, please correct with " '"git fetch --unshallow"' % wd.path + ) + + +def parse( + root, describe_command=DEFAULT_DESCRIBE, 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) + + if not has_command("git"): + return + + wd = GitWorkdir.from_potential_worktree(config.absolute_root) + if wd is None: + return + if pre_parse: + pre_parse(wd) + + if config.git_describe_command: + describe_command = config.git_describe_command + + out, unused_err, ret = wd.do_ex(describe_command) + if ret: + # If 'git git_describe_command' failed, try to get the information otherwise. + rev_node = wd.node() + dirty = wd.is_dirty() + + if rev_node is None: + return meta("0.0", distance=0, dirty=dirty, config=config) + + return meta( + "0.0", + distance=wd.count_all_nodes(), + node="g" + rev_node, + dirty=dirty, + branch=wd.get_branch(), + config=config, + ) + else: + tag, number, node, dirty = _git_parse_describe(out) + + branch = wd.get_branch() + if number: + return meta( + tag, + config=config, + distance=number, + node=node, + dirty=dirty, + branch=branch, + ) + else: + return meta(tag, config=config, node=node, dirty=dirty, branch=branch) + + +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 diff --git a/src/setuptools_scm/hacks.py b/src/setuptools_scm/hacks.py new file mode 100644 index 0000000..349d26f --- /dev/null +++ b/src/setuptools_scm/hacks.py @@ -0,0 +1,37 @@ +import os +from .utils import data_from_mime, trace +from .version import tag_to_version, meta + + +def parse_pkginfo(root, config=None): + + pkginfo = os.path.join(root, "PKG-INFO") + trace("pkginfo", pkginfo) + data = data_from_mime(pkginfo) + version = data.get("Version") + if version != "UNKNOWN": + return meta(version, preformatted=True, config=config) + + +def parse_pip_egg_info(root, config=None): + pipdir = os.path.join(root, "pip-egg-info") + if not os.path.isdir(pipdir): + return + items = os.listdir(pipdir) + trace("pip-egg-info", pipdir, items) + if not items: + return + return parse_pkginfo(os.path.join(pipdir, items[0]), config=config) + + +def fallback_version(root, config=None): + if config.parentdir_prefix_version is not None: + _, parent_name = os.path.split(os.path.abspath(root)) + if parent_name.startswith(config.parentdir_prefix_version): + version = tag_to_version( + parent_name[len(config.parentdir_prefix_version) :], config + ) + if version is not None: + return meta(str(version), preformatted=True, config=config) + if config.fallback_version is not None: + return meta(config.fallback_version, preformatted=True, config=config) diff --git a/src/setuptools_scm/hg.py b/src/setuptools_scm/hg.py new file mode 100644 index 0000000..d699d45 --- /dev/null +++ b/src/setuptools_scm/hg.py @@ -0,0 +1,113 @@ +import os +from .config import Configuration +from .utils import do, trace, data_from_mime, has_command +from .version import meta, tags_to_versions + + +def _hg_tagdist_normalize_tagcommit(config, tag, dist, node, branch): + dirty = node.endswith("+") + node = "h" + node.strip("+") + + # Detect changes since the specified tag + revset = ( + "(branch(.)" # look for revisions in this branch only + " and tag({tag!r})::." # after the last tag + # ignore commits that only modify .hgtags and nothing else: + " and (merge() or file('re:^(?!\\.hgtags).*$'))" + " and not tag({tag!r}))" # ignore the tagged commit itself + ).format(tag=tag) + if tag != "0.0": + commits = do( + ["hg", "log", "-r", revset, "--template", "{node|short}"], + config.absolute_root, + ) + else: + commits = True + trace("normalize", locals()) + if commits or dirty: + return meta( + tag, distance=dist, node=node, dirty=dirty, branch=branch, config=config + ) + else: + return meta(tag, config=config) + + +def parse(root, config=None): + if not config: + config = Configuration(root=root) + + if not has_command("hg"): + return + identity_data = do("hg id -i -b -t", config.absolute_root).split() + if not identity_data: + return + node = identity_data.pop(0) + branch = identity_data.pop(0) + if "tip" in identity_data: + # tip is not a real tag + identity_data.remove("tip") + tags = tags_to_versions(identity_data) + dirty = node[-1] == "+" + if tags: + return meta(tags[0], dirty=dirty, branch=branch, config=config) + + if node.strip("+") == "0" * 12: + trace("initial node", config.absolute_root) + return meta("0.0", config=config, dirty=dirty, branch=branch) + + try: + tag = get_latest_normalizable_tag(config.absolute_root) + dist = get_graph_distance(config.absolute_root, tag) + if tag == "null": + tag = "0.0" + dist = int(dist) + 1 + return _hg_tagdist_normalize_tagcommit(config, tag, dist, node, branch) + except ValueError: + pass # unpacking failed, old hg + + +def get_latest_normalizable_tag(root): + # Gets all tags containing a '.' (see #229) from oldest to newest + cmd = [ + "hg", + "log", + "-r", + "ancestors(.) and tag('re:\\.')", + "--template", + "{tags}\n", + ] + outlines = do(cmd, root).split() + if not outlines: + return "null" + tag = outlines[-1].split()[-1] + return tag + + +def get_graph_distance(root, rev1, rev2="."): + cmd = ["hg", "log", "-q", "-r", "{}::{}".format(rev1, rev2)] + out = do(cmd, root) + return len(out.strip().splitlines()) - 1 + + +def archival_to_version(data, config=None): + trace("data", data) + 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) diff --git a/src/setuptools_scm/integration.py b/src/setuptools_scm/integration.py new file mode 100644 index 0000000..c623db7 --- /dev/null +++ b/src/setuptools_scm/integration.py @@ -0,0 +1,48 @@ +from pkg_resources import iter_entry_points + +from .version import _warn_if_setuptools_outdated +from .utils import do, trace_exception +from . import _get_version, Configuration + + +def version_keyword(dist, keyword, value): + _warn_if_setuptools_outdated() + if not value: + return + if value is True: + value = {} + if getattr(value, "__call__", None): + value = value() + config = Configuration(**value) + dist.metadata.version = _get_version(config) + + +def find_files(path=""): + for ep in iter_entry_points("setuptools_scm.files_command"): + command = ep.load() + if isinstance(command, str): + # this technique is deprecated + res = do(ep.load(), path or ".").splitlines() + else: + res = command(path) + if res: + return res + return [] + + +def _args_from_toml(name="pyproject.toml"): + # todo: more sensible config initialization + # move this elper back to config and unify it with the code from get_config + + with open(name) as strm: + defn = __import__("toml").load(strm) + return defn.get("tool", {})["setuptools_scm"] + + +def infer_version(dist): + + try: + config = Configuration.from_file() + except Exception: + return trace_exception() + dist.metadata.version = _get_version(config) diff --git a/src/setuptools_scm/utils.py b/src/setuptools_scm/utils.py new file mode 100644 index 0000000..c31007a --- /dev/null +++ b/src/setuptools_scm/utils.py @@ -0,0 +1,163 @@ +""" +utils +""" +from __future__ import print_function, unicode_literals +import inspect +import warnings +import sys +import shlex +import subprocess +import os +import io +import platform +import traceback +import datetime + + +DEBUG = bool(os.environ.get("SETUPTOOLS_SCM_DEBUG")) +IS_WINDOWS = platform.system() == "Windows" +PY2 = sys.version_info < (3,) +PY3 = sys.version_info > (3,) +string_types = (str,) if PY3 else (str, unicode) # noqa + + +def no_git_env(env): + # adapted from pre-commit + # Too many bugs dealing with environment variables and GIT: + # https://github.com/pre-commit/pre-commit/issues/300 + # In git 2.6.3 (maybe others), git exports GIT_WORK_TREE while running + # pre-commit hooks + # In git 1.9.1 (maybe others), git exports GIT_DIR and GIT_INDEX_FILE + # while running pre-commit hooks in submodules. + # GIT_DIR: Causes git clone to clone wrong thing + # GIT_INDEX_FILE: Causes 'error invalid object ...' during commit + for k, v in env.items(): + if k.startswith("GIT_"): + trace(k, v) + return { + k: v + for k, v in env.items() + if not k.startswith("GIT_") + or k in ("GIT_EXEC_PATH", "GIT_SSH", "GIT_SSH_COMMAND") + } + + +def trace(*k): + if DEBUG: + print(*k) + sys.stdout.flush() + + +def trace_exception(): + DEBUG and traceback.print_exc() + + +def ensure_stripped_str(str_or_bytes): + if isinstance(str_or_bytes, str): + return str_or_bytes.strip() + else: + return str_or_bytes.decode("utf-8", "surrogateescape").strip() + + +def _always_strings(env_dict): + """ + On Windows and Python 2, environment dictionaries must be strings + and not unicode. + """ + if IS_WINDOWS or PY2: + env_dict.update((key, str(value)) for (key, value) in env_dict.items()) + return env_dict + + +def _popen_pipes(cmd, cwd): + return subprocess.Popen( + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + cwd=str(cwd), + env=_always_strings( + dict( + no_git_env(os.environ), + # os.environ, + # try to disable i18n + LC_ALL="C", + LANGUAGE="", + HGPLAIN="1", + ) + ), + ) + + +def do_ex(cmd, cwd="."): + trace("cmd", repr(cmd)) + if os.name == "posix" and not isinstance(cmd, (list, tuple)): + cmd = shlex.split(cmd) + + p = _popen_pipes(cmd, cwd) + out, err = p.communicate() + if out: + trace("out", repr(out)) + if err: + trace("err", repr(err)) + if p.returncode: + trace("ret", p.returncode) + return ensure_stripped_str(out), ensure_stripped_str(err), p.returncode + + +def do(cmd, cwd="."): + out, err, ret = do_ex(cmd, cwd) + if ret: + print(err) + return out + + +def data_from_mime(path): + with io.open(path, encoding="utf-8") as fp: + content = fp.read() + trace("content", repr(content)) + # the complex conditions come from reading pseudo-mime-messages + data = dict(x.split(": ", 1) for x in content.splitlines() if ": " in x) + trace("data", data) + return data + + +class UTC(datetime.tzinfo): + _ZERO = datetime.timedelta(0) + + def utcoffset(self, dt): + return self._ZERO + + def tzname(self, dt): + return "UTC" + + def dst(self, dt): + return self._ZERO + + +utc = UTC() + + +def function_has_arg(fn, argname): + assert inspect.isfunction(fn) + + if PY2: + argspec = inspect.getargspec(fn).args + else: + + argspec = inspect.signature(fn).parameters + + return argname in argspec + + +def has_command(name): + try: + p = _popen_pipes([name, "help"], ".") + except OSError: + trace(*sys.exc_info()) + res = False + else: + p.communicate() + res = not p.returncode + if not res: + warnings.warn("%r was not found" % name) + return res diff --git a/src/setuptools_scm/version.py b/src/setuptools_scm/version.py new file mode 100644 index 0000000..a3c8b94 --- /dev/null +++ b/src/setuptools_scm/version.py @@ -0,0 +1,349 @@ +from __future__ import print_function +import datetime +import warnings +import re + +from .config import Configuration +from .utils import trace, string_types, utc + +from pkg_resources import iter_entry_points + +from pkg_resources import parse_version as pkg_parse_version + +SEMVER_MINOR = 2 +SEMVER_PATCH = 3 +SEMVER_LEN = 3 + + +def _parse_version_tag(tag, config): + tagstring = tag if not isinstance(tag, string_types) 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("tag '{}' parsed to {}".format(tag, result)) + return result + + +def _get_version_class(): + modern_version = pkg_parse_version("1.0") + if isinstance(modern_version, tuple): + return None + else: + return type(modern_version) + + +VERSION_CLASS = _get_version_class() + + +class SetuptoolsOutdatedWarning(Warning): + pass + + +# append so integrators can disable the warning +warnings.simplefilter("error", SetuptoolsOutdatedWarning, append=True) + + +def _warn_if_setuptools_outdated(): + if VERSION_CLASS is None: + warnings.warn("your setuptools is too old (<12)", SetuptoolsOutdatedWarning) + + +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=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("tag {!r} no version found".format(tag)) + 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"] + ) + ) + + if VERSION_CLASS is not None: + version = pkg_parse_version(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(object): + def __init__( + self, + tag_version, + distance=None, + node=None, + dirty=False, + preformatted=False, + branch=None, + config=None, + **kw + ): + if kw: + trace("unknown args", kw) + self.tag = tag_version + if dirty and distance is None: + distance = 0 + self.distance = distance + self.node = node + self.time = datetime.datetime.now(utc) + self._extra = kw + self.dirty = dirty + self.preformatted = preformatted + self.branch = branch + self.config = config + + @property + def extra(self): + warnings.warn( + "ScmVersion.extra is deprecated and will be removed in future", + category=DeprecationWarning, + stacklevel=2, + ) + 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, + **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): + if preformatted: + return tag + if VERSION_CLASS is None or not isinstance(tag, VERSION_CLASS): + tag = tag_to_version(tag, config) + return tag + + +def meta( + tag, + distance=None, + dirty=False, + node=None, + preformatted=False, + branch=None, + config=None, + **kw +): + if not config: + warnings.warn( + "meta invoked without explicit configuration," + " will use defaults where required." + ) + parsed_version = _parse_tag(tag, preformatted, config) + trace("version", tag, "->", parsed_version) + assert parsed_version is not None, "cant parse version %s" % tag + return ScmVersion( + parsed_version, distance, node, dirty, preformatted, branch, config, **kw + ) + + +def guess_next_version(tag_version): + version = _strip_local(str(tag_version)) + return _bump_dev(version) or _bump_regex(version) + + +def _strip_local(version_string): + public, sep, local = version_string.partition("+") + return public + + +def _bump_dev(version): + if ".dev" not in version: + return + + prefix, tail = version.rsplit(".dev", 1) + assert tail == "0", "own dev numbers are unsupported" + return prefix + + +def _bump_regex(version): + prefix, tail = re.match(r"(.*?)(\d+)$", version).groups() + return "%s%d" % (prefix, int(tail) + 1) + + +def guess_next_dev_version(version): + if version.exact: + return version.format_with("{tag}") + else: + return version.format_next_version(guess_next_version) + + +def guess_next_simple_semver(version, retain, increment=True): + parts = [int(i) for i in str(version).split(".")[:retain]] + 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: + # Does the branch version up to the minor part match the tag? If not it + # might be like, an issue number or something and not a version number, so + # we only want to use it if it matches. + tag_ver_up_to_minor = str(version.tag).split(".")[:SEMVER_MINOR] + branch_ver_up_to_minor = branch_ver["version"].split(".")[:SEMVER_MINOR] + 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 _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 format_version(version, **config): + trace("scm version", version) + trace("config", config) + if version.preformatted: + return version.tag + version_scheme = callable_or_entrypoint( + "setuptools_scm.version_scheme", config["version_scheme"] + ) + local_scheme = callable_or_entrypoint( + "setuptools_scm.local_scheme", config["local_scheme"] + ) + main_version = version_scheme(version) + trace("version", main_version) + local_version = local_scheme(version) + trace("local_version", local_version) + return version_scheme(version) + local_scheme(version) diff --git a/src/setuptools_scm/win_py31_compat.py b/src/setuptools_scm/win_py31_compat.py new file mode 100644 index 0000000..82a11eb --- /dev/null +++ b/src/setuptools_scm/win_py31_compat.py @@ -0,0 +1,214 @@ +""" +Backport of os.path.samefile for Python prior to 3.2 +on Windows from jaraco.windows 3.8. + +DON'T EDIT THIS FILE! + +Instead, file tickets and PR's with `jaraco.windows +`_ and request +a port to setuptools_scm. +""" + +import os +import nt +import posixpath +import ctypes.wintypes +import sys +import __builtin__ as builtins + + +## +# From jaraco.windows.error + +def format_system_message(errno): + """ + Call FormatMessage with a system error number to retrieve + the descriptive error message. + """ + # first some flags used by FormatMessageW + ALLOCATE_BUFFER = 0x100 + FROM_SYSTEM = 0x1000 + + # Let FormatMessageW allocate the buffer (we'll free it below) + # Also, let it know we want a system error message. + flags = ALLOCATE_BUFFER | FROM_SYSTEM + source = None + message_id = errno + language_id = 0 + result_buffer = ctypes.wintypes.LPWSTR() + buffer_size = 0 + arguments = None + bytes = ctypes.windll.kernel32.FormatMessageW( + flags, + source, + message_id, + language_id, + ctypes.byref(result_buffer), + buffer_size, + arguments, + ) + # note the following will cause an infinite loop if GetLastError + # repeatedly returns an error that cannot be formatted, although + # this should not happen. + handle_nonzero_success(bytes) + message = result_buffer.value + ctypes.windll.kernel32.LocalFree(result_buffer) + return message + + +class WindowsError(builtins.WindowsError): + """ + More info about errors at + http://msdn.microsoft.com/en-us/library/ms681381(VS.85).aspx + """ + + def __init__(self, value=None): + if value is None: + value = ctypes.windll.kernel32.GetLastError() + strerror = format_system_message(value) + if sys.version_info > (3, 3): + args = 0, strerror, None, value + else: + args = value, strerror + super(WindowsError, self).__init__(*args) + + @property + def message(self): + return self.strerror + + @property + def code(self): + return self.winerror + + def __str__(self): + return self.message + + def __repr__(self): + return '{self.__class__.__name__}({self.winerror})'.format(**vars()) + + +def handle_nonzero_success(result): + if result == 0: + raise WindowsError() + + +## +# From jaraco.windows.api.filesystem + +FILE_FLAG_OPEN_REPARSE_POINT = 0x00200000 +FILE_FLAG_BACKUP_SEMANTICS = 0x2000000 +OPEN_EXISTING = 3 +FILE_ATTRIBUTE_NORMAL = 0x80 +FILE_READ_ATTRIBUTES = 0x80 +INVALID_HANDLE_VALUE = ctypes.wintypes.HANDLE(-1).value + + +class BY_HANDLE_FILE_INFORMATION(ctypes.Structure): + _fields_ = [ + ('file_attributes', ctypes.wintypes.DWORD), + ('creation_time', ctypes.wintypes.FILETIME), + ('last_access_time', ctypes.wintypes.FILETIME), + ('last_write_time', ctypes.wintypes.FILETIME), + ('volume_serial_number', ctypes.wintypes.DWORD), + ('file_size_high', ctypes.wintypes.DWORD), + ('file_size_low', ctypes.wintypes.DWORD), + ('number_of_links', ctypes.wintypes.DWORD), + ('file_index_high', ctypes.wintypes.DWORD), + ('file_index_low', ctypes.wintypes.DWORD), + ] + + @property + def file_size(self): + return (self.file_size_high << 32) + self.file_size_low + + @property + def file_index(self): + return (self.file_index_high << 32) + self.file_index_low + + +class SECURITY_ATTRIBUTES(ctypes.Structure): + _fields_ = ( + ('length', ctypes.wintypes.DWORD), + ('p_security_descriptor', ctypes.wintypes.LPVOID), + ('inherit_handle', ctypes.wintypes.BOOLEAN), + ) + + +LPSECURITY_ATTRIBUTES = ctypes.POINTER(SECURITY_ATTRIBUTES) + + +CreateFile = ctypes.windll.kernel32.CreateFileW +CreateFile.argtypes = ( + ctypes.wintypes.LPWSTR, + ctypes.wintypes.DWORD, + ctypes.wintypes.DWORD, + LPSECURITY_ATTRIBUTES, + ctypes.wintypes.DWORD, + ctypes.wintypes.DWORD, + ctypes.wintypes.HANDLE, +) +CreateFile.restype = ctypes.wintypes.HANDLE + +GetFileInformationByHandle = ctypes.windll.kernel32.GetFileInformationByHandle +GetFileInformationByHandle.restype = ctypes.wintypes.BOOL +GetFileInformationByHandle.argtypes = ( + ctypes.wintypes.HANDLE, + ctypes.POINTER(BY_HANDLE_FILE_INFORMATION), +) + + +## +# From jaraco.windows.filesystem + +def compat_stat(path): + """ + Generate stat as found on Python 3.2 and later. + """ + stat = os.stat(path) + info = get_file_info(path) + # rewrite st_ino, st_dev, and st_nlink based on file info + return nt.stat_result( + (stat.st_mode,) + + (info.file_index, info.volume_serial_number, info.number_of_links) + + stat[4:] + ) + + +def samefile(f1, f2): + """ + Backport of samefile from Python 3.2 with support for Windows. + """ + return posixpath.samestat(compat_stat(f1), compat_stat(f2)) + + +def get_file_info(path): + # open the file the same way CPython does in posixmodule.c + desired_access = FILE_READ_ATTRIBUTES + share_mode = 0 + security_attributes = None + creation_disposition = OPEN_EXISTING + flags_and_attributes = ( + FILE_ATTRIBUTE_NORMAL | + FILE_FLAG_BACKUP_SEMANTICS | + FILE_FLAG_OPEN_REPARSE_POINT + ) + template_file = None + + handle = CreateFile( + path, + desired_access, + share_mode, + security_attributes, + creation_disposition, + flags_and_attributes, + template_file, + ) + + if handle == INVALID_HANDLE_VALUE: + raise WindowsError() + + info = BY_HANDLE_FILE_INFORMATION() + res = GetFileInformationByHandle(handle, info) + handle_nonzero_success(res) + + return info diff --git a/testing/check_self_install.py b/testing/check_self_install.py new file mode 100644 index 0000000..de3ac79 --- /dev/null +++ b/testing/check_self_install.py @@ -0,0 +1,5 @@ +import pkg_resources +import setuptools_scm + +dist = pkg_resources.get_distribution("setuptools_scm") +assert dist.version == setuptools_scm.get_version(), dist.version diff --git a/testing/conftest.py b/testing/conftest.py new file mode 100644 index 0000000..5cb65f6 --- /dev/null +++ b/testing/conftest.py @@ -0,0 +1,96 @@ +import os +import itertools +import pytest +import six + +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("{} version {} from {!r}".format(pkg, version, path)) + return res + + +class Wd(object): + commit_command = None + add_command = None + + def __repr__(self): + return "".format(cwd=self.cwd) + + 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, six.text_type): + filename.write_text(value) + else: + filename.write_bytes(value) + return filename + + def _reason(self, given_reason): + if given_reason is None: + return "number-{c}".format(c=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.yield_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) diff --git a/testing/play_out_381.bash b/testing/play_out_381.bash new file mode 100755 index 0000000..be9d23c --- /dev/null +++ b/testing/play_out_381.bash @@ -0,0 +1,23 @@ +#!/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" < ../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"}) + + +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", + } + ) diff --git a/testing/test_functions.py b/testing/test_functions.py new file mode 100644 index 0000000..808a1d1 --- /dev/null +++ b/testing/test_functions.py @@ -0,0 +1,106 @@ +import pytest +import sys +import pkg_resources +from setuptools_scm import dump_version, get_version, PRETEND_KEY +from setuptools_scm.version import ( + guess_next_version, + meta, + format_version, + tag_to_version, +) + +from setuptools_scm.config import Configuration +from setuptools_scm.utils import has_command + +PY3 = sys.version_info > (2,) + + +class MockTime(object): + def __format__(self, *k): + return "time" + + +@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+dtime"), + ("dirty", "guess-next-dev no-local-version", "1.2.dev0"), + ("distance", "guess-next-dev node-and-date", "1.2.dev3"), + ("distancedirty", "guess-next-dev node-and-date", "1.2.dev3+dtime"), + ("distancedirty", "guess-next-dev no-local-version", "1.2.dev3"), + ("exact", "post-release node-and-date", "1.1"), + ("zerodistance", "post-release node-and-date", "1.1.post0"), + ("dirty", "post-release node-and-date", "1.1.post0+dtime"), + ("distance", "post-release node-and-date", "1.1.post3"), + ("distancedirty", "post-release node-and-date", "1.1.post3+dtime"), + ], +) +def test_format_version(version, monkeypatch, scheme, expected): + version = VERSIONS[version] + monkeypatch.setattr(version, "time", MockTime()) + vs, ls = scheme.split() + assert format_version(version, version_scheme=vs, local_scheme=ls) == expected + + +def test_dump_version_doesnt_bail_on_value_error(tmpdir): + write_to = "VERSION" + version = str(VERSIONS["exact"].tag) + with pytest.raises(ValueError) as exc_info: + dump_version(tmpdir.strpath, version, write_to) + assert str(exc_info.value).startswith("bad file format:") + + +@pytest.mark.parametrize( + "version", ["1.0", "1.2.3.dev1+ge871260", "1.2.3.dev15+ge871260.d20180625"] +) +def test_dump_version_works_with_pretend(version, tmpdir, monkeypatch): + monkeypatch.setenv(PRETEND_KEY, version) + get_version(write_to=str(tmpdir.join("VERSION.txt"))) + assert tmpdir.join("VERSION.txt").read() == version + + +def test_has_command(recwarn): + assert not has_command("yadayada_setuptools_aint_ne") + msg = recwarn.pop() + assert "yadayada" in str(msg.message) + + +@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, expected_version): + version = str(tag_to_version(tag)) + assert version == expected_version diff --git a/testing/test_git.py b/testing/test_git.py new file mode 100644 index 0000000..542a29f --- /dev/null +++ b/testing/test_git.py @@ -0,0 +1,273 @@ +import sys + +from setuptools_scm import integration +from setuptools_scm.utils import do, has_command +from setuptools_scm import git +import pytest +from datetime import datetime +from os.path import join as opj +from setuptools_scm.file_finder_git import git_find_files +import warnings + + +skip_if_win_27 = pytest.mark.skipif( + sys.platform == "win32" and sys.version_info[0] < 3, + reason="Not supported on Windows + Python 2.7", +) + + +with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + if not has_command("git"): + pytestmark = pytest.mark.skip(reason="git executable not found") + + +@pytest.fixture +def wd(wd, monkeypatch): + 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( + u"""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" + + +@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" + + 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" + + +@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) +def test_git_dirty_notag(wd): + wd.commit_testfile() + wd.write("test.txt", "test2") + wd("git add test.txt") + assert wd.version.startswith("0.1.dev1") + # the date on the tag is in UTC + today = datetime.utcnow().date() + # we are dirty, check for the tag + assert today.strftime(".d%Y%m%d") in wd.version + + +@pytest.mark.issue(193) +def test_git_worktree_support(wd, tmpdir): + wd.commit_testfile() + worktree = tmpdir.join("work_tree") + wd("git worktree add -b work-tree %s" % worktree) + + res = do([sys.executable, "-m", "setuptools_scm", "ls"], cwd=worktree) + assert str(worktree) in res + + +@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") + + +@skip_if_win_27 +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")] + + +@skip_if_win_27 +@pytest.mark.issue(228) +def test_git_archive_subdirectory(wd, monkeypatch): + wd("mkdir 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")] + + +@skip_if_win_27 +@pytest.mark.issue(251) +def test_git_archive_run_from_subdirectory(wd, monkeypatch): + wd("mkdir 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") + + +@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 diff --git a/testing/test_integration.py b/testing/test_integration.py new file mode 100644 index 0000000..68c3bfe --- /dev/null +++ b/testing/test_integration.py @@ -0,0 +1,42 @@ +import sys + +import pytest + +from setuptools_scm.utils import do + + +@pytest.fixture +def wd(wd): + 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}" + 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( + """[tool.setuptools_scm] +fallback_version = "12.34" +""" + ) + pkg.join("setup.py").write("__import__('setuptools').setup()") + res = do((sys.executable, "setup.py", "--version"), pkg) + assert res == "12.34" + + +def test_pyproject_support_with_git(tmpdir, monkeypatch, wd): + monkeypatch.delenv("SETUPTOOLS_SCM_DEBUG") + pkg = tmpdir.join("wd") + pkg.join("pyproject.toml").write("""[tool.setuptools_scm]""") + pkg.join("setup.py").write("__import__('setuptools').setup()") + res = do((sys.executable, "setup.py", "--version"), pkg) + assert res == "0.1.dev0" diff --git a/testing/test_main.py b/testing/test_main.py new file mode 100644 index 0000000..97ea05e --- /dev/null +++ b/testing/test_main.py @@ -0,0 +1,10 @@ +import os.path + + +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) diff --git a/testing/test_mercurial.py b/testing/test_mercurial.py new file mode 100644 index 0000000..29370c1 --- /dev/null +++ b/testing/test_mercurial.py @@ -0,0 +1,182 @@ +from setuptools_scm import format_version +from setuptools_scm.hg import archival_to_version, parse +from setuptools_scm import integration +from setuptools_scm.config import Configuration +from setuptools_scm.utils import has_command +import pytest +import warnings + + +with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + if not has_command("hg"): + pytestmark = pytest.mark.skip(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_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 taged 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 wont 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 superceded 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): + + 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") diff --git a/testing/test_regressions.py b/testing/test_regressions.py new file mode 100644 index 0000000..8bde373 --- /dev/null +++ b/testing/test_regressions.py @@ -0,0 +1,90 @@ +import sys +import subprocess + +from setuptools_scm import get_version +from setuptools_scm.git import parse +from setuptools_scm.utils import do_ex, do + +import pytest + + +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 diff --git a/testing/test_setuptools_support.py b/testing/test_setuptools_support.py new file mode 100644 index 0000000..ce3444e --- /dev/null +++ b/testing/test_setuptools_support.py @@ -0,0 +1,83 @@ +""" +integration tests that check setuptools version support +""" +import sys +import os +import subprocess +import pytest + +pytestmark = [ + pytest.mark.skipif( + "sys.version_info >= (3,6,0)", + reason="integration with old versions no longer needed on py3.6+", + ), + pytest.mark.xfail( + sys.platform == "win32", reason="path behaves unexpected on windows ci" + ), +] + + +@pytest.fixture(scope="session") +def get_setuptools_packagedir(request): + targets = request.config.cache.makedir("setuptools_installs") + + def makeinstall(version): + target = targets.ensure(version, dir=1) + subprocess.check_call( + [ + sys.executable, + "-m", + "pip", + "install", + "--no-binary", + "setuptools", + "setuptools==" + version, + "-t", + str(target), + ] + ) + return target + + return makeinstall + + +SCRIPT = """ +from __future__ import print_function +import sys +import setuptools +print(setuptools.__version__, 'expected', sys.argv[1]) +import setuptools_scm.version +from setuptools_scm.__main__ import main +main() +""" + + +def check(packagedir, expected_version, **env): + + old_pythonpath = os.environ.get("PYTHONPATH") + if old_pythonpath: + pythonpath = "{}:{}".format(old_pythonpath, packagedir) + else: + pythonpath = str(packagedir) + subprocess.check_call( + [sys.executable, "-c", SCRIPT, expected_version], + env=dict(os.environ, PYTHONPATH=pythonpath, **env), + ) + + +def test_old_setuptools_fails(get_setuptools_packagedir): + packagedir = get_setuptools_packagedir("0.9.8") + with pytest.raises(subprocess.CalledProcessError): + check(packagedir, "0.9.8") + + +def test_old_setuptools_allows_with_warnings(get_setuptools_packagedir): + + packagedir = get_setuptools_packagedir("0.9.8") + # filter using warning since in the early python startup + check(packagedir, "0.9.8", PYTHONWARNINGS="once::Warning") + + +def test_distlib_setuptools_works(get_setuptools_packagedir): + packagedir = get_setuptools_packagedir("12.0.1") + check(packagedir, "12.0.1") diff --git a/testing/test_version.py b/testing/test_version.py new file mode 100644 index 0000000..0b487b3 --- /dev/null +++ b/testing/test_version.py @@ -0,0 +1,111 @@ +import pytest +from setuptools_scm.config import Configuration +from setuptools_scm.version import ( + meta, + simplified_semver_version, + release_branch_semver_version, + 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 + + +@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="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 + + +@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): + config = Configuration() + if "+" in tag: + # pytest bug wrt cardinality + with pytest.warns(UserWarning): + result = meta(tag, config=config) + else: + result = meta(tag, config=config) + + assert result.tag.public == expected + + +@pytest.mark.issue("https://github.com/pypa/setuptools_scm/issues/286") +def test_tags_to_versions(): + config = Configuration() + versions = tags_to_versions(["1.0", "2.0", "3.0"], config=config) + assert isinstance(versions, list) # enable subscription diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..447c67e --- /dev/null +++ b/tox.ini @@ -0,0 +1,83 @@ +[tox] +envlist=py{27,34,35,36,37,38}-test,flake8,check_readme,py{27,37}-selfcheck + +[pytest] +testpaths=testing +filterwarnings=error +markers= + issue(id): reference to github issue + +[flake8] +max-complexity = 10 +max-line-length = 88 +ignore=E203,W503 +exclude= + .git, + .tox, + .env, + .venv, + .pytest_cache, + __pycache__, + ./setuptools_scm/win_py31_compat.py + +[testenv] +usedevelop=True +skip_install= + selfcheck: True + test: False +deps= + pytest + setuptools >= 42 +commands= + test: pytest [] + selfcheck: python setup.py --version +extras = + toml + +[testenv:flake8] +skip_install=True +deps= + flake8 + mccabe +commands = + flake8 src/setuptools_scm/ testing/ setup.py --exclude=setuptools_scm/win_py31_compat.py + +[testenv:check_readme] +skip_install=True +setenv = SETUPTOOLS_SCM_PRETEND_VERSION=2.0 +deps= + readme + check-manifest +commands= + python setup.py check -r + rst2html.py README.rst {envlogdir}/README.html --strict [] + check-manifest + +[testenv:upload] +deps= + wheel + twine +commands= + python setup.py clean --all rotate -k - -m .whl,.tar.gz,.zip + python setup.py -q egg_info + python setup.py -q sdist --formats zip bdist_wheel register + + + +[testenv:dist] +deps= wheel +whitelist_externals = rm +commands= + python setup.py -q clean --all + python setup.py -q rotate -k 0 -m .egg,.zip,.whl,.tar.gz + python setup.py -q egg_info + python setup.py -q sdist --formats zip,bztar bdist_wheel upload + +[testenv:devpi] +deps= + devpi-client +commands = + python setup.py -q egg_info + devpi upload --from-dir dist + +#XXX: envs for hg versions -- 2.34.1