diff options
author | Ronny Pfannschmidt <ronny.pfannschmidt@redhat.com> | 2020-12-11 22:52:22 +0100 |
---|---|---|
committer | Ronny Pfannschmidt <ronny.pfannschmidt@redhat.com> | 2020-12-11 22:52:22 +0100 |
commit | 7afad49aaaac94604682735cf32de226c41c28f6 (patch) | |
tree | c792cd3ce958b1cd99698920f380651c7a58cd46 | |
parent | 5e1a9694f1231f668988a491923b3ca7e7851718 (diff) | |
parent | bda1b523f77e15e62a788641c7e6dd41c24a1576 (diff) | |
download | setuptools-scm-7afad49aaaac94604682735cf32de226c41c28f6.tar.gz |
Merge branch 'master' into hbasria/master
39 files changed, 1350 insertions, 540 deletions
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..1756a18 --- /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@v2 + - name: set PY + run: echo "PY=$(python --version --version | sha256sum | cut -d' ' -f1)" >> $GITHUB_ENV + - 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..ec2390b --- /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@v2.0.1 + 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@v2.0.1 + 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@v2 + 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 }} @@ -20,6 +20,8 @@ __pycache__/ # Distribution / packaging .env/ env/ +.venv/ +venv/ build/ dist/ .eggs/ @@ -30,6 +32,7 @@ lib64/ # Installer logs pip-log.txt pip-delete-this-directory.txt +pip-wheel-metadata # Unit test / coverage reports htmlcov/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a80f778..0063a11 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,20 +1,22 @@ exclude: setuptools_scm/win_py31_compat.py repos: - repo: https://github.com/ambv/black - rev: 18.4a4 + rev: 20.8b1 hooks: - id: black args: [--safe, --quiet] - python_version: python3.6 - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v1.2.3 + rev: v3.3.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer - id: check-yaml - id: debug-statements +- repo: https://gitlab.com/pycqa/flake8 + rev: 3.8.4 + hooks: - id: flake8 - repo: https://github.com/asottile/pyupgrade - rev: v1.2.0 + rev: v2.7.4 hooks: - id: pyupgrade diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 75c3c0e..0000000 --- a/.travis.yml +++ /dev/null @@ -1,98 +0,0 @@ -language: python -sudo: false - -stages: -- linting -- test -- deploy - - -credentials: - - &pypi - provider: pypi - user: ronny - # use when testing, may require recreation of the user and its credentials - # server: https://test.pypi.org/legacy/ # Remove for deployment to official PyPi repo - password: - secure: QGJhDXmfFDKysMJJV/ONGaHHzG/aImhU3DdhEP63d657iQSn/Cb4EG/l9YmVnRzpJ94nSDXZB8YwptR7rid0bOtidb32lxN8n6UiWILCXWeAN2FE+tT9/0xIct4HUJZ8OttD1gft/Di722Gy+s9PzFwjwrV4efkxCzgjfYOjkMeq3aO6NoG3ur0iZXJh7ODwLp4sRFep2NpIEaXm2qMdnnXpck6bJ1q/NtvPx9CAZivd9HYa0evg5j1ENTz1mXXafhgF+0vRCBXA33xJuysO6CKtk+2mizL1QHfosOERiKl9+zPyZw+VvSchbCVwgxrMSiRcpGag+4SegyHrj1M/2YqfFzMF/yuFGcqXl2VkEqlnBQOVMNW3Kdcmnm+caNbddnv+M384WFz4nV8nWjcsD5l27+XlMWfuvskDIvZKtVCXmmbtqgwM4tqoYd6uxbnooRfwINTGx8sNzKP10xkaesB3ZBCEpecOKA1AXUAZ74RfYWWExv6eIuVGwyIJmOcD8M/17N8g58GxxO+88gx50EuhyNiRjYZDUipfVydfJwBwpD+p695NixUMITuksucQftjHsQp+laGWJlDIPvFwI85wDJUYAyrzn6L1W+smkm1bGomuliW2MJfxeSZAmSk4CE5VOpIWQTBmDLR3pxBhcaqzwdd4mAWvMi/fpM4yJJI= - on: - tags: yes -python: -- '2.7' -- '3.4' -- '3.5' -- '3.6' -- '3.7' -- '3.8-dev' -dist: xenial # needed for 3.7+ -env: -- TOXENV=py-test - -jobs: - include: - - stage: linting - name: check readme - python: '3.6' - env: TOXENV=check_readme - # - stage: test - # python: '3.7' - # dist: xenial - - stage: test - python: '2.7' - env: SELFINSTALL=1 - - stage: test - python: '3.6' - env: SELFINSTALL=1 - - - stage: linting - python: '3.6' - name: validate pre-commit - env: - install: - - pip install pre-commit - - pre-commit install-hooks - script: - - pre-commit run --all-files - - - &deploy - stage: deploy - name: "modern distributions" - python: '3.6' - install: - - pip install -U pip setuptools wheel - script: skip - deploy: - <<: *pypi - distributions: "sdist bdist_wheel" - - - &bdist_egg - <<: *deploy - name: "python eggs 2.7" - python: '2.7' - deploy: - <<: *pypi - distributions: "bdist_egg" - - <<: *bdist_egg - name: "python eggs 3.4" - python: '3.4' - - <<: *bdist_egg - name: "python eggs 3.5" - python: '3.5' - - <<: *bdist_egg - name: "python eggs 3.6" - python: '3.6' - - <<: *bdist_egg - name: "python eggs 3.7" - python: '3.7' - - <<: *bdist_egg - name: "python eggs 3.8" - python: '3.8-dev' - -cache: - directories: - - $HOME/.cache/pip - - $HOME/.cache/pre-commit - -install: pip install tox -script: -- python testing/runtests_travis.py diff --git a/CHANGELOG.rst b/CHANGELOG.rst index c6a1968..83666ae 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,5 +1,108 @@ +v5.0.0 +====== + + +Breaking changes: +* fix #339: strict errors on missing scms when parsing a scm dir to avoid false version lookups + +Bugfixes: + +* fix #352: add support for generally ignoring specific vcs roots +* fix #471: better error for version bump failing on complex but accepted tag +* fix #479: raise indicative error when tags carry non-parsable information +* Add `no-guess-dev` which does no next version guessing, just adds `.post1.devN` in + case there are new commits after the tag +* add python3.9 +* enhance documentation +* consider SOURCE_DATE_EPOCH for versioning +* add a version_tuple to write_to templates +* fix #321: add suppport for the ``SETUPTOOLS_SCM_PRETEND_VERSION_FOR_${DISTRIBUTION_NAME}`` env var to target the pretend key +* fix #142: clearly list supported scm +* fix #213: better error message for non-zero dev numbers in tags + + +v4.1.2 +======= + +* disallow git tags without dots by default again - #449 + +v4.1.1 +======= + +* drop jaraco.windows from pyproject.toml, allows for wheel builds on python2 + + +v4.1.0 +======= + +* include python 3.9 via the deadsnakes action +* return release_branch_semver scheme (it got dropped in a bad rebase) +* undo the devendoring of the samefile backport for python2.7 on windows +* re-enable the building of universal wheels +* fix handling of missing git/hg on python2.7 (python 3 exceptions where used) +* correct the tox flake8 invocation +* trigger builds on tags again + +v4.0.0 +====== + +* Add ``parentdir_prefix_version`` to support installs from GitHub release + tarballs. +* use Coordinated Universal Time (UTC) +* switch to github actions for ci +* fix documentation for ``tag_regex`` and add support for single digit versions +* document handling of enterprise distros with unsupported setuptools versions #312 +* switch to declarative metadata +* drop the internal copy of samefile and use a dependency on jaraco.windows on legacy systems +* select git tags based on the presence of numbers instead of dots +* enable getting a version form a parent folder prefix +* add release-branch-semver version scheme +* make global configuration available to version metadata +* drop official support for python 3.4 + +v3.5.0 +====== + +* add ``no-local-version`` local scheme and improve documentation for schemes + +v3.4.4 +====== + +* fix #403: also sort out resource warnings when dealing with git file finding + +v3.4.3 +====== + +* fix #399: ensure the git file finder terminates subprocess after reading archive + +v3.4.2 +====== + +* fix #395: correctly transfer tag regex in the Configuration constructor +* rollback --first-parent for git describe as it turns out to be a regression for some users + +v3.4.1 +====== + +* pull in #377 to fix #374: correctly set up the default version scheme for pyproject usage. + this bugfix got missed when ruushing the release. + +v3.4.0 +====== + +* fix #181 - add support for projects built under setuptools declarative config + by way of the setuptools.finalize_distribution_options hook in Setuptools 42. + * fix #305 - ensure the git file finder closes filedescriptors even when errors happen +* fix #381 - clean out env vars from the git hook system to ensure correct function from within + +* modernize docs wrt importlib.metadata + +*edited* + +* use --first-parent for git describe + v3.3.3 ====== diff --git a/MANIFEST.in b/MANIFEST.in index df70a56..b74fbe0 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,9 +1,9 @@ exclude *.nix -exclude appveyor.yml -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 @@ -1,21 +1,86 @@ 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. -It also handles file finders for the supported SCMs. +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 +``setuptools_scm`` support the following scm out of the box: + +* git +* mercurial + + + +.. image:: https://github.com/pypa/setuptools_scm/workflows/python%20tests+artifacts+release/badge.svg + :target: https://github.com/pypa/setuptools_scm/actions + +.. image:: https://tidelift.com/badges/package/pypi/setuptools-scm + :target: https://tidelift.com/subscription/pkg/pypi-setuptools-scm?utm_source=pypi-setuptools-scm&utm_medium=readme + + +``pyproject.toml`` usage +------------------------ + +The preferred way to configure ``setuptools_scm`` is to author +settings in a ``tool.setuptools_scm`` section of ``pyproject.toml``. + +This feature requires Setuptools 42 or later, released in Nov, 2019. +If your project needs to support build from sdist on older versions +of Setuptools, you will need to also implement the ``setup.py usage`` +for those legacy environments. + +First, ensure that ``setuptools_scm`` is present during the project's +built step by specifying it as one of the build requirements. + +.. code:: toml + + # pyproject.toml + [build-system] + requires = ["setuptools>=42", "wheel", "setuptools_scm[toml]>=3.4"] + +Note that the ``toml`` extra must be supplied. + +That will be sufficient to require ``setuptools_scm`` for projects +that support PEP 518 (`pip <https://pypi.org/project/pip>`_ and +`pep517 <https://pypi.org/project/pep517/>`_). Many tools, +especially those that invoke ``setup.py`` for any reason, may +continue to rely on ``setup_requires``. For maximum compatibility +with those uses, consider also including a ``setup_requires`` directive +(described below in ``setup.py usage`` and ``setup.cfg``). + +To enable version inference, add this section to your pyproject.toml: + +.. code:: toml + + # pyproject.toml + [tool.setuptools_scm] + +Including this section is comparable to supplying +``use_scm_version=True`` in ``setup.py``. Additionally, +include arbitrary keyword arguments in that section +to be supplied to ``get_version()``. For example: + +.. code:: toml + + # pyproject.toml + + [tool.setuptools_scm] + write_to = "pkg/version.py" -.. image:: https://tidelift.com/badges/github/pypa/setuptools_scm - :target: https://tidelift.com/subscription/pkg/pypi-setuptools_scm?utm_source=pypi-setuptools_scm&utm_medium=readme ``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: @@ -42,25 +107,16 @@ Arguments to ``get_version()`` (see below) may be passed as a dictionary to from setuptools import setup setup( ..., - use_scm_version = {"root": "..", "relative_to": __file__}, + use_scm_version = { + "root": "..", + "relative_to": __file__, + "local_scheme": "node-and-timestamp" + }, setup_requires=['setuptools_scm'], ..., ) -Once configured, you can access the version number in your package via -``pkg_resources`` (`PEP-0396 <https://www.python.org/dev/peps/pep-0396>`_). For -example: - -.. code:: python - - from pkg_resources import get_distribution, DistributionNotFound - try: - __version__ = get_distribution(__name__).version - except DistributionNotFound: - # package is not installed - pass - -You can also confirm the version number locally via ``setup.py``: +You can confirm the version number locally via ``setup.py``: .. code-block:: shell @@ -73,8 +129,8 @@ You can also confirm the version number locally via ``setup.py``: not defined in ``setup.cfg``. -``setup.cfg`` -------------- +``setup.cfg`` usage +------------------- If using `setuptools 30.3.0 <https://setuptools.readthedocs.io/en/latest/setuptools.html#configuring-setup-using-setup-cfg-files>`_ @@ -133,6 +189,43 @@ than the project's root, you can use: 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 (added in Python 3.8) +or the `importlib_metadata`_ backport: + +.. code:: python + + from importlib.metadata import version, PackageNotFoundError + + try: + __version__ = version("package-name") + except PackageNotFoundError: + # package is not installed + pass + +Alternatively, you can use ``pkg_resources`` which is included in +``setuptools``: + +.. code:: python + + from pkg_resources import get_distribution, DistributionNotFound + + try: + __version__ = get_distribution("package-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 ----------------- @@ -152,7 +245,7 @@ the working directory for good reasons and using the installed metadata prevents using needless volatile data there. Notable Plugins ----------------- +--------------- `setuptools_scm_git_archive <https://pypi.python.org/pypi/setuptools_scm_git_archive>`_ Provides partial support for obtaining versions from git archives that @@ -162,7 +255,7 @@ Notable Plugins Default versioning scheme --------------------------- +------------------------- In the standard configuration ``setuptools_scm`` takes a look at three things: @@ -177,13 +270,14 @@ no distance and clean: distance and clean: ``{next_version}.dev{distance}+{scm letter}{revision hash}`` no distance and not clean: - ``{tag}+dYYYMMMDD`` + ``{tag}+dYYYYMMDD`` distance and not clean: - ``{next_version}.dev{distance}+{scm letter}{revision hash}.dYYYMMMDD`` + ``{next_version}.dev{distance}+{scm letter}{revision hash}.dYYYYMMDD`` The next version is calculated by adding ``1`` to the last numeric component of the tag. + For Git projects, the version relies on `git describe <https://git-scm.com/docs/git-describe>`_, so you will see an additional ``g`` prepended to the ``{revision hash}``. @@ -204,7 +298,7 @@ accordingly. Builtin mechanisms for obtaining version numbers --------------------------------------------------- +------------------------------------------------ 1. the SCM itself (git/hg) 2. ``.hg_archival`` files (mercurial archives) @@ -220,7 +314,7 @@ File finders hook makes most of MANIFEST.in unnecessary ``setuptools_scm`` implements a `file_finders <https://setuptools.readthedocs.io/en/latest/setuptools.html#adding-support-for-revision-control-systems>`_ -entry point which returns all files tracked by by your SCM. This eliminates +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: @@ -277,16 +371,28 @@ The currently supported configuration keys are: supplying ``__file__``. :tag_regex: - A Python regex string to extract the version part from any SCM tag. - The regex needs to contain three named groups prefix, version and suffix, - where ``version`` captures the actual version information. + A Python regex string to extract the version part from any SCM tag. + The regex needs to contain either a single match group, or a group + named ``version``, that captures the actual version information. Defaults to the value of ``setuptools_scm.config.DEFAULT_TAG_REGEX`` (see `config.py <src/setuptools_scm/config.py>`_). +:parentdir_prefix_version: + If the normal methods for detecting the version (SCM version, + sdist metadata) fail, and the parent directory name starts with + ``parentdir_prefix_version``, then this prefix is stripped and the rest of + the parent directory name is matched with ``tag_regex`` to get a version + string. If this parameter is unset (the default), then this fallback is + not used. + + This is intended to cover GitHub's "release tarballs", which extract into + directories named ``projectname-tag/`` (in which case + ``parentdir_prefix_version`` can be set e.g. to ``projectname-``). + :fallback_version: A version string that will be used if no other method for detecting the - version worked (e.g., when using a tarball with no metadata). If this is + 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. @@ -337,11 +443,31 @@ Environment variables its used as the primary source for the version number in which case it will be a unparsed string + +:SETUPTOOLS_SCM_PRETEND_VERSION_FOR_${UPPERCASED_DIST_NAME}: + when defined and not empty, + its used as the primary source for the version number + in which case it will be a unparsed string + + it takes precedence over ``SETUPTOOLS_SCM_PRETEND_VERSION`` + + :SETUPTOOLS_SCM_DEBUG: when defined and not empty, a lot of debug information will be printed as part of ``setuptools_scm`` operating +:SOURCE_DATE_EPOCH: + when defined, used as the timestamp from which the + ``node-and-date`` and ``node-and-timestamp`` local parts are + derived, otherwise the current time is used + (https://reproducible-builds.org/docs/source-date-epoch/) + + +:SETUPTOOLS_SCM_IGNORE_VCS_ROOTS: + when defined, a ``os.pathsep`` separated list + of directory names to ignore for root finding + Extending setuptools_scm ------------------------ @@ -359,7 +485,7 @@ Adding a new SCM 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.version.ScmVersion`` instance + The return value MUST be a ``setuptools_scm.version.ScmVersion`` instance created by the function ``setuptools_scm.version:meta``. ``setuptools_scm.files_command`` @@ -374,18 +500,35 @@ Version number construction ``setuptools_scm.version_scheme`` Configures how the version number is constructed given a - ``setuptools.version.ScmVersion`` instance and should return a string + ``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) - :post-release: generates post release versions (adds :code:`postN`) + :guess-next-dev: Automatically guesses the next development version (default). + Guesses the upcoming release by incrementing the pre-release segment if present, + otherwise by incrementing the micro segment. Then appends :code:`.devN`. + In case the tag ends with ``.dev0`` the version is not bumped + and custom ``.devN`` versions will trigger a error. + :post-release: generates post release versions (adds :code:`.postN`) + :python-simplified-semver: Basic semantic versioning. Guesses the upcoming release + by incrementing the minor segment and setting the micro segment to zero if the + current branch contains the string ``'feature'``, otherwise by incrementing the + micro version. Then appends :code:`.devN`. Not compatible with pre-releases. + :release-branch-semver: Semantic versioning for projects with release branches. The + same as ``guess-next-dev`` (incrementing the pre-release or micro segment) if on + a release branch: a branch whose name (ignoring namespace) parses as a version + that matches the most recent tag up to the minor segment. Otherwise if on a + non-release branch, increments the minor segment and sets the micro segment to + zero, then appends :code:`.devN`. + :no-guess-dev: Does no next version guessing, just adds :code:`.post1.devN` ``setuptools_scm.local_scheme`` Configures how the local part of a version is rendered given a - ``setuptools.version.ScmVersion`` instance and should return a string + ``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: @@ -394,6 +537,8 @@ Version number construction :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`` @@ -426,7 +571,7 @@ The callable must return the configuration. 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, @@ -437,15 +582,35 @@ some environments require a test prior to install, $ 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`_. +`PSF Code of Conduct`_. -.. _PyPA Code of Conduct: https://www.pypa.io/en/latest/code-of-conduct/ +.. _PSF Code of Conduct: https://github.com/pypa/.github/blob/main/CODE_OF_CONDUCT.md Security Contact ================ diff --git a/appveyor.yml b/appveyor.yml deleted file mode 100644 index 995e4b4..0000000 --- a/appveyor.yml +++ /dev/null @@ -1,45 +0,0 @@ -environment:
- matrix:
- - PYTHON: "C:\\Python27"
- TOXENV: "py-test"
-
- - PYTHON: "C:\\Python27-x64"
- TOXENV: "py-test"
-
- - PYTHON: "C:\\Python34"
- TOXENV: "py-test"
-
- - PYTHON: "C:\\Python34-x64"
- TOXENV: "py-test"
-
-init:
- - "SET PATH=%PYTHON%;%PYTHON%\\Scripts;%PATH%"
-
- - ECHO "Updating Environment"
- - python -m pip install -U setuptools
- - python -m pip install -U pip
- - python -m pip install -U wheel
- - python -m pip install -U tox
-
-
-install:
- # Check that we have the expected version and architecture for Python
- - python -c "import sys, os;sys.stdout.write(str(sys.version) + os.linesep)"
- - "python -c \"import struct; print(struct.calcsize('P') * 8)\""
- - python -m pip list
-
-build: false # Not a C# project, build stuff at the test step instead.
-
-test_script:
- # Build the compiled extension and run the project tests
- - python -m tox
-
-after_test:
- # If tests are successful, create a whl package for the project.
- - "%CMD_IN_ENV% python setup.py bdist_wheel"
- - ps: "ls dist"
-
-artifacts:
- # Archive the generated wheel package in the ci.appveyor.com build report.
- - path: dist\*
-
diff --git a/default.nix b/default.nix deleted file mode 100644 index 5305bec..0000000 --- a/default.nix +++ /dev/null @@ -1,14 +0,0 @@ -{pkgs ? import <nixpkgs> {}}: -with pkgs.pythonPackages; -buildPythonPackage { - name = "setuptools_scm"; - src = ./.; - version = "git"; - buildInputs = [ - setuptools - pip - pytest - pkgs.git - pkgs.mercurial - ]; -} diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..8fe2f47 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["setuptools>=42", "wheel"] +build-backend = "setuptools.build_meta" @@ -1,9 +1,86 @@ -[bdist_wheel] -universal=1 - [metadata] # ensure that the LICENSE file is included in the built wheels 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 +long_description_content_type=text/x-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 -[devpi:upload] -formats=sdist,bdist_wheel +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 + no-guess-dev = setuptools_scm.version:no_guess_dev_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 @@ -24,6 +24,9 @@ def scm_config(): 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 @@ -46,73 +49,5 @@ def scm_config(): return dict(version=get_version(root=here, parse=parse, **config)) -with open("README.rst") as fp: - long_description = fp.read() - - -arguments = dict( - name="setuptools_scm", - url="https://github.com/pypa/setuptools_scm/", - zip_safe=True, - author="Ronny Pfannschmidt", - author_email="opensource@ronnypfannschmidt.de", - description=("the blessed package to manage your versions by scm tags"), - long_description=long_description, - license="MIT", - packages=["setuptools_scm"], - package_dir={"": "src"}, - entry_points=""" - [distutils.setup_keywords] - use_scm_version = setuptools_scm.integration:version_keyword - - [setuptools.file_finders] - setuptools_scm = setuptools_scm.integration:find_files - - [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 - - [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 - """, - classifiers=[ - "Development Status :: 4 - Beta", - "Intended Audience :: Developers", - "License :: OSI Approved :: MIT License", - "Programming Language :: Python", - "Programming Language :: Python :: 2", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 2.7", - "Programming Language :: Python :: 3.4", - "Programming Language :: Python :: 3.5", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", - "Topic :: Software Development :: Libraries", - "Topic :: Software Development :: Version Control", - "Topic :: System :: Software Distribution", - "Topic :: Utilities", - ], - python_requires=">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*", -) - if __name__ == "__main__": - arguments.update(scm_config()) - setuptools.setup(**arguments) + setuptools.setup(**scm_config()) diff --git a/src/setuptools_scm/__init__.py b/src/setuptools_scm/__init__.py index f3886ed..47b9e44 100644 --- a/src/setuptools_scm/__init__.py +++ b/src/setuptools_scm/__init__.py @@ -5,12 +5,18 @@ import os import warnings -from .config import Configuration -from .utils import function_has_arg, string_types +from .config import ( + Configuration, + DEFAULT_VERSION_SCHEME, + DEFAULT_LOCAL_SCHEME, + DEFAULT_TAG_REGEX, +) +from .utils import function_has_arg, string_types, trace from .version import format_version, meta from .discover import iter_matching_entrypoints PRETEND_KEY = "SETUPTOOLS_SCM_PRETEND_VERSION" +PRETEND_KEY_NAMED = PRETEND_KEY + "_FOR_{name}" TEMPLATES = { ".py": """\ @@ -18,6 +24,7 @@ TEMPLATES = { # file generated by setuptools_scm # don't change, don't track in version control version = {version!r} +version_tuple = {version_tuple!r} """, ".txt": "{version}", } @@ -75,12 +82,34 @@ def dump_version(root, version, write_to, template=None): os.path.splitext(target)[1], target ) ) + + # version_tuple: each field is converted to an int if possible or kept as string + fields = tuple(version.split(".")) + version_fields = [] + for field in fields: + try: + v = int(field) + except ValueError: + v = field + version_fields.append(v) + with open(target, "w") as fp: - fp.write(template.format(version=version)) + fp.write(template.format(version=version, version_tuple=tuple(version_fields))) def _do_parse(config): - pretended = os.environ.get(PRETEND_KEY) + + trace("dist name:", config.dist_name) + if config.dist_name is not None: + pretended = os.environ.get( + PRETEND_KEY_NAMED.format(name=config.dist_name.upper()) + ) + else: + pretended = None + + if pretended is None: + pretended = os.environ.get(PRETEND_KEY) + if pretended: # we use meta here since the pretended version # must adhere to the pep to begin with @@ -116,16 +145,18 @@ def _do_parse(config): def get_version( root=".", - version_scheme="guess-next-dev", - local_scheme="node-and-date", + version_scheme=DEFAULT_VERSION_SCHEME, + local_scheme=DEFAULT_LOCAL_SCHEME, write_to=None, write_to_template=None, relative_to=None, - tag_regex=None, + tag_regex=DEFAULT_TAG_REGEX, + parentdir_prefix_version=None, fallback_version=None, fallback_root=".", parse=None, git_describe_command=None, + dist_name=None, ): """ If supplied, relative_to should be a file from which root may @@ -134,30 +165,24 @@ def get_version( root of the repository by supplying ``__file__``. """ - config = Configuration() - config.root = root - config.fallback_root = fallback_root - config.version_scheme = version_scheme - config.local_scheme = local_scheme - config.write_to = write_to - config.write_to_template = write_to_template - config.relative_to = relative_to - config.tag_regex = tag_regex - config.fallback_version = fallback_version - config.parse = parse - config.git_describe_command = git_describe_command + 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=version_scheme, local_scheme=local_scheme + parsed_version, + version_scheme=config.version_scheme, + local_scheme=config.local_scheme, ) dump_version( - root=root, + root=config.root, version=version_string, - write_to=write_to, - template=write_to_template, + write_to=config.write_to, + template=config.write_to_template, ) return version_string diff --git a/src/setuptools_scm/config.py b/src/setuptools_scm/config.py index 38f79ae..f0d9243 100644 --- a/src/setuptools_scm/config.py +++ b/src/setuptools_scm/config.py @@ -6,8 +6,9 @@ import warnings from .utils import trace -DEFAULT_TAG_REGEX = r"^(?:[\w-]+-)?(?P<version>[vV]?\d+(?:\.\d+){0,2}[^\+]+)(?:\+.*)?$" -DEFAULT_VERSION_SCHEME = "version_scheme" +DEFAULT_TAG_REGEX = r"^(?:[\w-]+-)?(?P<version>[vV]?\d+(?:\.\d+){0,2}[^\+]*)(?:\+.*)?$" +DEFAULT_VERSION_SCHEME = "guess-next-dev" +DEFAULT_LOCAL_SCHEME = "node-and-date" def _check_tag_regex(value): @@ -39,32 +40,38 @@ def _check_absolute_root(root, relative_to): class Configuration(object): """ Global configuration model """ - _root = None - version_scheme = None - local_scheme = None - write_to = None - write_to_template = None - fallback_version = None - _relative_to = None - parse = None - _tag_regex = None - _absolute_root = None - - def __init__(self, relative_to=None, root="."): + 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, + dist_name=None, + ): # TODO: self._relative_to = relative_to self._root = "." self.root = root - self.version_scheme = DEFAULT_VERSION_SCHEME - self.local_scheme = "node-and-date" - self.write_to = "" - self.write_to_template = None - self.fallback_version = None - self.fallback_root = "." - self.parse = None - self.tag_regex = DEFAULT_TAG_REGEX - self.git_describe_command = None + self.version_scheme = version_scheme + self.local_scheme = local_scheme + self.write_to = write_to + self.write_to_template = write_to_template + self.parentdir_prefix_version = parentdir_prefix_version + self.fallback_version = fallback_version + self.fallback_root = fallback_root + self.parse = parse + self.tag_regex = tag_regex + self.git_describe_command = git_describe_command + self.dist_name = dist_name @property def fallback_root(self): @@ -105,3 +112,16 @@ class Configuration(object): @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/file_finder.py b/src/setuptools_scm/file_finder.py index 18712bf..5b85117 100644 --- a/src/setuptools_scm/file_finder.py +++ b/src/setuptools_scm/file_finder.py @@ -1,4 +1,5 @@ import os +from .utils import trace def scm_find_files(path, scm_files, scm_dirs): @@ -31,10 +32,9 @@ def scm_find_files(path, scm_files, 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) - ): + 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))) @@ -51,6 +51,19 @@ def scm_find_files(path, scm_files, scm_dirs): # 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, path))) + res.append(os.path.join(path, os.path.relpath(fullfilename, realpath))) seen.add(realdirpath) return res + + +def is_toplevel_acceptable(toplevel): + "" + if toplevel is None: + return False + + ignored = os.environ.get("SETUPTOOLS_SCM_IGNORE_VCS_ROOTS", "").split(os.pathsep) + ignored = [os.path.normcase(p) for p in ignored] + + trace(toplevel, ignored) + + return toplevel not in ignored diff --git a/src/setuptools_scm/file_finder_git.py b/src/setuptools_scm/file_finder_git.py index 5cda162..1d3e69b 100644 --- a/src/setuptools_scm/file_finder_git.py +++ b/src/setuptools_scm/file_finder_git.py @@ -3,6 +3,7 @@ import subprocess import tarfile import logging from .file_finder import scm_find_files +from .file_finder import is_toplevel_acceptable from .utils import trace log = logging.getLogger(__name__) @@ -49,8 +50,9 @@ def _git_ls_files_and_dirs(toplevel): try: return _git_interpret_archive(proc.stdout, toplevel) finally: - # ensure we avoid ressource warnings by cleaning up the pocess - proc.wait() + # 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") @@ -59,7 +61,10 @@ def _git_ls_files_and_dirs(toplevel): def git_find_files(path=""): toplevel = _git_toplevel(path) - if not toplevel: + if not is_toplevel_acceptable(toplevel): return [] + fullpath = os.path.abspath(os.path.normpath(path)) + if not fullpath.startswith(toplevel): + trace("toplevel mismatch", toplevel, fullpath) git_files, git_dirs = _git_ls_files_and_dirs(toplevel) return scm_find_files(path, git_files, git_dirs) diff --git a/src/setuptools_scm/file_finder_hg.py b/src/setuptools_scm/file_finder_hg.py index 2aa1e16..816560d 100644 --- a/src/setuptools_scm/file_finder_hg.py +++ b/src/setuptools_scm/file_finder_hg.py @@ -2,6 +2,7 @@ import os import subprocess from .file_finder import scm_find_files +from .file_finder import is_toplevel_acceptable def _hg_toplevel(path): @@ -41,7 +42,7 @@ def _hg_ls_files_and_dirs(toplevel): def hg_find_files(path=""): toplevel = _hg_toplevel(path) - if not toplevel: + if not is_toplevel_acceptable(toplevel): return [] hg_files, hg_dirs = _hg_ls_files_and_dirs(toplevel) return scm_find_files(path, hg_files, hg_dirs) diff --git a/src/setuptools_scm/git.py b/src/setuptools_scm/git.py index d7a524d..a193f93 100644 --- a/src/setuptools_scm/git.py +++ b/src/setuptools_scm/git.py @@ -1,5 +1,5 @@ from .config import Configuration -from .utils import do_ex, trace, has_command +from .utils import do_ex, trace, require_command from .version import meta from os.path import isfile, join @@ -12,7 +12,7 @@ except ImportError: from .win_py31_compat import samefile -DEFAULT_DESCRIBE = "git describe --dirty --tags --long --match *.* --first-parent" +DEFAULT_DESCRIBE = "git describe --dirty --tags --long --match *[0-9]*" class GitWorkdir(object): @@ -65,7 +65,7 @@ class GitWorkdir(object): def warn_on_shallow(wd): """experimental, may change at any time""" if wd.is_shallow(): - warnings.warn('"%s" is shallow and may cause errors' % (wd.path,)) + warnings.warn('"{}" is shallow and may cause errors'.format(wd.path)) def fetch_on_shallow(wd): @@ -92,8 +92,7 @@ def parse( if not config: config = Configuration(root=root) - if not has_command("git"): - return + require_command("git") wd = GitWorkdir.from_potential_worktree(config.absolute_root) if wd is None: diff --git a/src/setuptools_scm/hacks.py b/src/setuptools_scm/hacks.py index 1ef0bb4..349d26f 100644 --- a/src/setuptools_scm/hacks.py +++ b/src/setuptools_scm/hacks.py @@ -1,6 +1,6 @@ import os from .utils import data_from_mime, trace -from .version import meta +from .version import tag_to_version, meta def parse_pkginfo(root, config=None): @@ -25,5 +25,13 @@ def parse_pip_egg_info(root, config=None): 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 index 8fedd68..2ac9141 100644 --- a/src/setuptools_scm/hg.py +++ b/src/setuptools_scm/hg.py @@ -1,6 +1,6 @@ import os from .config import Configuration -from .utils import do, trace, data_from_mime, has_command +from .utils import do, trace, data_from_mime, require_command from .version import meta, tags_to_versions @@ -15,9 +15,7 @@ def _hg_tagdist_normalize_tagcommit(config, tag, dist, node, branch): # 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 - ) + ).format(tag=tag) if tag != "0.0": commits = do( ["hg", "log", "-r", revset, "--template", "{node|short}"], @@ -38,8 +36,7 @@ def parse(root, config=None): if not config: config = Configuration(root=root) - if not has_command("hg"): - return + require_command("hg") identity_data = do("hg id -i -b -t", config.absolute_root).split() if not identity_data: return @@ -71,7 +68,12 @@ def parse(root, config=None): 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" + "hg", + "log", + "-r", + "ancestors(.) and tag('re:\\.')", + "--template", + "{tags}\n", ] outlines = do(cmd, root).split() if not outlines: @@ -81,7 +83,7 @@ def get_latest_normalizable_tag(root): def get_graph_distance(root, rev1, rev2="."): - cmd = ["hg", "log", "-q", "-r", "%s::%s" % (rev1, rev2)] + cmd = ["hg", "log", "-q", "-r", "{}::{}".format(rev1, rev2)] out = do(cmd, root) return len(out.strip().splitlines()) - 1 diff --git a/src/setuptools_scm/integration.py b/src/setuptools_scm/integration.py index e18b3e5..ffd4521 100644 --- a/src/setuptools_scm/integration.py +++ b/src/setuptools_scm/integration.py @@ -1,8 +1,8 @@ from pkg_resources import iter_entry_points from .version import _warn_if_setuptools_outdated -from .utils import do -from . import get_version +from .utils import do, trace_exception, trace +from . import _get_version, Configuration def version_keyword(dist, keyword, value): @@ -13,8 +13,13 @@ def version_keyword(dist, keyword, value): value = {} if getattr(value, "__call__", None): value = value() - - dist.metadata.version = get_version(**value) + assert ( + "dist_name" not in value + ), "dist_name may not be specified in the setup keyword " + trace("dist name", dist, dist.name) + dist_name = dist.name if dist.name != 0 else None + config = Configuration(dist_name=dist_name, **value) + dist.metadata.version = _get_version(config) def find_files(path=""): @@ -28,3 +33,21 @@ def find_files(path=""): if res: return res return [] + + +def _args_from_toml(name="pyproject.toml"): + # todo: more sensible config initialization + # move this helper 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 index 5b59005..413e98a 100644 --- a/src/setuptools_scm/utils.py +++ b/src/setuptools_scm/utils.py @@ -10,6 +10,7 @@ import subprocess import os import io import platform +import traceback DEBUG = bool(os.environ.get("SETUPTOOLS_SCM_DEBUG")) @@ -19,12 +20,37 @@ 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() @@ -43,7 +69,6 @@ def _always_strings(env_dict): def _popen_pipes(cmd, cwd): - return subprocess.Popen( cmd, stdout=subprocess.PIPE, @@ -51,7 +76,8 @@ def _popen_pipes(cmd, cwd): cwd=str(cwd), env=_always_strings( dict( - os.environ, + no_git_env(os.environ), + # os.environ, # try to disable i18n LC_ALL="C", LANGUAGE="", @@ -106,7 +132,7 @@ def function_has_arg(fn, argname): return argname in argspec -def has_command(name): +def has_command(name, warn=True): try: p = _popen_pipes([name, "help"], ".") except OSError: @@ -115,6 +141,11 @@ def has_command(name): else: p.communicate() res = not p.returncode - if not res: - warnings.warn("%r was not found" % name) + if not res and warn: + warnings.warn("%r was not found" % name, category=RuntimeWarning) return res + + +def require_command(name): + if not has_command(name, warn=False): + raise EnvironmentError("%r was not found" % name) diff --git a/src/setuptools_scm/version.py b/src/setuptools_scm/version.py index 4f46331..f97dca5 100644 --- a/src/setuptools_scm/version.py +++ b/src/setuptools_scm/version.py @@ -2,7 +2,8 @@ from __future__ import print_function import datetime import warnings import re -from itertools import chain, repeat, islice +import time +import os from .config import Configuration from .utils import trace, string_types @@ -16,11 +17,6 @@ SEMVER_PATCH = 3 SEMVER_LEN = 3 -def _pad(iterable, size, padding=None): - padded = chain(iterable, repeat(padding)) - return list(islice(padded, size)) - - def _parse_version_tag(tag, config): tagstring = tag if not isinstance(tag, string_types) else str(tag) match = config.tag_regex.match(tagstring) @@ -34,11 +30,11 @@ def _parse_version_tag(tag, config): result = { "version": match.group(key), - "prefix": match.group(0)[:match.start(key)], - "suffix": match.group(0)[match.end(key):], + "prefix": match.group(0)[: match.start(key)], + "suffix": match.group(0)[match.end(key) :], } - trace("tag '%s' parsed to %s" % (tag, result)) + trace("tag '{}' parsed to {}".format(tag, result)) return result @@ -89,7 +85,7 @@ def tag_to_version(tag, config=None): tagdict = _parse_version_tag(tag, config) if not isinstance(tagdict, dict) or not tagdict.get("version", None): - warnings.warn("tag %r no version found" % (tag,)) + warnings.warn("tag {!r} no version found".format(tag)) return None version = tagdict["version"] @@ -97,7 +93,9 @@ def tag_to_version(tag, config=None): if tagdict.get("suffix", ""): warnings.warn( - "tag %r will be stripped of its suffix '%s'" % (tag, tagdict["suffix"]) + "tag {!r} will be stripped of its suffix '{}'".format( + tag, tagdict["suffix"] + ) ) if VERSION_CLASS is not None: @@ -122,7 +120,6 @@ def tags_to_versions(tags, config=None): class ScmVersion(object): - def __init__( self, tag_version, @@ -131,6 +128,7 @@ class ScmVersion(object): dirty=False, preformatted=False, branch=None, + config=None, **kw ): if kw: @@ -140,11 +138,14 @@ class ScmVersion(object): distance = 0 self.distance = distance self.node = node - self.time = datetime.datetime.now() + self.time = datetime.datetime.utcfromtimestamp( + int(os.environ.get("SOURCE_DATE_EPOCH", time.time())) + ) self._extra = kw self.dirty = dirty self.preformatted = preformatted self.branch = branch + self.config = config @property def extra(self): @@ -192,7 +193,14 @@ def _parse_tag(tag, preformatted, config): def meta( - tag, distance=None, dirty=False, node=None, preformatted=False, config=None, **kw + tag, + distance=None, + dirty=False, + node=None, + preformatted=False, + branch=None, + config=None, + **kw ): if not config: warnings.warn( @@ -201,8 +209,10 @@ def meta( ) 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, **kw) + assert parsed_version is not None, "Can't parse version %s" % tag + return ScmVersion( + parsed_version, distance, node, dirty, preformatted, branch, config, **kw + ) def guess_next_version(tag_version): @@ -220,13 +230,26 @@ def _bump_dev(version): return prefix, tail = version.rsplit(".dev", 1) - assert tail == "0", "own dev numbers are unsupported" + if tail != "0": + raise ValueError( + "choosing custom numbers for the `.devX` distance " + "is not supported.\n " + "The {version} can't be bumped\n" + "Please drop the tag or create a new supported one".format(version=version) + ) return prefix def _bump_regex(version): - prefix, tail = re.match(r"(.*?)(\d+)$", version).groups() - return "%s%d" % (prefix, int(tail) + 1) + match = re.match(r"(.*?)(\d+)$", version) + if match is None: + raise ValueError( + "{version} does not end with a number to bump, " + "please correct or use a custom version scheme".format(version=version) + ) + else: + prefix, tail = match.groups() + return "%s%d" % (prefix, int(tail) + 1) def guess_next_dev_version(version): @@ -237,12 +260,19 @@ def guess_next_dev_version(version): def guess_next_simple_semver(version, retain, increment=True): - parts = map(int, str(version).split(".")) - parts = _pad(parts, retain, 0) + try: + parts = [int(i) for i in str(version).split(".")[:retain]] + except ValueError: + raise ValueError( + "{version} can't be parsed as numeric version".format(version=version) + ) + while len(parts) < retain: + parts.append(0) if increment: parts[-1] += 1 - parts = _pad(parts, SEMVER_LEN, 0) - return ".".join(map(str, parts)) + while len(parts) < SEMVER_LEN: + parts.append(0) + return ".".join(str(i) for i in parts) def simplified_semver_version(version): @@ -259,6 +289,42 @@ def simplified_semver_version(version): ) +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 no_guess_dev_version(version): + if version.exact: + return version.format_with("{tag}") + else: + return version.format_with("{tag}.post1.dev{distance}") + + def _format_local_with_time(version, time_format): if version.exact or version.node is None: @@ -283,6 +349,10 @@ 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}") 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 index 0d34731..5f6cdd5 100644 --- a/testing/conftest.py +++ b/testing/conftest.py @@ -2,6 +2,8 @@ import os import itertools import pytest +# 2009-02-13T23:31:30+00:00 +os.environ["SOURCE_DATE_EPOCH"] = "1234567890" os.environ["SETUPTOOLS_SCM_DEBUG"] = "1" VERSION_PKGS = ["setuptools", "setuptools_scm"] @@ -21,6 +23,9 @@ class Wd(object): commit_command = None add_command = None + def __repr__(self): + return "<WD {cwd}>".format(cwd=self.cwd) + def __init__(self, cwd): self.cwd = cwd self.__counter = itertools.count() @@ -33,10 +38,13 @@ class Wd(object): return do(cmd, self.cwd) def write(self, name, value, **kw): - filename = self.cwd.join(name) + filename = self.cwd / name if kw: value = value.format(**kw) - filename.write(value) + if isinstance(value, bytes): + filename.write_bytes(value) + else: + filename.write_text(value) return filename def _reason(self, given_reason): @@ -83,5 +91,7 @@ def debug_mode(): @pytest.fixture -def wd(tmpdir): - return Wd(tmpdir.ensure("wd", dir=True)) +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" <<EOF +#!/usr/bin/env bash +virtualenv "$PWD/venv" +"$PWD/venv/bin/pip" install -e "$1" +"$PWD/venv/bin/pip" install --no-clean "$PWD/black" +EOF +chmod +x "$PWD/y/.git/modules/z/hooks/pre-commit" +cd y/z +git commit -m "test" diff --git a/testing/runtests_travis.py b/testing/runtests_travis.py deleted file mode 100644 index d2d5f9e..0000000 --- a/testing/runtests_travis.py +++ /dev/null @@ -1,21 +0,0 @@ - -from subprocess import call - -import os - -if os.environ.get("TOXENV"): - # normal tox run, lets jsut have tox do its job - import tox - - tox.cmdline() -elif os.environ.get("SELFINSTALL"): - # self install testing needs some clarity - # so its being executed without any other tools running - call("python setup.py sdist", shell=True) - call("easy_install dist/*", shell=True) - import pkg_resources - - dist = pkg_resources.get_distribution("setuptools_scm") - import setuptools_scm - - assert dist.version == setuptools_scm.get_version(), dist.version diff --git a/testing/test_basic_api.py b/testing/test_basic_api.py index f342e23..5111542 100644 --- a/testing/test_basic_api.py +++ b/testing/test_basic_api.py @@ -58,6 +58,18 @@ def test_root_parameter_pass_by(monkeypatch, tmpdir): setuptools_scm.get_version(root=tmpdir.strpath) +def test_parentdir_prefix(tmpdir, monkeypatch): + monkeypatch.delenv("SETUPTOOLS_SCM_DEBUG") + p = tmpdir.ensure("projectname-v12.34", dir=True) + p.join("setup.py").write( + """from setuptools import setup +setup(use_scm_version={"parentdir_prefix_version": "projectname-"}) +""" + ) + res = do((sys.executable, "setup.py", "--version"), p) + assert res == "12.34" + + def test_fallback(tmpdir, monkeypatch): monkeypatch.delenv("SETUPTOOLS_SCM_DEBUG") p = tmpdir.ensure("sub/package", dir=1) @@ -89,16 +101,19 @@ def test_dump_version(tmpdir): dump_version(sp, "1.0", "first.txt") assert tmpdir.join("first.txt").read() == "1.0" - dump_version(sp, "1.0", "first.py") + + dump_version(sp, "1.0.dev42", "first.py") content = tmpdir.join("first.py").read() - assert repr("1.0") in content + lines = content.splitlines() + assert "version = '1.0.dev42'" in lines + assert "version_tuple = (1, 0, 'dev42')" in lines + import ast ast.parse(content) def test_parse_plain_fails(recwarn): - def parse(root): return "tricked you" diff --git a/testing/test_config.py b/testing/test_config.py index eadbaf3..49f1d7a 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -1,5 +1,7 @@ -from setuptools_scm.config import Configuration +from __future__ import unicode_literals +from setuptools_scm.config import Configuration +import re import pytest @@ -9,6 +11,10 @@ import pytest ("apache-arrow-0.9.0", "0.9.0"), ("arrow-0.9.0", "0.9.0"), ("arrow-0.9.0-rc", "0.9.0-rc"), + ("arrow-1", "1"), + ("arrow-1+", "1"), + ("arrow-1+foo", "1"), + ("arrow-1.1+foo", "1.1"), ("v1.1", "v1.1"), ("V1.1", "V1.1"), ], @@ -16,5 +22,18 @@ import pytest def test_tag_regex(tag, expected_version): config = Configuration() match = config.tag_regex.match(tag) + assert match version = match.group("version") assert version == expected_version + + +def test_config_from_pyproject(tmpdir): + fn = tmpdir / "pyproject.toml" + fn.write_text("[tool.setuptools_scm]\n", encoding="utf-8") + assert Configuration.from_file(str(fn)) + + +def test_config_regex_init(): + tag_regex = re.compile(r"v(\d+)") + conf = Configuration(tag_regex=tag_regex) + assert conf.tag_regex is tag_regex diff --git a/testing/test_file_finder.py b/testing/test_file_finder.py index 0bc9a7f..55b9cea 100644 --- a/testing/test_file_finder.py +++ b/testing/test_file_finder.py @@ -7,25 +7,35 @@ from setuptools_scm.integration import find_files @pytest.fixture(params=["git", "hg"]) -def inwd(request, wd): +def inwd(request, wd, monkeypatch): if request.param == "git": - wd("git init") + if sys.platform == "win32" and sys.version_info[0] < 3: + pytest.skip("Long/short path names supported on Windows Python 2.7") + try: + wd("git init") + except OSError: + pytest.skip("git executable not found") wd("git config user.email test@example.com") wd('git config user.name "a test"') wd.add_command = "git add ." wd.commit_command = "git commit -m test-{reason}" elif request.param == "hg": - wd("hg init") + try: + wd("hg init") + except OSError: + pytest.skip("hg executable not found") wd.add_command = "hg add ." wd.commit_command = 'hg commit -m test-{reason} -u test -d "0 0"' - (wd.cwd / "file1").ensure(file=True) - adir = (wd.cwd / "adir").ensure(dir=True) - (adir / "filea").ensure(file=True) - bdir = (wd.cwd / "bdir").ensure(dir=True) - (bdir / "fileb").ensure(file=True) + (wd.cwd / "file1").touch() + adir = wd.cwd / "adir" + adir.mkdir() + (adir / "filea").touch() + bdir = wd.cwd / "bdir" + bdir.mkdir() + (bdir / "fileb").touch() wd.add_and_commit() - with wd.cwd.as_cwd(): - yield wd + monkeypatch.chdir(wd.cwd) + yield wd def _sep(paths): @@ -39,31 +49,30 @@ def test_basic(inwd): def test_whitespace(inwd): - (inwd.cwd / "adir" / "space file").ensure(file=True) + (inwd.cwd / "adir" / "space file").touch() inwd.add_and_commit() assert set(find_files("adir")) == _sep({"adir/space file", "adir/filea"}) def test_case(inwd): - (inwd.cwd / "CamelFile").ensure(file=True) - (inwd.cwd / "file2").ensure(file=True) + (inwd.cwd / "CamelFile").touch() + (inwd.cwd / "file2").touch() inwd.add_and_commit() - assert ( - set(find_files()) - == _sep({"CamelFile", "file2", "file1", "adir/filea", "bdir/fileb"}) + assert set(find_files()) == _sep( + {"CamelFile", "file2", "file1", "adir/filea", "bdir/fileb"} ) @pytest.mark.skipif(sys.platform == "win32", reason="symlinks to dir not supported") def test_symlink_dir(inwd): - (inwd.cwd / "adir" / "bdirlink").mksymlinkto("../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_source_not_in_scm(inwd): - (inwd.cwd / "adir" / "bdirlink").mksymlinkto("../bdir") + (inwd.cwd / "adir" / "bdirlink").symlink_to("../bdir") assert set(find_files("adir")) == _sep({"adir/filea"}) @@ -71,39 +80,39 @@ def test_symlink_dir_source_not_in_scm(inwd): sys.platform == "win32", reason="symlinks to files not supported on windows" ) def test_symlink_file(inwd): - (inwd.cwd / "adir" / "file1link").mksymlinkto("../file1") + (inwd.cwd / "adir" / "file1link").symlink_to("../file1") inwd.add_and_commit() - assert ( - set(find_files("adir")) == _sep({"adir/filea", "adir/file1link"}) # -> ../file1 - ) + assert set(find_files("adir")) == _sep( + {"adir/filea", "adir/file1link"} + ) # -> ../file1 @pytest.mark.skipif( sys.platform == "win32", reason="symlinks to files not supported on windows" ) def test_symlink_file_source_not_in_scm(inwd): - (inwd.cwd / "adir" / "file1link").mksymlinkto("../file1") + (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").mksymlinkto("../adir") + (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").mksymlinkto("../bdir") - (inwd.cwd / "adir" / "bdirlink").mksymlinkto("../bdir") + (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").mksymlinkto(os.path.join(__file__, "..")) + (inwd.cwd / "adir" / "outsidedirlink").symlink_to(os.path.join(__file__, "..")) inwd.add_and_commit() assert set(find_files("adir")) == _sep({"adir/filea"}) @@ -112,68 +121,68 @@ def test_symlink_dir_out_of_git(inwd): sys.platform == "win32", reason="symlinks to files not supported on windows" ) def test_symlink_file_out_of_git(inwd): - (inwd.cwd / "adir" / "outsidefilelink").mksymlinkto(__file__) + (inwd.cwd / "adir" / "outsidefilelink").symlink_to(__file__) inwd.add_and_commit() assert set(find_files("adir")) == _sep({"adir/filea"}) +@pytest.mark.parametrize("path_add", ["{cwd}", "{cwd}" + os.pathsep + "broken"]) +def test_ignore_root(inwd, monkeypatch, path_add): + monkeypatch.setenv("SETUPTOOLS_SCM_IGNORE_VCS_ROOTS", path_add.format(cwd=inwd.cwd)) + assert find_files() == [] + + def test_empty_root(inwd): subdir = inwd.cwd / "cdir" / "subdir" - subdir.ensure(dir=True) - (subdir / "filec").ensure(file=True) + 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.ensure(dir=True) - (subdir / "xfile").ensure(file=True) + subdir.mkdir(parents=True) + (subdir / "xfile").touch() inwd.add_and_commit() - assert ( - set(find_files("adir")) == _sep({"adir/filea", "adir/emptysubdir/subdir/xfile"}) + 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").ensure(dir=True) - (inwd.cwd / "data" / "datafile").ensure(file=True) - (inwd.cwd / "adir" / "datalink").mksymlinkto("../data") - (inwd.cwd / "adir" / "filealink").mksymlinkto("filea") + (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", - } - ) + 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").ensure(dir=True) - (inwd.cwd / "data" / "datafile").ensure(file=True) + (inwd.cwd / "data").mkdir() + (inwd.cwd / "data" / "datafile").touch() inwd.add_and_commit() - (inwd.cwd / "adir" / "datalink").mksymlinkto("../data") - (inwd.cwd / "adir" / "filealink").mksymlinkto("filea") - assert ( - set(find_files()) - == _sep( - { - "file1", - "adir/filea", - # adir/datalink and adir/afilelink not included - # because the symlink themselves are not in scm - "bdir/fileb", - "data/datafile", - } - ) + (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 index c3d78b6..ffc8081 100644 --- a/testing/test_functions.py +++ b/testing/test_functions.py @@ -15,12 +15,6 @@ 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", [ @@ -51,19 +45,21 @@ VERSIONS = { [ ("exact", "guess-next-dev node-and-date", "1.1"), ("zerodistance", "guess-next-dev node-and-date", "1.2.dev0"), - ("dirty", "guess-next-dev node-and-date", "1.2.dev0+dtime"), + ("zerodistance", "guess-next-dev no-local-version", "1.2.dev0"), + ("dirty", "guess-next-dev node-and-date", "1.2.dev0+d20090213"), + ("dirty", "guess-next-dev no-local-version", "1.2.dev0"), ("distance", "guess-next-dev node-and-date", "1.2.dev3"), - ("distancedirty", "guess-next-dev node-and-date", "1.2.dev3+dtime"), + ("distancedirty", "guess-next-dev node-and-date", "1.2.dev3+d20090213"), + ("distancedirty", "guess-next-dev no-local-version", "1.2.dev3"), ("exact", "post-release node-and-date", "1.1"), ("zerodistance", "post-release node-and-date", "1.1.post0"), - ("dirty", "post-release node-and-date", "1.1.post0+dtime"), + ("dirty", "post-release node-and-date", "1.1.post0+d20090213"), ("distance", "post-release node-and-date", "1.1.post3"), - ("distancedirty", "post-release node-and-date", "1.1.post3+dtime"), + ("distancedirty", "post-release node-and-date", "1.1.post3+d20090213"), ], ) -def test_format_version(version, monkeypatch, scheme, expected): +def test_format_version(version, 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 diff --git a/testing/test_git.py b/testing/test_git.py index 9307850..1b57fed 100644 --- a/testing/test_git.py +++ b/testing/test_git.py @@ -1,16 +1,28 @@ import sys from setuptools_scm import integration -from setuptools_scm.utils import do +from setuptools_scm.utils import do, has_command from setuptools_scm import git import pytest -from datetime import date +from datetime import datetime from os.path import join as opj from setuptools_scm.file_finder_git import git_find_files +skip_if_win_27 = pytest.mark.skipif( + sys.platform == "win32" and sys.version_info[0] < 3, + reason="Not supported on Windows + Python 2.7", +) + + +pytestmark = pytest.mark.skipif( + not has_command("git", warn=False), reason="git executable not found" +) + + @pytest.fixture -def wd(wd): +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"') @@ -33,9 +45,10 @@ def test_parse_describe_output(given, tag, number, node, dirty): def test_root_relative_to(tmpdir, wd, monkeypatch): monkeypatch.delenv("SETUPTOOLS_SCM_DEBUG") - p = wd.cwd.ensure("sub/package", dir=1) - p.join("setup.py").write( - """from setuptools import setup + 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__}) """ @@ -44,7 +57,14 @@ setup(use_scm_version={"root": "../..", assert res == "0.1.dev0" +def test_git_gone(wd, monkeypatch): + monkeypatch.setenv("PATH", str(wd.cwd / "not-existing")) + with pytest.raises(EnvironmentError, match="'git' was not found"): + git.parse(str(wd.cwd), git.DEFAULT_DESCRIBE) + + @pytest.mark.issue("https://github.com/pypa/setuptools_scm/issues/298") +@pytest.mark.issue(403) def test_file_finder_no_history(wd, caplog): file_list = git_find_files(str(wd.cwd)) assert file_list == [] @@ -105,14 +125,21 @@ def test_git_worktree(wd): @pytest.mark.issue(86) -def test_git_dirty_notag(wd): +@pytest.mark.parametrize("today", [False, True]) +def test_git_dirty_notag(today, wd, monkeypatch): + if today: + monkeypatch.delenv("SOURCE_DATE_EPOCH", raising=False) wd.commit_testfile() wd.write("test.txt", "test2") wd("git add test.txt") assert wd.version.startswith("0.1.dev1") - today = date.today() + if today: + # the date on the tag is in UTC + tag = datetime.utcnow().date().strftime(".d%Y%m%d") + else: + tag = ".d20090213" # we are dirty, check for the tag - assert today.strftime(".d%Y%m%d") in wd.version + assert tag in wd.version @pytest.mark.issue(193) @@ -157,8 +184,10 @@ def test_git_shallow_autocorrect(shallow_wd, recwarn): def test_find_files_stop_at_root_git(wd): wd.commit_testfile() - wd.cwd.ensure("project/setup.cfg") - assert integration.find_files(str(wd.cwd / "project")) == [] + project = wd.cwd / "project" + project.mkdir() + project.joinpath("setup.cfg").touch() + assert integration.find_files(str(project)) == [] @pytest.mark.issue(128) @@ -173,7 +202,8 @@ def test_alphanumeric_tags_match(wd): assert wd.version.startswith("0.1.dev1+g") -def test_git_archive_export_ignore(wd): +@skip_if_win_27 +def test_git_archive_export_ignore(wd, monkeypatch): wd.write("test1.txt", "test") wd.write("test2.txt", "test") wd.write( @@ -184,28 +214,30 @@ def test_git_archive_export_ignore(wd): ) wd("git add test1.txt test2.txt") wd.commit() - with wd.cwd.as_cwd(): - assert integration.find_files(".") == [opj(".", "test1.txt")] + 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): +def test_git_archive_subdirectory(wd, monkeypatch): wd("mkdir foobar") wd.write("foobar/test1.txt", "test") wd("git add foobar") wd.commit() - with wd.cwd.as_cwd(): - assert integration.find_files(".") == [opj(".", "foobar", "test1.txt")] + 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): +def test_git_archive_run_from_subdirectory(wd, monkeypatch): wd("mkdir foobar") wd.write("foobar/test1.txt", "test") wd("git add foobar") wd.commit() - with (wd.cwd / "foobar").as_cwd(): - assert integration.find_files(".") == [opj(".", "test1.txt")] + monkeypatch.chdir(wd.cwd / "foobar") + assert integration.find_files(".") == [opj(".", "test1.txt")] def test_git_feature_branch_increments_major(wd): @@ -228,6 +260,39 @@ def test_not_matching_tags(wd): 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" - ) + ).startswith("0.11.2") + + +@pytest.mark.issue("https://github.com/pypa/setuptools_scm/issues/411") +@pytest.mark.xfail(reason="https://github.com/pypa/setuptools_scm/issues/449") +def test_non_dotted_version(wd): + wd.commit_testfile() + wd("git tag apache-arrow-1") + wd.commit_testfile() + assert wd.get_version().startswith("2") + + +def test_non_dotted_version_with_updated_regex(wd): + wd.commit_testfile() + wd("git tag apache-arrow-1") + wd.commit_testfile() + assert wd.get_version(tag_regex=r"^apache-arrow-([\.0-9]+)$").startswith("2") + + +def test_non_dotted_tag_no_version_match(wd): + wd.commit_testfile() + wd("git tag apache-arrow-0.11.1") + wd.commit_testfile() + wd("git tag apache-arrow") + wd.commit_testfile() + assert wd.get_version().startswith("0.11.2.dev2") + + +@pytest.mark.issue("https://github.com/pypa/setuptools_scm/issues/381") +def test_gitdir(monkeypatch, wd): + """""" + wd.commit_testfile() + normal = wd.version + # git hooks set this and break subsequent setuptools_scm unless we clean + monkeypatch.setenv("GIT_DIR", __file__) + assert wd.version == normal diff --git a/testing/test_integration.py b/testing/test_integration.py new file mode 100644 index 0000000..446aac0 --- /dev/null +++ b/testing/test_integration.py @@ -0,0 +1,59 @@ +import sys + +import pytest + +from setuptools_scm.utils import do +from setuptools_scm import PRETEND_KEY, PRETEND_KEY_NAMED + + +@pytest.fixture +def wd(wd): + wd("git init") + wd("git config user.email test@example.com") + wd('git config user.name "a test"') + wd.add_command = "git add ." + wd.commit_command = "git commit -m test-{reason}" + return wd + + +def test_pyproject_support(tmpdir, monkeypatch): + pytest.importorskip("toml") + monkeypatch.delenv("SETUPTOOLS_SCM_DEBUG") + pkg = tmpdir.ensure("package", dir=42) + pkg.join("pyproject.toml").write( + """[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" + + +def test_pretend_version(tmpdir, monkeypatch, wd): + monkeypatch.setenv(PRETEND_KEY, "1.0.0") + + assert wd.get_version() == "1.0.0" + assert wd.get_version(dist_name="ignored") == "1.0.0" + + +def test_pretend_version_named(tmpdir, monkeypatch, wd): + monkeypatch.setenv(PRETEND_KEY_NAMED.format(name="test".upper()), "1.0.0") + monkeypatch.setenv(PRETEND_KEY_NAMED.format(name="test2".upper()), "2.0.0") + assert wd.get_version(dist_name="test") == "1.0.0" + assert wd.get_version(dist_name="test2") == "2.0.0" + + +def test_pretend_version_name_takes_precedence(tmpdir, monkeypatch, wd): + monkeypatch.setenv(PRETEND_KEY_NAMED.format(name="test".upper()), "1.0.0") + monkeypatch.setenv(PRETEND_KEY, "2.0.0") + assert wd.get_version(dist_name="test") == "1.0.0" diff --git a/testing/test_mercurial.py b/testing/test_mercurial.py index e25bfe1..265e207 100644 --- a/testing/test_mercurial.py +++ b/testing/test_mercurial.py @@ -2,9 +2,15 @@ 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 +pytestmark = pytest.mark.skipif( + not has_command("hg", warn=False), reason="hg executable not found" +) + + @pytest.fixture def wd(wd): wd("hg init") @@ -16,7 +22,9 @@ def wd(wd): archival_mapping = { "1.0": {"tag": "1.0"}, "1.1.dev3+h000000000000": { - "latesttag": "1.0", "latesttagdistance": "3", "node": "0" * 20 + "latesttag": "1.0", + "latesttagdistance": "3", + "node": "0" * 20, }, "0.0": {"node": "0" * 20}, "1.2.2": {"tag": "release-1.2.2"}, @@ -36,15 +44,23 @@ def test_archival_to_version(expected, data): ) -def test_find_files_stop_at_root_hg(wd): +def test_hg_gone(wd, monkeypatch): + monkeypatch.setenv("PATH", str(wd.cwd / "not-existing")) + with pytest.raises(EnvironmentError, match="'hg' was not found"): + parse(str(wd.cwd)) + + +def test_find_files_stop_at_root_hg(wd, monkeypatch): wd.commit_testfile() - wd.cwd.ensure("project/setup.cfg") + project = wd.cwd / "project" + project.mkdir() + project.joinpath("setup.cfg").touch() # setup.cfg has not been committed - assert integration.find_files(str(wd.cwd / "project")) == [] + assert integration.find_files(str(project)) == [] # issue 251 wd.add_and_commit() - with (wd.cwd / "project").as_cwd(): - assert integration.find_files() == ["setup.cfg"] + monkeypatch.chdir(project) + assert integration.find_files() == ["setup.cfg"] # XXX: better tests for tag prefixes @@ -79,7 +95,7 @@ def test_version_from_hg_id(wd): def test_version_from_archival(wd): # entrypoints are unordered, # cleaning the wd ensure this test wont break randomly - wd.cwd.join(".hg").remove() + 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" @@ -140,10 +156,9 @@ def test_version_bump_from_merge_commit(wd): @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.join(".hgtags").open("a") as tagfile: - tagfile.write("0 0\n") + """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 @@ -154,7 +169,7 @@ def test_version_bump_from_commit_including_hgtag_mods(wd): @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. + """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"') diff --git a/testing/test_regressions.py b/testing/test_regressions.py index f3b0fc6..8bde373 100644 --- a/testing/test_regressions.py +++ b/testing/test_regressions.py @@ -27,9 +27,13 @@ def test_pkginfo_noscmroot(tmpdir, monkeypatch): res = do((sys.executable, "setup.py", "--version"), p) assert res == "1.0" - do("git init", p.dirpath()) - res = do((sys.executable, "setup.py", "--version"), p) - assert res == "0.1.dev0" + 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): diff --git a/testing/test_version.py b/testing/test_version.py index a287a0d..ee13801 100644 --- a/testing/test_version.py +++ b/testing/test_version.py @@ -1,6 +1,13 @@ import pytest from setuptools_scm.config import Configuration -from setuptools_scm.version import meta, simplified_semver_version, tags_to_versions +from setuptools_scm.version import ( + meta, + simplified_semver_version, + release_branch_semver_version, + tags_to_versions, + no_guess_dev_version, + guess_next_version, +) c = Configuration() @@ -43,6 +50,92 @@ def test_next_semver(version, expected_next): assert computed == expected_next +def test_next_semver_bad_tag(): + + version = meta("1.0.0-foo", config=c) + with pytest.raises( + ValueError, match="1.0.0-foo can't be parsed as numeric version" + ): + simplified_semver_version(version) + + +@pytest.mark.parametrize( + "version, expected_next", + [ + pytest.param(meta("1.0.0", config=c), "1.0.0", id="exact"), + pytest.param( + meta("1.0.0", distance=2, branch="master", config=c), + "1.1.0.dev2", + id="development_branch", + ), + pytest.param( + meta("1.0.0rc1", distance=2, branch="master", config=c), + "1.1.0.dev2", + id="development_branch_release_candidate", + ), + pytest.param( + meta("1.0.0", distance=2, branch="maintenance/1.0.x", config=c), + "1.0.1.dev2", + id="release_branch_legacy_version", + ), + pytest.param( + meta("1.0.0", distance=2, branch="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( + "version, expected_next", + [ + pytest.param( + meta("1.0.0", distance=2, branch="default", config=c), + "1.0.0.post1.dev2", + id="dev_distance", + ), + pytest.param( + meta("1.0", distance=2, branch="default", config=c), + "1.0.post1.dev2", + id="dev_distance_short_tag", + ), + pytest.param( + meta("1.0.0", distance=None, branch="default", config=c), + "1.0.0", + id="no_dev_distance", + ), + ], +) +def test_no_guess_version(version, expected_next): + computed = no_guess_dev_version(version) + assert computed == expected_next + + +def test_bump_dev_version_zero(): + guess_next_version("1.0.dev0") + + +def test_bump_dev_version_nonzero_raises(): + with pytest.raises(ValueError) as excinfo: + guess_next_version("1.0.dev1") + + assert str(excinfo.value) == ( + "choosing custom numbers for the `.devX` distance " + "is not supported.\n " + "The 1.0.dev1 can't be bumped\n" + "Please drop the tag or create a new supported one" + ) + + @pytest.mark.parametrize( "tag, expected", [ @@ -52,20 +145,28 @@ def test_next_semver(version, expected_next): ], ) def test_tag_regex1(tag, expected): - config = Configuration() - config.tag_regex = r"^(?P<prefix>v)?(?P<version>[^\+]+)(?P<suffix>.*)?$" if "+" in tag: # pytest bug wrt cardinality with pytest.warns(UserWarning): - result = meta(tag, config=config) + result = meta(tag, config=c) else: - result = meta(tag, config=config) + result = meta(tag, config=c) assert 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) + versions = tags_to_versions(["1.0", "2.0", "3.0"], config=c) assert isinstance(versions, list) # enable subscription + + +@pytest.mark.issue("https://github.com/pypa/setuptools_scm/issues/471") +def test_version_bump_bad(): + with pytest.raises( + ValueError, + match=".*does not end with a number to bump, " + "please correct or use a custom version scheme", + ): + + guess_next_version(tag_version="2.0.0-alpha.5-PMC") @@ -1,7 +1,8 @@ [tox] -envlist=py{27,34,35,36,37,38}-test,flake8,check_readme,py{27,37}-selfcheck +envlist=py{27,34,35,36,37,38,39}-test,flake8,check_readme,check-dist,py{27,37}-selfcheck,docs [pytest] +testpaths=testing filterwarnings=error markers= issue(id): reference to github issue @@ -9,6 +10,7 @@ markers= [flake8] max-complexity = 10 max-line-length = 88 +ignore=E203,W503 exclude= .git, .tox, @@ -16,7 +18,7 @@ exclude= .venv, .pytest_cache, __pycache__, - ./setuptools_scm/win_py31_compat.py + ./src/setuptools_scm/win_py31_compat.py [testenv] usedevelop=True @@ -25,9 +27,12 @@ skip_install= test: False deps= pytest + setuptools >= 42 commands= - test: py.test [] + test: pytest [] selfcheck: python setup.py --version +extras = + toml [testenv:flake8] skip_install=True @@ -35,27 +40,28 @@ deps= flake8 mccabe commands = - flake8 setuptools_scm/ testing/ setup.py --exclude=setuptools_scm/win_py31_compat.py + flake8 src/setuptools_scm/ testing/ setup.py --exclude=src/setuptools_scm/win_py31_compat.py [testenv:check_readme] skip_install=True setenv = SETUPTOOLS_SCM_PRETEND_VERSION=2.0 deps= - readme check-manifest + docutils + pygments commands= - python setup.py check -r rst2html.py README.rst {envlogdir}/README.html --strict [] check-manifest -[testenv:upload] +[testenv:check_dist] deps= wheel twine commands= - python setup.py clean --all rotate -k - -m .whl,.tar.gz,.zip + python setup.py clean --all rotate -k 0 -m .whl,.tar.gz,.zip python setup.py -q egg_info - python setup.py -q sdist --formats zip bdist_wheel register + python setup.py -q sdist --formats zip bdist_wheel + twine check dist/* |