Imported Upstream version 8.0.4 sandbox/python3-setuptools_scm_8.0.4_20240206 upstream/8.0.4
authorTizenOpenSource <tizenopensrc@samsung.com>
Tue, 6 Feb 2024 05:21:33 +0000 (14:21 +0900)
committerTizenOpenSource <tizenopensrc@samsung.com>
Tue, 6 Feb 2024 05:21:33 +0000 (14:21 +0900)
93 files changed:
.gitattributes [new file with mode: 0644]
.github/dependabot.yml [new file with mode: 0644]
.github/workflows/pre-commit.yml [deleted file]
.github/workflows/python-tests.yml
.gitignore [deleted file]
CHANGELOG.md [new file with mode: 0644]
CHANGELOG.rst [deleted file]
MANIFEST.in
PKG-INFO
README.md [new file with mode: 0644]
README.rst [deleted file]
_own_version_helper.py [new file with mode: 0644]
docs/config.md [new file with mode: 0644]
docs/customizing.md [new file with mode: 0644]
docs/examples/version_scheme_code/pyproject.toml [new file with mode: 0644]
docs/examples/version_scheme_code/setup.py [new file with mode: 0644]
docs/extending.md [new file with mode: 0644]
docs/index.md [new file with mode: 0644]
docs/overrides.md [new file with mode: 0644]
docs/usage.md [new file with mode: 0644]
hatch.toml [new file with mode: 0644]
mkdocs.yml [new file with mode: 0644]
mypy.ini [new file with mode: 0644]
packaging/python3-setuptools_scm.manifest [new file with mode: 0644]
packaging/python3-setuptools_scm.spec [new file with mode: 0644]
packaging/setuptools_scm-8.0.4-py3-none-any.whl [new file with mode: 0644]
pyproject.toml
setup.cfg
setup.py [deleted file]
src/setuptools_scm.egg-info/PKG-INFO [deleted file]
src/setuptools_scm.egg-info/SOURCES.txt [deleted file]
src/setuptools_scm.egg-info/dependency_links.txt [deleted file]
src/setuptools_scm.egg-info/entry_points.txt [deleted file]
src/setuptools_scm.egg-info/requires.txt [deleted file]
src/setuptools_scm.egg-info/top_level.txt [deleted file]
src/setuptools_scm.egg-info/zip-safe [deleted file]
src/setuptools_scm/.git_archival.txt [new file with mode: 0644]
src/setuptools_scm/__init__.py
src/setuptools_scm/__main__.py
src/setuptools_scm/_cli.py [new file with mode: 0644]
src/setuptools_scm/_config.py [new file with mode: 0644]
src/setuptools_scm/_entrypoints.py [new file with mode: 0644]
src/setuptools_scm/_file_finders/__init__.py [new file with mode: 0644]
src/setuptools_scm/_file_finders/git.py [new file with mode: 0644]
src/setuptools_scm/_file_finders/hg.py [new file with mode: 0644]
src/setuptools_scm/_get_version_impl.py [new file with mode: 0644]
src/setuptools_scm/_integration/__init__.py [new file with mode: 0644]
src/setuptools_scm/_integration/dump_version.py [new file with mode: 0644]
src/setuptools_scm/_integration/pyproject_reading.py [new file with mode: 0644]
src/setuptools_scm/_integration/setuptools.py [new file with mode: 0644]
src/setuptools_scm/_integration/toml.py [new file with mode: 0644]
src/setuptools_scm/_log.py [new file with mode: 0644]
src/setuptools_scm/_modify_version.py [new file with mode: 0644]
src/setuptools_scm/_overrides.py [new file with mode: 0644]
src/setuptools_scm/_run_cmd.py [new file with mode: 0644]
src/setuptools_scm/_types.py [new file with mode: 0644]
src/setuptools_scm/_version_cls.py [new file with mode: 0644]
src/setuptools_scm/config.py [deleted file]
src/setuptools_scm/discover.py
src/setuptools_scm/fallbacks.py [new file with mode: 0644]
src/setuptools_scm/file_finder.py [deleted file]
src/setuptools_scm/file_finder_git.py [deleted file]
src/setuptools_scm/file_finder_hg.py [deleted file]
src/setuptools_scm/git.py
src/setuptools_scm/hacks.py [deleted file]
src/setuptools_scm/hg.py
src/setuptools_scm/hg_git.py [new file with mode: 0644]
src/setuptools_scm/integration.py
src/setuptools_scm/scm_workdir.py [new file with mode: 0644]
src/setuptools_scm/utils.py [deleted file]
src/setuptools_scm/version.py
src/setuptools_scm/win_py31_compat.py [deleted file]
testing/Dockerfile.busted-buster [new file with mode: 0644]
testing/Dockerfile.rawhide-git [new file with mode: 0644]
testing/__init__.py [new file with mode: 0644]
testing/check_self_install.py [deleted file]
testing/conftest.py
testing/test_basic_api.py
testing/test_cli.py [new file with mode: 0644]
testing/test_config.py
testing/test_file_finder.py
testing/test_functions.py
testing/test_git.py
testing/test_hg_git.py [new file with mode: 0644]
testing/test_integration.py
testing/test_internal_log_level.py [new file with mode: 0644]
testing/test_main.py
testing/test_mercurial.py
testing/test_regressions.py
testing/test_setuptools_support.py [deleted file]
testing/test_version.py
testing/wd_wrapper.py [new file with mode: 0644]
tox.ini

diff --git a/.gitattributes b/.gitattributes
new file mode 100644 (file)
index 0000000..00a7b00
--- /dev/null
@@ -0,0 +1 @@
+.git_archival.txt  export-subst
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644 (file)
index 0000000..cbd920f
--- /dev/null
@@ -0,0 +1,10 @@
+version: 2
+updates:
+  - package-ecosystem: "github-actions"
+    directory: "/"
+    schedule:
+      interval: "weekly"
+  - package-ecosystem: "pip"
+    directory: "/"
+    schedule:
+      interval: "weekly"
diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml
deleted file mode 100644 (file)
index c154a12..0000000
+++ /dev/null
@@ -1,20 +0,0 @@
-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
index ca8df3f990b7a025401809d562fa1e640027b03a..d0b92c02c9c19f7f82637cb876f6dc1f4c0ea3a6 100644 (file)
@@ -4,152 +4,123 @@ on:
   pull_request:
   push:
     branches:
-    - master
+    - "*"
     tags:
     - "v*"
-  release:
+
+concurrency:
+  group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }}
+  cancel-in-progress: true
+
+env:
+  FORCE_COLOR: 1
 
 jobs:
+
+  package:
+    name: Build & inspect our package.
+    runs-on: ubuntu-latest
+
+    steps:
+      - uses: actions/checkout@v4
+        with:
+          fetch-depth: 0
+
+      - uses: hynek/build-and-inspect-python-package@v1
+
   test:
+    needs: [package]
     runs-on: ${{ matrix.os }}
     strategy:
       fail-fast: false
       matrix:
-        python_version: [ '2.7', '3.5', '3.6', '3.7', '3.8', 'pypy2', 'pypy3' ]
+        python_version: [ '3.8', '3.9', '3.10', '3.11', '3.12.0-beta - 3.12', 'pypy-3.8' ]
         os: [windows-latest, ubuntu-latest] #, macos-latest]
-        exclude:
-        - os: windows-latest
-          python_version: "pypy2"
         include:
-        - os: ubuntu-latest
-          python_version: '3.9-dev'
+        - os: windows-latest
+          python_version: 'msys2'
 
     name: ${{ matrix.os }} - Python ${{ matrix.python_version }}
     steps:
-      - uses: actions/checkout@v1
+      - uses: actions/checkout@v4
       - name: Setup python
-        uses: actions/setup-python@v2
-        if: matrix.python_version != '3.9-dev'
+        uses: actions/setup-python@v4
+        if: matrix.python_version != 'msys2'
         with:
           python-version: ${{ matrix.python_version }}
           architecture: x64
-      - name: Set up Python ${{ matrix.python_version }} (deadsnakes)
-        uses: deadsnakes/action@v1.0.0
-        if: matrix.python_version == '3.9-dev'
+      - name: Setup MSYS2
+        uses: msys2/setup-msys2@v2
+        if: matrix.python_version == 'msys2'
         with:
-          python-version: ${{ matrix.python_version }}
-          architecture: x64
-      - run: pip install -U setuptools
-      - run: pip install -e .[toml] pytest
-      - run: pytest
-
-  check_selfinstall:
-    runs-on: ubuntu-latest
-    strategy:
-      fail-fast: false
-      matrix:
-        python_version: [ '2.7', '3.5', '3.6', '3.7', '3.8', 'pypy2', 'pypy3' ]
-    name: check self install - Python ${{ matrix.python_version }}
-    steps:
-      - uses: actions/checkout@v1
-      - name: Setup python
-        uses: actions/setup-python@v2
+          msystem: MINGW64
+          install: git mingw-w64-x86_64-python mingw-w64-x86_64-python-setuptools
+          update: true
+      - name: Setup GnuPG
+        # At present, the Windows VMs only come with the copy of GnuPG that's bundled
+        # with Git for Windows. If we want to use this version _and_ be able to set
+        # arbitrary GnuPG home directories, then the test would need to figure out when
+        # to convert Windows-style paths into Unix-style paths with cygpath, which is
+        # unreasonable.
+        #
+        # Instead, we'll install a version of GnuPG that can handle Windows-style paths.
+        # However, due to <https://dev.gnupg.org/T5593>, installation fails if the PATH
+        # environment variable is too long. Consequently, we need to shorten PATH to
+        # something minimal before we can install GnuPG. For further details, see
+        # <https://github.com/actions/virtual-environments/issues/2876>.
+        #
+        # Additionally, we'll explicitly set `gpg.program` to ensure Git for Windows
+        # doesn't invoke the bundled GnuPG, otherwise we'll run into
+        # <https://dev.gnupg.org/T5504>. See also: <https://dev.gnupg.org/T3020>.
+        run: |
+          $env:PATH = "C:\Program Files\Git\bin;C:\Windows\system32;C:\Windows;C:\Windows\System32\Wbem;C:\Windows\System32\WindowsPowerShell\v1.0\;C:\ProgramData\Chocolatey\bin"
+          [Environment]::SetEnvironmentVariable("Path", $env:PATH, "Machine")
+          choco install gnupg -y --no-progress
+          echo "C:\Program Files (x86)\gnupg\bin" >> $env:GITHUB_PATH
+          git config --system gpg.program "C:\Program Files (x86)\gnupg\bin\gpg.exe"
+        if: runner.os == 'Windows'
+      - run: pip install -U 'setuptools>=61'
+      - uses: actions/download-artifact@v3
         with:
-          python-version: ${{ matrix.python_version }}
-          architecture: x64
-      # self install testing needs some clarity
-      # so its being executed without any other tools running
-      - run: pip install -U setuptools
-      - run: python setup.py egg_info
-      - run: python setup.py sdist
-      - run: easy_install dist/*
-      - run: python testing/check_self_install.py
+          name: Packages
+          path: dist
+      - shell: bash
+        run: pip install "$(echo -n dist/*whl)[toml,test]"
+      - run: |
+          $(hg debuginstall --template "{pythonexe}") -m pip install hg-git --user
+        if: matrix.os == 'ubuntu-latest'
+      # this hopefull helps with os caches, hg init sometimes gets 20s timeouts
+      - run: hg version
+      - run: pytest
+        timeout-minutes: 15
 
+  dist_upload:
 
-  eggs:
     runs-on: ubuntu-latest
-
+    if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags')
+    permissions:
+      id-token: write
     needs: [test]
-    name: Python ${{ matrix.python_version }} eggs
-    strategy:
-      matrix:
-        python_version: ['2.7', '3.5', '3.6', '3.7', '3.8', '3.9-dev']
     steps:
-    - uses: actions/checkout@v1
-    - name: Setup python
-      uses: actions/setup-python@v2
-      if: matrix.python_version != '3.9-dev'
-      with:
-        python-version: ${{ matrix.python_version }}
-        architecture: x64
-    - name: Set up Python ${{ matrix.python_version }} (deadsnakes)
-      uses: deadsnakes/action@v1.0.0
-      if: matrix.python_version == '3.9-dev'
-      with:
-        python-version: ${{ matrix.python_version }}
-        architecture: x64
-    - name: Install dependencies
-      run: |
-        python -m pip install --upgrade pip
-        pip install --upgrade wheel setuptools
-    - run: python setup.py egg_info
-    - name: Build package
-      run: python setup.py bdist_egg
-    - uses: actions/upload-artifact@v2
+    - uses: actions/download-artifact@v3
       with:
-        name: dist
+        name: Packages
         path: dist
+    - name: Publish package to PyPI
+      uses: pypa/gh-action-pypi-publish@release/v1
 
-  dist:
+  test-pypi-upload:
     runs-on: ubuntu-latest
-
     needs: [test]
-    name: Python bdist/wheel
-    steps:
-    - uses: actions/checkout@v1
-    - uses: actions/setup-python@v1
-      with:
-        python-version: "3.8"
-    - name: Install dependencies
-      run: |
-        python -m pip install --upgrade pip
-        pip install --upgrade wheel setuptools
-    - run: python setup.py egg_info
-    - name: Build package
-      run: python setup.py bdist_wheel sdist
-    - uses: actions/upload-artifact@v2
-      with:
-        name: dist
-        path: dist
-
-
-  dist_check:
-    runs-on: ubuntu-latest
-    needs: [eggs, dist]
-    steps:
-    - uses: actions/setup-python@v2
-      with:
-        python-version: "3.8"
-    - name: Install dependencies
-      run: pip install twine
-    - uses: actions/download-artifact@v2
-      with:
-        name: dist
-        path: dist
-    - run: twine check dist/*
-
-  dist_upload:
-
-    runs-on: ubuntu-latest
-    if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags')
-    needs: [dist_check]
+    permissions:
+      id-token: write
     steps:
-    - uses: actions/download-artifact@v2
+    - uses: actions/download-artifact@v3
       with:
-        name: dist
+        name: Packages
         path: dist
     - name: Publish package to PyPI
-      uses: pypa/gh-action-pypi-publish@master
+      continue-on-error: true
+      uses: pypa/gh-action-pypi-publish@release/v1
       with:
-        user: __token__
-        password: ${{ secrets.pypi_token }}
+        repository-url: https://test.pypi.org/legacy/
diff --git a/.gitignore b/.gitignore
deleted file mode 100644 (file)
index c51b132..0000000
+++ /dev/null
@@ -1,48 +0,0 @@
-### 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.md b/CHANGELOG.md
new file mode 100644 (file)
index 0000000..db8efbe
--- /dev/null
@@ -0,0 +1,714 @@
+
+# v8.0.4
+
+## Changed
+
+- introduce scriv for changelog management
+- reconfigure local build backend to use an attribute instead of star imports from setuptools
+- introduce ruff as a linter
+- ensure the setuptools version keyword correctly load pyproject.toml configuration
+- add build and wheel to the test requirements for regression testing
+- move internal toml handling to own module
+
+## Fixed
+
+- fix #925: allow `write_to` to be an absolute path when it's a subdirectory of the root
+- fix #932: ensure type annotations in version file don't cause linter issues
+- fix #930: temporary restore `DEFAULT_VERSION_SCHEME` and `DEFAULT_LOCAL_SCHEME` on the `setuptools_scm` package
+
+
+
+# v8.0.3
+
+## bugfix
+
+- fix #918 for good - remove external importlib-metadata to avoid source only loop
+- fix #926: ensure mypy on python3.8 works with the version file
+
+# v8.0.2
+
+## bugfix
+
+- fix #919: restore legacy version-file behaviour for external callers + add Deprecation warning
+- fix #918: use packaging from setuptools for self-build
+- fix #914: ignore the deprecated git archival plugin as its integrated now
+- fix #912: ensure mypy safety of the version template + regression test
+- fix #913: use 240s timeout instead of 20 for `git unshallow`
+  to account for large repos or slow connections
+
+
+# v8.0.1
+
+## bugfix
+
+- update version file template to work on older python versions by using type comments
+- ensure tag regex from setup.py is parsed into regex
+
+# v8.0.0
+
+## breaking
+
+- remove legacy version parser api - config arg always required
+- turn Configuration into a dataclass
+- require configuration to always pass into helpers
+- hide file-finders implementation in private module
+- renamed setuptools_scm.hacks to setuptools_scm.fallbacks and drop support for pip-egg-info
+- remove trace function and use logging instead
+- unify `distance=None` and `distance=0` they should mean the same andwhere hiding dirty states that are now explicitly dirty
+- depend on later importlib for the full selectable api
+- move setuptools integration code to private sub-package
+- use normalized dist names for the `SETUPTOOLS_SCM_PRETEND_VERSION_FOR_${DIST_NAME}` env var
+- drop support for python 3.7
+- introduce `version_file` as replacement for `write_to`
+
+## features
+
+- created a directory for the vcs-versioning package and added it to pypi
+- git: expect main as possible default branch
+- drop version_from_scm helper
+- trim down exposed public api
+- no longer self-call twice in setuptools
+- add support for version schemes by import
+- chores
+
+    - migrate own metadata to pyproject.toml
+    - consolidate version schemes
+    - stricter tag typing
+    - pre-compiled regex
+    - move helpers to private modules
+
+- support passing log levels to SETUPTOOLS_SCM_DEBUG
+- support using rich.logging as console log handler if installed
+- fix #527: type annotation in default version template
+- fix #549: use fallbacks when scm search raises CommandNotFoundError
+
+## bugfixes
+
+- fix #883: use HeadersParser to ensure only mime metadata in headers is used
+- fix #884: parse calver dates from versions with the v prefix
+- don't use a C locale without UTF-8 support, when running commands.
+
+# v7.1.0
+
+- #748: use `tomllib` from stdlib
+- fix #762: handle non-ascii in setup.cfg
+- #752: implement fallback file finders for archives
+- #765: removed coding header in python template
+- declared Python 3.11 support
+- fix #759: update .git_archival.txt templates match git-describe invocation
+- fix #772: fix handling of .git-archival.txt from tagged commit
+
+# v7.0.5
+
+- fixes #742, #745: correctly hande accidentally released archival files
+
+# v7.0.4
+
+- fix #727: correctly handle incomplete archival from setuptools_scm_git_archival
+- fix #691: correctly handle specifying root in pyproject.toml
+- correct root override check condition (to ensure absolute path matching)
+- allow root by the cli to be considered relative to the cli (using abspath)
+
+# v7.0.3
+
+- fix mercurial usage when pip primes a isolated environment
+- fix regression for branch names on git + add a test
+
+# v7.0.2
+
+- fix #723 and #722: remove bootstrap dependencies
+- ensure we read the distribution name from `setup.cfg` if needed even for `pyproject.toml`
+
+# v7.0.1
+
+- fix #718: Avoid `ModuleNotFoundError` by requiring `importlib_metadata` in `python<3.8`
+
+# v7.0.0
+
+- drop python 3.6 support
+- include git archival support
+- fix #707: support git version detection even when git protects against mismatched owners
+  (common with misconfigured containers, thanks @chrisburr )
+- fix #548: correctly handle parsing the commit timestamp of HEAD when `log.showSignature` is set
+
+# v6.4.2
+
+- fix #671: `NoReturn` is not available in painfully dead python 3.6
+
+# v6.4.1
+
+-   fix regression #669: restore get_version signature
+-   fix #668: harden the self-test for distribution extras
+
+# 6.4.0
+
+-   compatibility adjustments for setuptools \>58
+- only put minimal setuptools version into toml extra to warn people with old strict pins
+- correctly handle hg-git self-use
+- better mercurial detection
+- modernize packaging setup
+- python 3.10 support
+- better handling of setuptools install command deprecation
+- consider `pyproject.tomls` when running as command
+- use list in git describe command to avoid shell expansions while supporting both windows and posix
+- add `--strip-dev` flag to `python -m setuptools_scm` to print the next guessed version cleanly
+- ensure no-guess-dev will fail on bad tags instead of generating invalid versions
+- ensure we use utc everywhere to avoid confusion
+
+# 6.3.2
+
+- fix #629: correctly convert Version data in tags_to_version parser to avoid errors
+
+# 6.3.1
+
+- fix #625: restore tomli in install_requires after the regression changes in took it out
+  and some users never added it even tho they have pyproject.toml files
+
+# 6.3.0
+
+## warning
+
+This release explicitly warns on unsupported setuptools. This
+unfortunately has to happen as the legacy `setup_requires` mechanism
+incorrectly configures the setuptools working-set when a more recent
+setuptools version than available is required.
+
+As all releases of setuptools are affected as the historic mechanism for
+ensuring a working setuptools setup was shipping a `ez_setup` file next
+to `setup.py`, which would install the required version of setuptools.
+
+This mechanism has long since been deprecated and removed as most people
+haven\'t been using it
+
+## bugfixes
+
+- fix #612: depend on packaging to ensure version parsing parts
+- fix #611: correct the typo that hid away the toml extra and add it in `setup.py` as well
+- fix #615: restore support for the git_archive plugin which doesn't pass over the config
+- restore the ability to run on old setuptools while to avoid breaking pipelines
+
+# v6.2.0
+
+-   fix #608: resolve tomli dependency issue by making it a hard
+    dependency as all intended/supported install options use pip/wheel
+    this is only a feature release
+-   ensure python 3.10 works
+
+# v6.1.1
+
+-   fix #605: completely disallow bdist_egg - modern enough
+    setuptools\>=45 uses pip
+-   fix #606: re-integrate and harden toml parsing
+-   fix #597: harden and expand support for figuring the current
+    distribution name from [pyproject.toml]{.title-ref}
+    ([project.name]{.title-ref} or
+    [tool.setuptools_scm.dist_name]{.title-ref}) section or
+    [setup.cfg]{.title-ref} ([metadata.name]{.title-ref})
+
+# v6.1.0
+
+-   fix #587: don\'t fail file finders when distribution is not given
+-   fix #524: new parameters `normalize` and `version_cls` to customize
+    the version normalization class.
+-   fix #585: switch from toml to tomli for toml 1.0 support
+-   fix #591: allow to opt in for searching parent directories in the
+    api
+-   fix #589: handle yaml encoding using the expected defaults
+-   fix #575: recommend storing the version_module inside of
+    `mypkg/_version.py`
+-   fix #571: accept branches starting with `v` as release branches
+-   fix #557: Use `packaging.version` for `version_tuple`
+-   fix #544: enhance errors on unsupported python/setuptools versions
+
+# v6.0.1
+
+-   fix #537: drop node_date on old git to avoid errors on missing %cI
+
+# v6.0.0
+
+-   fix #517: drop dead python support \>3.6 required
+-   drop dead setuptools support \> 45 required (can install wheels)
+-   drop egg building (use wheels)
+-   add git node_date metadata to get the commit time-stamp of HEAD
+-   allow version schemes to be priority ordered lists of version
+    schemes
+-   support for calendar versioning (calver) by date
+
+# v5.0.2
+
+-   fix #415: use git for matching prefixes to support the windows
+    situation
+
+# v5.0.1
+
+- fix #509: support `SETUPTOOLS_SCM_PRETEND_VERSION_FOR_${DISTRIBUTION_NAME}` for `pyproject.toml`
+
+# v5.0.0
+
+Breaking changes:
+
+-   fix #339: strict errors on missing scm when parsing a scm dir to
+    avoid false version lookups
+-   fix #337: if relative_to is a directory instead of a file, consider
+    it as direct target instead of the containing folder and print a
+    warning
+
+Bugfixes:
+
+-   fix #352: add support for generally ignoring specific vcs roots
+-   fix #471: better error for version bump failing on complex but
+    accepted tag
+-   fix #479: raise indicative error when tags carry non-parsable
+    information
+-   Add `no-guess-dev` which does no next version guessing,
+    just adds `.post1.devN` in case there are new commits after the tag
+-   add python3.9
+-   enhance documentation
+-   consider SOURCE_DATE_EPOCH for versioning
+-   add a version_tuple to write_to templates
+-   fix #321: add support for the
+    `SETUPTOOLS_SCM_PRETEND_VERSION_FOR_${DISTRIBUTION_NAME}` env var to
+    target the pretend key
+-   fix #142: clearly list supported scm
+-   fix #213: better error message for non-zero dev numbers in tags
+-   fix #356: add git branch to version on describe failure
+
+# v4.1.2
+
+-   disallow git tags without dots by default again - #449
+
+# v4.1.1
+
+-   drop jaraco.windows from pyproject.toml, allows for wheel builds on
+    python2
+
+# v4.1.0
+
+-   include python 3.9 via the deadsnakes action
+-   return release_branch_semver scheme (it got dropped in a bad rebase)
+-   undo the devendoring of the samefile backport for python2.7 on
+    windows
+-   re-enable the building of universal wheels
+-   fix handling of missing git/hg on python2.7 (python 3 exceptions
+    where used)
+-   correct the tox flake8 invocation
+-   trigger builds on tags again
+
+# v4.0.0
+
+-   Add `parentdir_prefix_version` to support installs from GitHub
+    release tarballs.
+-   use Coordinated Universal Time (UTC)
+-   switch to github actions for ci
+-   fix documentation for `tag_regex` and add support for single digit
+    versions
+-   document handling of enterprise distros with unsupported setuptools
+    versions #312
+-   switch to declarative metadata
+-   drop the internal copy of samefile and use a dependency on
+    jaraco.windows on legacy systems
+-   select git tags based on the presence of numbers instead of dots
+-   enable getting a version form a parent folder prefix
+-   add release-branch-semver version scheme
+-   make global configuration available to version metadata
+-   drop official support for python 3.4
+
+# v3.5.0
+
+-   add `no-local-version` local scheme and improve documentation for
+    schemes
+
+# v3.4.4
+
+-   fix #403: also sort out resource warnings when dealing with git file
+    finding
+
+# v3.4.3
+
+-   fix #399: ensure the git file finder terminates subprocess after
+    reading archive
+
+# v3.4.2
+
+-   fix #395: correctly transfer tag regex in the Configuration
+    constructor
+-   rollback \--first-parent for git describe as it turns out to be a
+    regression for some users
+
+# v3.4.1
+
+-   pull in #377 to fix #374: correctly set up the default version
+    scheme for pyproject usage. this bugfix got missed when rushing the
+    release.
+
+# v3.4.0
+
+-   fix #181 - add support for projects built under setuptools
+    declarative config by way of the
+    setuptools.finalize_distribution_options hook in Setuptools 42.
+-   fix #305 - ensure the git file finder closes file descriptors even
+    when errors happen
+-   fix #381 - clean out env vars from the git hook system to ensure
+    correct function from within
+-   modernize docs wrt importlib.metadata
+
+*edited*
+
+-   use \--first-parent for git describe
+
+# v3.3.3
+
+-   add eggs for python3.7 and 3.8 to the deploy
+
+# v3.3.2
+
+-   fix #335 - fix python3.8 support and add builds for up to python3.8
+
+# v3.3.1
+
+-   fix #333 (regression from #198) - use a specific fallback root when
+    calling fallbacks. Remove old hack that resets the root when
+    fallback entrypoints are present.
+
+# v3.3.0
+
+-   fix #198 by adding the `fallback_version` option, which sets the
+    version to be used when everything else fails.
+
+# v3.2.0
+
+\* fix #303 and #283 by adding the option `git_describe_command` to
+allow the user to control the way that [git describe]{.title-ref} is
+called.
+
+# v3.1.0
+
+-   fix #297 - correct the invocation in version_from_scm and deprecate
+    it as its exposed by accident
+-   fix #298 - handle git file listing on empty repositories
+-   fix #268 - deprecate ScmVersion.extra
+
+# v3.0.6
+
+-   fix #295 - correctly handle self install from tarballs
+
+# v3.0.5
+
+-   fix #292 - match leading `V` character as well
+
+    <https://www.python.org/dev/peps/pep-0440/#preceding-v-character>
+
+# v3.0.4
+
+-   re-release of 3.0.3 after fixing the release process
+
+v3.0.3 (pulled from pypi due to a packaging issue) ======
+
+-   fix #286 - duo an oversight a helper function was returning a
+    generator instead of a list
+
+# v3.0.2
+
+-   fix a regression from tag parsing - support for multi-dashed
+    prefixes - #284
+
+# v3.0.1
+
+-   fix a regression in setuptools_scm.git.parse - reorder arguments so
+    the positional invocation from before works as expected #281
+
+# v3.0.0
+
+-   introduce pre-commit and use black
+-   print the origin module to help testing
+-   switch to src layout (breaking change)
+-   no longer alias tag and parsed_version in order to support
+    understanding a version parse failure
+-   require parse results to be ScmVersion or None (breaking change)
+-   fix #266 by requiring the prefix word to be a word again (breaking
+    change as the bug allowed arbitrary prefixes while the original
+    feature only allowed words\")
+-   introduce an internal config object to allow the configuration for
+    tag parsing and prefixes (thanks to \@punkadiddle for introducing it
+    and passing it through)
+
+# v2.1.0
+
+-   enhance docs for sphinx usage
+-   add symlink support to file finder for git #247 (thanks Stéphane
+    Bidoul)
+-   enhance tests handling win32 (thanks Stéphane Bidoul)
+
+# v2.0.0
+
+-   fix #237 - correct imports in code examples
+-   improve mercurial commit detection (thanks Aaron)
+-   breaking change: remove support for setuptools before parsed
+    versions
+-   reintroduce manifest as the travis deploy can\'t use the file finder
+-   reconfigure flake8 for future compatibility with black
+-   introduce support for branch name in version metadata and support a
+    opt-in simplified semver version scheme
+
+# v1.17.0
+
+-   fix regression in git support - use a function to ensure it works in
+    egg installed mode
+
+-   actually fail if file finding fails in order to see broken setups
+    instead of generating broken dists
+
+    (thanks Mehdi ABAAKOUK for both)
+
+# v1.16.2
+
+-   fix regression in handling git export ignores (thanks Mehdi
+    ABAAKOUK)
+
+# v1.16.1
+
+-   fix regression in support for old setuptools versions (thanks Marco
+    Clemencic)
+
+# v1.16.0
+
+-   drop support for eol python versions
+-   #214 - fix misuse in surrogate-escape api
+-   add the node-and-timestamp local version scheme
+-   respect git export ignores
+-   avoid shlex.split on windows
+-   fix #218 - better handling of mercurial edge-cases with tag commits
+    being considered as the tagged commit
+-   fix #223 - remove the dependency on the internal `SetuptoolsVersion`
+    as it was removed after long-standing deprecation
+
+v1.15.7 ======
+
+-   Fix #174 with #207: Re-use samefile backport as developed in
+    jaraco.windows, and only use the backport where samefile is not
+    available.
+
+# v1.15.6
+
+-   fix #171 by unpinning the py version to allow a fixed one to get
+    installed
+
+# v1.15.5
+
+-   fix #167 by correctly respecting preformatted version metadata from
+    PKG-INFO/EGG-INFO
+
+# v1.15.4
+
+-   fix issue #164: iterate all found entry points to avoid errors when
+    pip remakes egg-info
+-   enhance self-use to enable pip install from github again
+
+# v1.15.3
+
+-   bring back correctly getting our version in the own sdist, finalizes
+    #114
+-   fix issue #150: strip local components of tags
+
+# v1.15.2
+
+-   fix issue #128: return None when a scm specific parse fails in a
+    worktree to ease parse reuse
+
+# v1.15.1
+
+-   fix issue #126: the local part of any tags is discarded when
+    guessing new versions
+-   minor performance optimization by doing fewer git calls in the usual
+    cases
+
+# v1.15.0
+
+-   more sophisticated ignoring of mercurial tag commits when
+    considering distance in commits (thanks Petre Mierlutiu)
+-   fix issue #114: stop trying to be smart for the sdist and ensure it's
+    always correctly using itself
+-   update trove classifiers
+-   fix issue #84: document using the installed package metadata for
+    sphinx
+-   fix issue #81: fail more gracious when git/hg are missing
+-   address issue #93: provide an experimental api to customize
+    behaviour on shallow git repos a custom parse function may pick pre
+    parse actions to do when using git
+
+# v1.14.1
+
+- fix #109: when detecting a dirty git workdir
+
+  don't consider untracked file (this was a regression due to #86 in v1.13.1)
+- consider the distance 0 when the git node is unknown (happens when you haven't committed anything)
+
+# v1.14.0
+
+-   publish bdist_egg for python 2.6, 2.7 and 3.3-3.5
+-   fix issue #107 - dont use node if it is None
+
+# v1.13.1
+
+-   fix issue #86 - detect dirty git workdir without tags
+
+# v1.13.0
+
+-   fix regression caused by the fix of #101
+    -   assert types for version dumping
+    -   strictly pass all versions through parsed version metadata
+
+# v1.12.0
+
+-   fix issue #97 - add support for mercurial plugins
+-   fix issue #101 - write version cache even for pretend version
+    (thanks anarcat for reporting and fixing)
+
+# v1.11.1
+
+-   fix issue #88 - better docs for sphinx usage (thanks Jason)
+-   fix issue #89 - use normpath to deal with windows (thanks Te-jé
+    Rodgers for reporting and fixing)
+
+# v1.11.0
+
+-   always run tag_to_version so in order to handle prefixes on old
+    setuptools (thanks to Brian May)
+-   drop support for python 3.2
+-   extend the error message on missing scm metadata (thanks Markus
+    Unterwaditzer)
+-   fix bug when using callable version_scheme (thanks Esben Haabendal)
+
+# v1.10.1
+
+-   fix issue #73 - in hg pre commit merge, consider parent1 instead of
+    failing
+
+# v1.10.0
+
+-   add support for overriding the version number via the environment
+    variable SETUPTOOLS_SCM_PRETEND_VERSION
+
+-   fix issue #63 by adding the `--match` parameter to the git describe
+    call and prepare the possibility of passing more options to scm
+    backends
+
+-   fix issue #70 and #71 by introducing the parse keyword to specify
+    custom scm parsing, it's an expert feature, use with caution
+
+    this change also introduces the setuptools_scm.parse_scm_fallback
+    entrypoint which can be used to register custom archive fallbacks
+
+# v1.9.0
+
+- Add `relative_to` parameter to `get_version` function, fixes #44 per #45.
+
+# v1.8.0
+
+- fix issue with setuptools wrong version warnings being printed to standard out. User is informed now by distutils-warnings.
+- restructure root finding, we now reliably ignore outer scm and prefer PKG-INFO over scm, fixes #43 and #45
+
+# v1.7.0
+
+- correct the url to github thanks David Szotten
+- enhance scm not found errors with a note on git tarballs thanks Markus
+- add support for `write_to_template`
+
+# v1.6.0
+
+- bail out early if the scm is missing
+
+  this brings issues with git tarballs and older devpi-client releases
+  to light, before we would let the setup stay at version 0.0, now
+  there is a ValueError
+
+- properly raise errors on write_to misuse (thanks Te-jé Rodgers)
+
+# v1.5.5
+
+- Fix bug on Python 2 on Windows when environment has unicode fields.
+
+# v1.5.4
+
+- Fix bug on Python 2 when version is loaded from existing metadata.
+
+# v1.5.3
+
+- #28: Fix decoding error when PKG-INFO contains non-ASCII.
+
+# v1.5.2
+
+- add zip_safe flag
+
+# v1.5.1
+
+- fix file access bug I missed in 1.5
+
+# v1.5.0
+
+- moved setuptools integration related code to own file
+- support storing version strings into a module/text file using the `write_to` configuration parameter
+
+# v1.4.0
+
+- proper handling for sdist
+- fix file-finder failure from windows
+- reshuffle docs
+
+# v1.3.0
+
+- support setuptools easy_install egg creation details by hardwire-ing the version in the sdist
+
+# v1.2.0
+
+- enhance self-use
+
+# v1.1.0
+
+- enable self-use
+
+# v1.0.0
+
+- documentation enhancements
+
+# v0.26
+
+- rename to setuptools_scm
+- split into package, add lots of entry points for extension
+- pluggable version schemes
+
+# v0.25
+
+- fix pep440 support this reshuffles the complete code for version guessing
+
+# v0.24
+
+- don't drop dirty flag on node finding
+- fix distance for dirty flagged versions
+- use dashes for time again, its normalisation with setuptools
+- remove the own version attribute, it was too fragile to test for
+- include file finding
+- handle edge cases around dirty tagged versions
+
+# v0.23
+
+- windows compatibility fix (thanks stefan) drop samefile since it`s missing in some python2 versions on windows
+- add tests to the source tarballs
+
+# v0.22
+
+- windows compatibility fix (thanks stefan) use samefile since it does path normalisation
+
+# v0.21
+
+- fix the own version attribute (thanks stefan)
+
+# v0.20
+
+- fix issue 11: always take git describe long format to avoid the source of the ambiguity
+- fix issue 12: add a `__version__` attribute via pkginfo
+
+# v0.19
+
+- configurable next version guessing
+- fix distance guessing (thanks stefan)
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
deleted file mode 100644 (file)
index 251ee8d..0000000
+++ /dev/null
@@ -1,504 +0,0 @@
-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)
index 4bbd88d9e45d03b43592b86b7bb3568c29dfe153..6b9e32043c2052a83b531c8450386729e81e26ee 100644 (file)
@@ -1,10 +1,25 @@
 exclude *.nix
-exclude .travis.yaml
 exclude .pre-commit-config.yaml
+exclude changelog.d/*
+exclude .git_archival.txt
+exclude .readthedocs.yaml
 include *.py
 include testing/*.py
 include tox.ini
 include *.rst
 include LICENSE
 include *.toml
+include mypy.ini
+include testing/Dockerfile.*
+include src/setuptools_scm/.git_archival.txt
+include README.md
+include CHANGELOG.md
+
+
 recursive-include testing *.bash
+prune nextgen
+
+recursive-include docs *.md
+include docs/examples/version_scheme_code/*.py
+include docs/examples/version_scheme_code/*.toml
+include mkdocs.yml
index 1f3b082c8b8ca2413793611a2fc8c238bd8efdef..9ad3e8dce7fb1556412f25449c5b2b3d21eaf0f9 100644 (file)
--- a/PKG-INFO
+++ b/PKG-INFO
 Metadata-Version: 2.1
-Name: setuptools_scm
-Version: 4.1.2
+Name: setuptools-scm
+Version: 8.0.4
 Summary: the blessed package to manage your versions by scm tags
-Home-page: https://github.com/pypa/setuptools_scm/
-Author: Ronny Pfannschmidt
-Author-email: opensource@ronnypfannschmidt.de
-License: MIT
-Description: setuptools_scm
-        ===============
-        
-        ``setuptools_scm`` handles managing your Python package versions
-        in SCM metadata instead of declaring them as the version argument
-        or in a SCM managed file.
-        
-        Additionally ``setuptools_scm`` provides setuptools with a list of files that are managed by the SCM
-        (i.e. it automatically adds all of the SCM-managed files to the sdist).
-        Unwanted files must be excluded by discarding them via ``MANIFEST.in``.
-        
-        .. image:: https://travis-ci.org/pypa/setuptools_scm.svg?branch=master
-            :target: https://travis-ci.org/pypa/setuptools_scm
-        
-        .. image:: https://tidelift.com/badges/package/pypi/setuptools-scm
-           :target: https://tidelift.com/subscription/pkg/pypi-setuptools-scm?utm_source=pypi-setuptools-scm&utm_medium=readme
-        
-        
-        ``pyproject.toml`` usage
-        ------------------------
-        
-        The preferred way to configure ``setuptools_scm`` is to author
-        settings in a ``tool.setuptools_scm`` section of ``pyproject.toml``.
-        
-        This feature requires Setuptools 42 or later, released in Nov, 2019.
-        If your project needs to support build from sdist on older versions
-        of Setuptools, you will need to also implement the ``setup.py usage``
-        for those legacy environments.
-        
-        First, ensure that ``setuptools_scm`` is present during the project's
-        built step by specifying it as one of the build requirements.
-        
-        .. code:: toml
-        
-            # pyproject.toml
-            [build-system]
-            requires = ["setuptools>=42", "wheel", "setuptools_scm[toml]>=3.4"]
-        
-        Note that the ``toml`` extra must be supplied.
-        
-        That will be sufficient to require ``setuptools_scm`` for projects
-        that support PEP 518 (`pip <https://pypi.org/project/pip>`_ and
-        `pep517 <https://pypi.org/project/pep517/>`_). Many tools,
-        especially those that invoke ``setup.py`` for any reason, may
-        continue to rely on ``setup_requires``. For maximum compatibility
-        with those uses, consider also including a ``setup_requires`` directive
-        (described below in ``setup.py usage`` and ``setup.cfg``).
-        
-        To enable version inference, add this section to your pyproject.toml:
-        
-        .. code:: toml
-        
-            # pyproject.toml
-            [tool.setuptools_scm]
-        
-        Including this section is comparable to supplying
-        ``use_scm_version=True`` in ``setup.py``. Additionally,
-        include arbitrary keyword arguments in that section
-        to be supplied to ``get_version()``. For example:
-        
-        .. code:: toml
-        
-            # pyproject.toml
-        
-            [tool.setuptools_scm]
-            write_to = "pkg/version.py"
-        
-        
-        ``setup.py`` usage
-        ------------------
-        
-        The following settings are considered legacy behavior and
-        superseded by the ``pyproject.toml`` usage, but for maximal
-        compatibility, projects may also supply the configuration in
-        this older form.
-        
-        To use ``setuptools_scm`` just modify your project's ``setup.py`` file
-        like this:
-        
-        * Add ``setuptools_scm`` to the ``setup_requires`` parameter.
-        * Add the ``use_scm_version`` parameter and set it to ``True``.
-        
-        For example:
-        
-        .. code:: python
-        
-            from setuptools import setup
-            setup(
-                ...,
-                use_scm_version=True,
-                setup_requires=['setuptools_scm'],
-                ...,
-            )
-        
-        Arguments to ``get_version()`` (see below) may be passed as a dictionary to
-        ``use_scm_version``. For example:
-        
-        .. code:: python
-        
-            from setuptools import setup
-            setup(
-                ...,
-                use_scm_version = {
-                    "root": "..",
-                    "relative_to": __file__,
-                    "local_scheme": "node-and-timestamp"
-                },
-                setup_requires=['setuptools_scm'],
-                ...,
-            )
-        
-        You can confirm the version number locally via ``setup.py``:
-        
-        .. code-block:: shell
-        
-            $ python setup.py --version
-        
-        .. note::
-        
-           If you see unusual version numbers for packages but ``python setup.py
-           --version`` reports the expected version number, ensure ``[egg_info]`` is
-           not defined in ``setup.cfg``.
-        
-        
-        ``setup.cfg`` usage
-        -------------------
-        
-        If using `setuptools 30.3.0
-        <https://setuptools.readthedocs.io/en/latest/setuptools.html#configuring-setup-using-setup-cfg-files>`_
-        or greater, you can store ``setup_requires`` configuration in ``setup.cfg``.
-        However, ``use_scm_version`` must still be placed in ``setup.py``. For example:
-        
-        .. code:: python
-        
-            # setup.py
-            from setuptools import setup
-            setup(
-                use_scm_version=True,
-            )
-        
-        .. code:: ini
-        
-            # setup.cfg
-            [metadata]
-            ...
-        
-            [options]
-            setup_requires =
-              setuptools_scm
-            ...
-        
-        .. important::
-        
-            Ensure neither the ``[metadata]`` ``version`` option nor the ``[egg_info]``
-            section are defined, as these will interfere with ``setuptools_scm``.
-        
-        You may also need to define a ``pyproject.toml`` file (`PEP-0518
-        <https://www.python.org/dev/peps/pep-0518>`_) to ensure you have the required
-        version of ``setuptools``:
-        
-        .. code:: ini
-        
-            # pyproject.toml
-            [build-system]
-            requires = ["setuptools>=30.3.0", "wheel", "setuptools_scm"]
-        
-        For more information, refer to the `setuptools issue #1002
-        <https://github.com/pypa/setuptools/issues/1002>`_.
-        
-        
-        Programmatic usage
-        ------------------
-        
-        In order to use ``setuptools_scm`` from code that is one directory deeper
-        than the project's root, you can use:
-        
-        .. code:: python
-        
-            from setuptools_scm import get_version
-            version = get_version(root='..', relative_to=__file__)
-        
-        See `setup.py Usage`_ above for how to use this within ``setup.py``.
-        
-        
-        Retrieving package version at runtime
-        -------------------------------------
-        
-        If you have opted not to hardcode the version number inside the package,
-        you can retrieve it at runtime from PEP-0566_ metadata using
-        ``importlib.metadata`` from the standard library
-        or the `importlib_metadata`_ backport:
-        
-        .. code:: python
-        
-            from importlib.metadata import version, PackageNotFoundError
-        
-            try:
-                __version__ = version(__name__)
-            except PackageNotFoundError:
-                # package is not installed
-               pass
-        
-        Alternatively, you can use ``pkg_resources`` which is included in
-        ``setuptools``:
-        
-        .. code:: python
-        
-           from pkg_resources import get_distribution, DistributionNotFound
-        
-           try:
-               __version__ = get_distribution(__name__).version
-           except DistributionNotFound:
-                # package is not installed
-               pass
-        
-        This does place a runtime dependency on ``setuptools``.
-        
-        .. _PEP-0566: https://www.python.org/dev/peps/pep-0566/
-        .. _importlib_metadata: https://pypi.org/project/importlib-metadata/
-        
-        
-        Usage from Sphinx
-        -----------------
-        
-        It is discouraged to use ``setuptools_scm`` from Sphinx itself,
-        instead use ``pkg_resources`` after editable/real installation:
-        
-        .. code:: python
-        
-            # contents of docs/conf.py
-            from pkg_resources import get_distribution
-            release = get_distribution('myproject').version
-            # for example take major/minor
-            version = '.'.join(release.split('.')[:2])
-        
-        The underlying reason is, that services like *Read the Docs* sometimes change
-        the working directory for good reasons and using the installed metadata
-        prevents using needless volatile data there.
-        
-        Notable Plugins
-        ----------------
-        
-        `setuptools_scm_git_archive <https://pypi.python.org/pypi/setuptools_scm_git_archive>`_
-            Provides partial support for obtaining versions from git archives that
-            belong to tagged versions. The only reason for not including it in
-            ``setuptools_scm`` itself is Git/GitHub not supporting sufficient metadata
-            for untagged/followup commits, which is preventing a consistent UX.
-        
-        
-        Default versioning scheme
-        --------------------------
-        
-        In the standard configuration ``setuptools_scm`` takes a look at three things:
-        
-        1. latest tag (with a version number)
-        2. the distance to this tag (e.g. number of revisions since latest tag)
-        3. workdir state (e.g. uncommitted changes since latest tag)
-        
-        and uses roughly the following logic to render the version:
-        
-        no distance and clean:
-            ``{tag}``
-        distance and clean:
-            ``{next_version}.dev{distance}+{scm letter}{revision hash}``
-        no distance and not clean:
-            ``{tag}+dYYYYMMDD``
-        distance and not clean:
-            ``{next_version}.dev{distance}+{scm letter}{revision hash}.dYYYYMMDD``
-        
-        The next version is calculated by adding ``1`` to the last numeric component of
-        the tag.
-        
-        For Git projects, the version relies on `git describe <https://git-scm.com/docs/git-describe>`_,
-        so you will see an additional ``g`` prepended to the ``{revision hash}``.
-        
-        Semantic Versioning (SemVer)
-        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-        
-        Due to the default behavior it's necessary to always include a
-        patch version (the ``3`` in ``1.2.3``), or else the automatic guessing
-        will increment the wrong part of the SemVer (e.g. tag ``2.0`` results in
-        ``2.1.devX`` instead of ``2.0.1.devX``). So please make sure to tag
-        accordingly.
-        
-        .. note::
-        
-            Future versions of ``setuptools_scm`` will switch to `SemVer
-            <http://semver.org/>`_ by default hiding the the old behavior as an
-            configurable option.
-        
-        
-        Builtin mechanisms for obtaining version numbers
-        --------------------------------------------------
-        
-        1. the SCM itself (git/hg)
-        2. ``.hg_archival`` files (mercurial archives)
-        3. ``PKG-INFO``
-        
-        .. note::
-        
-            Git archives are not supported due to Git shortcomings
-        
-        
-        File finders hook makes most of MANIFEST.in unnecessary
-        -------------------------------------------------------
-        
-        ``setuptools_scm`` implements a `file_finders
-        <https://setuptools.readthedocs.io/en/latest/setuptools.html#adding-support-for-revision-control-systems>`_
-        entry point which returns all files tracked by your SCM. This eliminates
-        the need for a manually constructed ``MANIFEST.in`` in most cases where this
-        would be required when not using ``setuptools_scm``, namely:
-        
-        * To ensure all relevant files are packaged when running the ``sdist`` command.
-        
-        * When using `include_package_data <https://setuptools.readthedocs.io/en/latest/setuptools.html#including-data-files>`_
-          to include package data as part of the ``build`` or ``bdist_wheel``.
-        
-        ``MANIFEST.in`` may still be used: anything defined there overrides the hook.
-        This is mostly useful to exclude files tracked in your SCM from packages,
-        although in principle it can be used to explicitly include non-tracked files
-        too.
-        
-        
-        Configuration parameters
-        ------------------------
-        
-        In order to configure the way ``use_scm_version`` works you can provide
-        a mapping with options instead of a boolean value.
-        
-        The currently supported configuration keys are:
-        
-        :root:
-            Relative path to cwd, used for finding the SCM root; defaults to ``.``
-        
-        :version_scheme:
-            Configures how the local version number is constructed; either an
-            entrypoint name or a callable.
-        
-        :local_scheme:
-            Configures how the local component of the version is constructed; either an
-            entrypoint name or a callable.
-        
-        :write_to:
-            A path to a file that gets replaced with a file containing the current
-            version. It is ideal for creating a ``version.py`` file within the
-            package, typically used to avoid using `pkg_resources.get_distribution`
-            (which adds some overhead).
-        
-            .. warning::
-        
-              Only files with :code:`.py` and :code:`.txt` extensions have builtin
-              templates, for other file types it is necessary to provide
-              :code:`write_to_template`.
-        
-        :write_to_template:
-            A newstyle format string that is given the current version as
-            the ``version`` keyword argument for formatting.
-        
-        :relative_to:
-            A file from which the root can be resolved.
-            Typically called by a script or module that is not in the root of the
-            repository to point ``setuptools_scm`` at the root of the repository by
-            supplying ``__file__``.
-        
-        :tag_regex:
-           A Python regex string to extract the version part from any SCM tag.
-            The regex needs to contain either a single match group, or a group
-            named ``version``, that captures the actual version information.
-        
-            Defaults to the value of ``setuptools_scm.config.DEFAULT_TAG_REGEX``
-            (see `config.py <src/setuptools_scm/config.py>`_).
-        
-        :parentdir_prefix_version:
-            If the normal methods for detecting the version (SCM version,
-            sdist metadata) fail, and the parent directory name starts with
-            ``parentdir_prefix_version``, then this prefix is stripped and the rest of
-            the parent directory name is matched with ``tag_regex`` to get a version
-            string.  If this parameter is unset (the default), then this fallback is
-            not used.
-        
-            This is intended to cover GitHub's "release tarballs", which extract into
-            directories named ``projectname-tag/`` (in which case
-            ``parentdir_prefix_version`` can be set e.g. to ``projectname-``).
-        
-        :fallback_version:
-            A version string that will be used if no other method for detecting the
-            version worked (e.g., when using a tarball with no metadata). If this is
-            unset (the default), setuptools_scm will error if it fails to detect the
-            version.
-        
-        :parse:
-            A function that will be used instead of the discovered SCM for parsing the
-            version.
-            Use with caution, this is a function for advanced use, and you should be
-            familiar with the ``setuptools_scm`` internals to use it.
-        
-        :git_describe_command:
-            This command will be used instead the default ``git describe`` command.
-            Use with caution, this is a function for advanced use, and you should be
-            familiar with the ``setuptools_scm`` internals to use it.
-        
-            Defaults to the value set by ``setuptools_scm.git.DEFAULT_DESCRIBE``
-            (see `git.py <src/setuptools_scm/git.py>`_).
-        
-        To use ``setuptools_scm`` in other Python code you can use the ``get_version``
-        function:
-        
-        .. code:: python
-        
-            from setuptools_scm import get_version
-            my_version = get_version()
-        
-        It optionally accepts the keys of the ``use_scm_version`` parameter as
-        keyword arguments.
-        
-        Example configuration in ``setup.py`` format:
-        
-        .. code:: python
-        
-            from setuptools import setup
-        
-            setup(
-                use_scm_version={
-                    'write_to': 'version.py',
-                    'write_to_template': '__version__ = "{version}"',
-                    'tag_regex': r'^(?P<prefix>v)?(?P<version>[^\+]+)(?P<suffix>.*)?$',
-                }
-            )
-        
-        Environment variables
-        ---------------------
-        
-        :SETUPTOOLS_SCM_PRETEND_VERSION:
-            when defined and not empty,
-            its used as the primary source for the version number
-            in which case it will be a unparsed string
-        
-        :SETUPTOOLS_SCM_DEBUG:
-            when defined and not empty,
-            a lot of debug information will be printed as part of ``setuptools_scm``
-            operating
-        
-        Extending setuptools_scm
-        ------------------------
-        
-        ``setuptools_scm`` ships with a few ``setuptools`` entrypoints based hooks to
-        extend its default capabilities.
-        
-        Adding a new SCM
-        ~~~~~~~~~~~~~~~~
-        
-        ``setuptools_scm`` provides two entrypoints for adding new SCMs:
-        
-        ``setuptools_scm.parse_scm``
-            A function used to parse the metadata of the current workdir
-            using the name of the control directory/file of your SCM as the
-            entrypoint's name. E.g. for the built-in entrypoint for git the
-            entrypoint is named ``.git`` and references ``setuptools_scm.git:parse``
-        
-          The return value MUST be a ``setuptools_scm.version.ScmVersion`` instance
-          created by the function ``setuptools_scm.version:meta``.
-        
-        ``setuptools_scm.files_command``
-          Either a string containing a shell command that prints all SCM managed
-          files in its current working directory or a callable, that given a
-          pathname will return that list.
-        
-          Also use then name of your SCM control directory as name of the entrypoint.
-        
-        Version number construction
-        ~~~~~~~~~~~~~~~~~~~~~~~~~~~
-        
-        ``setuptools_scm.version_scheme``
-            Configures how the version number is constructed given a
-            ``setuptools_scm.version.ScmVersion`` instance and should return a string
-            representing the version.
-        
-            Available implementations:
-        
-            :guess-next-dev: Automatically guesses the next development version (default).
-                Guesses the upcoming release by incrementing the pre-release segment if present,
-                otherwise by incrementing the micro segment. Then appends :code:`.devN`.
-            :post-release: generates post release versions (adds :code:`.postN`)
-            :python-simplified-semver: Basic semantic versioning. Guesses the upcoming release
-                by incrementing the minor segment and setting the micro segment to zero if the
-                current branch contains the string ``'feature'``, otherwise by incrementing the
-                micro version. Then appends :code:`.devN`. Not compatible with pre-releases.
-            :release-branch-semver: Semantic versioning for projects with release branches. The
-                same as ``guess-next-dev`` (incrementing the pre-release or micro segment) if on
-                a release branch: a branch whose name (ignoring namespace) parses as a version
-                that matches the most recent tag up to the minor segment. Otherwise if on a
-                non-release branch, increments the minor segment and sets the micro segment to
-                zero, then appends :code:`.devN`.
-        
-        ``setuptools_scm.local_scheme``
-            Configures how the local part of a version is rendered given a
-            ``setuptools_scm.version.ScmVersion`` instance and should return a string
-            representing the local version.
-            Dates and times are in Coordinated Universal Time (UTC), because as part
-            of the version, they should be location independent.
-        
-            Available implementations:
-        
-            :node-and-date: adds the node on dev versions and the date on dirty
-                            workdir (default)
-            :node-and-timestamp: like ``node-and-date`` but with a timestamp of
-                                 the form ``{:%Y%m%d%H%M%S}`` instead
-            :dirty-tag: adds ``+dirty`` if the current workdir has changes
-            :no-local-version: omits local version, useful e.g. because pypi does
-                               not support it
-        
-        
-        Importing in ``setup.py``
-        ~~~~~~~~~~~~~~~~~~~~~~~~~
-        
-        To support usage in ``setup.py`` passing a callable into ``use_scm_version``
-        is supported.
-        
-        Within that callable, ``setuptools_scm`` is available for import.
-        The callable must return the configuration.
-        
-        
-        .. code:: python
-        
-            # content of setup.py
-            import setuptools
-        
-            def myversion():
-                from setuptools_scm.version import get_local_dirty_tag
-                def clean_scheme(version):
-                    return get_local_dirty_tag(version) if version.dirty else '+clean'
-        
-                return {'local_scheme': clean_scheme}
-        
-            setup(
-                ...,
-                use_scm_version=myversion,
-                ...
-            )
-        
-        
-        Note on testing non-installed versions
-        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-        
-        While the general advice is to test against a installed version,
-        some environments require a test prior to install,
-        
-        .. code::
-        
-          $ python setup.py egg_info
-          $ PYTHONPATH=$PWD:$PWD/src pytest
-        
-        
-        Interaction with Enterprise Distributions
-        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-        
-        Some enterprise distributions like RHEL7 and others
-        ship rather old setuptools versions due to various release management details.
-        
-        On such distributions one might observe errors like:
-        
-        :code:``setuptools_scm.version.SetuptoolsOutdatedWarning: your setuptools is too old (<12)``
-        
-        In those case its typically possible to build by using a sdist against ``setuptools_scm<2.0``.
-        As those old setuptools versions lack sensible types for versions,
-        modern setuptools_scm is unable to support them sensibly.
-        
-        In case the project you need to build can not be patched to either use old setuptools_scm,
-        its still possible to install a more recent version of setuptools in order to handle the build
-        and/or install the package by using wheels or eggs.
-        
-        
-        
-        
-        Code of Conduct
-        ---------------
-        
-        Everyone interacting in the ``setuptools_scm`` project's codebases, issue
-        trackers, chat rooms, and mailing lists is expected to follow the
-        `PyPA Code of Conduct`_.
-        
-        .. _PyPA Code of Conduct: https://www.pypa.io/en/latest/code-of-conduct/
-        
-        Security Contact
-        ================
-        
-        To report a security vulnerability, please use the
-        `Tidelift security contact <https://tidelift.com/security>`_.
-        Tidelift will coordinate the fix and disclosure.
-        
-Platform: UNKNOWN
+Author-email: Ronny Pfannschmidt <opensource@ronnypfannschmidt.de>
+License: Permission is hereby granted, free of charge, to any person obtaining a copy
+        of this software and associated documentation files (the "Software"), to deal
+        in the Software without restriction, including without limitation the rights
+        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+        copies of the Software, and to permit persons to whom the Software is
+        furnished to do so, subject to the following conditions:
+        
+        The above copyright notice and this permission notice shall be included in
+        all copies or substantial portions of the Software.
+        
+        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+        THE SOFTWARE.
+        
+Project-URL: documentation, https://setuptools-scm.readthedocs.io/
+Project-URL: repository, https://github.com/pypa/setuptools_scm/
 Classifier: Development Status :: 5 - Production/Stable
 Classifier: Intended Audience :: Developers
 Classifier: License :: OSI Approved :: MIT License
 Classifier: Programming Language :: Python
-Classifier: Programming Language :: Python :: 2.7
-Classifier: Programming Language :: Python :: 3
-Classifier: Programming Language :: Python :: 3.5
-Classifier: Programming Language :: Python :: 3.6
-Classifier: Programming Language :: Python :: 3.7
+Classifier: Programming Language :: Python :: 3 :: Only
 Classifier: Programming Language :: Python :: 3.8
+Classifier: Programming Language :: Python :: 3.9
+Classifier: Programming Language :: Python :: 3.10
+Classifier: Programming Language :: Python :: 3.11
 Classifier: Topic :: Software Development :: Libraries
 Classifier: Topic :: Software Development :: Version Control
 Classifier: Topic :: System :: Software Distribution
 Classifier: Topic :: Utilities
-Requires-Python: !=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7
+Requires-Python: >=3.8
+Description-Content-Type: text/markdown
+License-File: LICENSE
+Requires-Dist: packaging>=20
+Requires-Dist: setuptools
+Requires-Dist: tomli>=1; python_version < "3.11"
+Requires-Dist: typing-extensions
+Provides-Extra: docs
+Requires-Dist: entangled_cli[rich]; extra == "docs"
+Requires-Dist: mkdocs; extra == "docs"
+Requires-Dist: mkdocs-entangled-plugin; extra == "docs"
+Requires-Dist: mkdocs-material; extra == "docs"
+Requires-Dist: mkdocstrings[python]; extra == "docs"
+Requires-Dist: pygments; extra == "docs"
+Provides-Extra: rich
+Requires-Dist: rich; extra == "rich"
+Provides-Extra: test
+Requires-Dist: build; extra == "test"
+Requires-Dist: pytest; extra == "test"
+Requires-Dist: rich; extra == "test"
+Requires-Dist: wheel; extra == "test"
 Provides-Extra: toml
+
+# setuptools_scm
+[![github ci](https://github.com/pypa/setuptools_scm/workflows/python%20tests+artifacts+release/badge.svg)](https://github.com/pypa/setuptools_scm/actions)
+[![Documentation Status](https://readthedocs.org/projects/setuptools-scm/badge/?version=latest)](https://setuptools-scm.readthedocs.io/en/latest/?badge=latest)
+[![tidelift](https://tidelift.com/badges/package/pypi/setuptools-scm) ](https://tidelift.com/subscription/pkg/pypi-setuptools-scm?utm_source=pypi-setuptools-scm&utm_medium=readme)
+
+## about
+
+[setuptools-scm] extracts Python package versions from `git` or
+`hg` metadata instead of declaring them as the version argument
+or in an SCM managed file.
+
+Additionally, [setuptools-scm] provides setuptools
+with a list of files that are managed by the SCM <br/>
+(i.e. it automatically adds **all of** the SCM-managed files to the sdist).<br/>
+Unwanted files must be excluded via `MANIFEST.in`.
+
+
+## `pyproject.toml` usage
+
+The preferred way to configure [setuptools-scm] is to author
+settings in a `tool.setuptools_scm` section of `pyproject.toml`.
+
+This feature requires setuptools 60 or later.
+First, ensure that [setuptools-scm] is present during the project's
+build step by specifying it as one of the build requirements.
+
+```toml
+[build-system]
+requires = [
+    "setuptools>=60",
+    "setuptools-scm>=8.0"]
+```
+
+That will be sufficient to require [setuptools-scm] for projects
+that support [PEP 518] like [pip] and [build].
+
+[pip]: https://pypi.org/project/pip
+[build]: https://pypi.org/project/build
+[PEP 518]: https://peps.python.org/pep-0518/
+
+
+To enable version inference, you need to set the version
+dynamically in the `project` section of `pyproject.toml`:
+
+```toml title="pyproject.toml"
+[project]
+# version = "0.0.1"  # Remove any existing version parameter.
+dynamic = ["version"]
+[tool.setuptools_scm]
+```
+
+Additionally, a version file can be written by specifying:
+
+```toml title="pyproject.toml"
+[tool.setuptools_scm]
+version_file = "pkg/_version.py"
+```
+
+Where `pkg` is the name of your package.
+
+If you need to confirm which version string is being generated or debug the configuration,
+you can install [setuptools-scm] directly in your working environment and run:
+
+```console
+$ python -m setuptools_scm
+# To explore other options, try:
+$ python -m setuptools_scm --help
+```
+
+For further configuration see the [documentation].
+
+[setuptools-scm]: https://github.com/pypa/setuptools_scm
+[documentation]: https://setuptools-scm.readthedocs.io/
+
+
+## Interaction with Enterprise Distributions
+
+Some enterprise distributions like RHEL7
+ship rather old setuptools versions.
+
+In those cases its typically possible to build by using an sdist against `setuptools_scm<2.0`.
+As those old setuptools versions lack sensible types for versions,
+modern [setuptools-scm] is unable to support them sensibly.
+
+It's strongly recommended to build a wheel artifact using modern Python and setuptools,
+then installing the artifact instead of trying to run against old setuptools versions.
+
+
+## Code of Conduct
+
+
+Everyone interacting in the [setuptools-scm] project's codebases, issue
+trackers, chat rooms, and mailing lists is expected to follow the
+[PSF Code of Conduct].
+
+[PSF Code of Conduct]: https://github.com/pypa/.github/blob/main/CODE_OF_CONDUCT.md
+
+
+## Security Contact
+
+To report a security vulnerability, please use the
+[Tidelift security contact](https://tidelift.com/security).
+Tidelift will coordinate the fix and disclosure.
diff --git a/README.md b/README.md
new file mode 100644 (file)
index 0000000..fc6d8a3
--- /dev/null
+++ b/README.md
@@ -0,0 +1,103 @@
+# setuptools_scm
+[![github ci](https://github.com/pypa/setuptools_scm/workflows/python%20tests+artifacts+release/badge.svg)](https://github.com/pypa/setuptools_scm/actions)
+[![Documentation Status](https://readthedocs.org/projects/setuptools-scm/badge/?version=latest)](https://setuptools-scm.readthedocs.io/en/latest/?badge=latest)
+[![tidelift](https://tidelift.com/badges/package/pypi/setuptools-scm) ](https://tidelift.com/subscription/pkg/pypi-setuptools-scm?utm_source=pypi-setuptools-scm&utm_medium=readme)
+
+## about
+
+[setuptools-scm] extracts Python package versions from `git` or
+`hg` metadata instead of declaring them as the version argument
+or in an SCM managed file.
+
+Additionally, [setuptools-scm] provides setuptools
+with a list of files that are managed by the SCM <br/>
+(i.e. it automatically adds **all of** the SCM-managed files to the sdist).<br/>
+Unwanted files must be excluded via `MANIFEST.in`.
+
+
+## `pyproject.toml` usage
+
+The preferred way to configure [setuptools-scm] is to author
+settings in a `tool.setuptools_scm` section of `pyproject.toml`.
+
+This feature requires setuptools 60 or later.
+First, ensure that [setuptools-scm] is present during the project's
+build step by specifying it as one of the build requirements.
+
+```toml
+[build-system]
+requires = [
+    "setuptools>=60",
+    "setuptools-scm>=8.0"]
+```
+
+That will be sufficient to require [setuptools-scm] for projects
+that support [PEP 518] like [pip] and [build].
+
+[pip]: https://pypi.org/project/pip
+[build]: https://pypi.org/project/build
+[PEP 518]: https://peps.python.org/pep-0518/
+
+
+To enable version inference, you need to set the version
+dynamically in the `project` section of `pyproject.toml`:
+
+```toml title="pyproject.toml"
+[project]
+# version = "0.0.1"  # Remove any existing version parameter.
+dynamic = ["version"]
+[tool.setuptools_scm]
+```
+
+Additionally, a version file can be written by specifying:
+
+```toml title="pyproject.toml"
+[tool.setuptools_scm]
+version_file = "pkg/_version.py"
+```
+
+Where `pkg` is the name of your package.
+
+If you need to confirm which version string is being generated or debug the configuration,
+you can install [setuptools-scm] directly in your working environment and run:
+
+```console
+$ python -m setuptools_scm
+# To explore other options, try:
+$ python -m setuptools_scm --help
+```
+
+For further configuration see the [documentation].
+
+[setuptools-scm]: https://github.com/pypa/setuptools_scm
+[documentation]: https://setuptools-scm.readthedocs.io/
+
+
+## Interaction with Enterprise Distributions
+
+Some enterprise distributions like RHEL7
+ship rather old setuptools versions.
+
+In those cases its typically possible to build by using an sdist against `setuptools_scm<2.0`.
+As those old setuptools versions lack sensible types for versions,
+modern [setuptools-scm] is unable to support them sensibly.
+
+It's strongly recommended to build a wheel artifact using modern Python and setuptools,
+then installing the artifact instead of trying to run against old setuptools versions.
+
+
+## Code of Conduct
+
+
+Everyone interacting in the [setuptools-scm] project's codebases, issue
+trackers, chat rooms, and mailing lists is expected to follow the
+[PSF Code of Conduct].
+
+[PSF Code of Conduct]: https://github.com/pypa/.github/blob/main/CODE_OF_CONDUCT.md
+
+
+## Security Contact
+
+To report a security vulnerability, please use the
+[Tidelift security contact](https://tidelift.com/security).
+Tidelift will coordinate the fix and disclosure.
diff --git a/README.rst b/README.rst
deleted file mode 100644 (file)
index 074eb6c..0000000
+++ /dev/null
@@ -1,589 +0,0 @@
-setuptools_scm
-===============
-
-``setuptools_scm`` handles managing your Python package versions
-in SCM metadata instead of declaring them as the version argument
-or in a SCM managed file.
-
-Additionally ``setuptools_scm`` provides setuptools with a list of files that are managed by the SCM
-(i.e. it automatically adds all of the SCM-managed files to the sdist).
-Unwanted files must be excluded by discarding them via ``MANIFEST.in``.
-
-.. image:: https://travis-ci.org/pypa/setuptools_scm.svg?branch=master
-    :target: https://travis-ci.org/pypa/setuptools_scm
-
-.. image:: https://tidelift.com/badges/package/pypi/setuptools-scm
-   :target: https://tidelift.com/subscription/pkg/pypi-setuptools-scm?utm_source=pypi-setuptools-scm&utm_medium=readme
-
-
-``pyproject.toml`` usage
-------------------------
-
-The preferred way to configure ``setuptools_scm`` is to author
-settings in a ``tool.setuptools_scm`` section of ``pyproject.toml``.
-
-This feature requires Setuptools 42 or later, released in Nov, 2019.
-If your project needs to support build from sdist on older versions
-of Setuptools, you will need to also implement the ``setup.py usage``
-for those legacy environments.
-
-First, ensure that ``setuptools_scm`` is present during the project's
-built step by specifying it as one of the build requirements.
-
-.. code:: toml
-
-    # pyproject.toml
-    [build-system]
-    requires = ["setuptools>=42", "wheel", "setuptools_scm[toml]>=3.4"]
-
-Note that the ``toml`` extra must be supplied.
-
-That will be sufficient to require ``setuptools_scm`` for projects
-that support PEP 518 (`pip <https://pypi.org/project/pip>`_ and
-`pep517 <https://pypi.org/project/pep517/>`_). Many tools,
-especially those that invoke ``setup.py`` for any reason, may
-continue to rely on ``setup_requires``. For maximum compatibility
-with those uses, consider also including a ``setup_requires`` directive
-(described below in ``setup.py usage`` and ``setup.cfg``).
-
-To enable version inference, add this section to your pyproject.toml:
-
-.. code:: toml
-
-    # pyproject.toml
-    [tool.setuptools_scm]
-
-Including this section is comparable to supplying
-``use_scm_version=True`` in ``setup.py``. Additionally,
-include arbitrary keyword arguments in that section
-to be supplied to ``get_version()``. For example:
-
-.. code:: toml
-
-    # pyproject.toml
-
-    [tool.setuptools_scm]
-    write_to = "pkg/version.py"
-
-
-``setup.py`` usage
-------------------
-
-The following settings are considered legacy behavior and
-superseded by the ``pyproject.toml`` usage, but for maximal
-compatibility, projects may also supply the configuration in
-this older form.
-
-To use ``setuptools_scm`` just modify your project's ``setup.py`` file
-like this:
-
-* Add ``setuptools_scm`` to the ``setup_requires`` parameter.
-* Add the ``use_scm_version`` parameter and set it to ``True``.
-
-For example:
-
-.. code:: python
-
-    from setuptools import setup
-    setup(
-        ...,
-        use_scm_version=True,
-        setup_requires=['setuptools_scm'],
-        ...,
-    )
-
-Arguments to ``get_version()`` (see below) may be passed as a dictionary to
-``use_scm_version``. For example:
-
-.. code:: python
-
-    from setuptools import setup
-    setup(
-        ...,
-        use_scm_version = {
-            "root": "..",
-            "relative_to": __file__,
-            "local_scheme": "node-and-timestamp"
-        },
-        setup_requires=['setuptools_scm'],
-        ...,
-    )
-
-You can confirm the version number locally via ``setup.py``:
-
-.. code-block:: shell
-
-    $ python setup.py --version
-
-.. note::
-
-   If you see unusual version numbers for packages but ``python setup.py
-   --version`` reports the expected version number, ensure ``[egg_info]`` is
-   not defined in ``setup.cfg``.
-
-
-``setup.cfg`` usage
--------------------
-
-If using `setuptools 30.3.0
-<https://setuptools.readthedocs.io/en/latest/setuptools.html#configuring-setup-using-setup-cfg-files>`_
-or greater, you can store ``setup_requires`` configuration in ``setup.cfg``.
-However, ``use_scm_version`` must still be placed in ``setup.py``. For example:
-
-.. code:: python
-
-    # setup.py
-    from setuptools import setup
-    setup(
-        use_scm_version=True,
-    )
-
-.. code:: ini
-
-    # setup.cfg
-    [metadata]
-    ...
-
-    [options]
-    setup_requires =
-      setuptools_scm
-    ...
-
-.. important::
-
-    Ensure neither the ``[metadata]`` ``version`` option nor the ``[egg_info]``
-    section are defined, as these will interfere with ``setuptools_scm``.
-
-You may also need to define a ``pyproject.toml`` file (`PEP-0518
-<https://www.python.org/dev/peps/pep-0518>`_) to ensure you have the required
-version of ``setuptools``:
-
-.. code:: ini
-
-    # pyproject.toml
-    [build-system]
-    requires = ["setuptools>=30.3.0", "wheel", "setuptools_scm"]
-
-For more information, refer to the `setuptools issue #1002
-<https://github.com/pypa/setuptools/issues/1002>`_.
-
-
-Programmatic usage
-------------------
-
-In order to use ``setuptools_scm`` from code that is one directory deeper
-than the project's root, you can use:
-
-.. code:: python
-
-    from setuptools_scm import get_version
-    version = get_version(root='..', relative_to=__file__)
-
-See `setup.py Usage`_ above for how to use this within ``setup.py``.
-
-
-Retrieving package version at runtime
--------------------------------------
-
-If you have opted not to hardcode the version number inside the package,
-you can retrieve it at runtime from PEP-0566_ metadata using
-``importlib.metadata`` from the standard library
-or the `importlib_metadata`_ backport:
-
-.. code:: python
-
-    from importlib.metadata import version, PackageNotFoundError
-
-    try:
-        __version__ = version(__name__)
-    except PackageNotFoundError:
-        # package is not installed
-       pass
-
-Alternatively, you can use ``pkg_resources`` which is included in
-``setuptools``:
-
-.. code:: python
-
-   from pkg_resources import get_distribution, DistributionNotFound
-
-   try:
-       __version__ = get_distribution(__name__).version
-   except DistributionNotFound:
-        # package is not installed
-       pass
-
-This does place a runtime dependency on ``setuptools``.
-
-.. _PEP-0566: https://www.python.org/dev/peps/pep-0566/
-.. _importlib_metadata: https://pypi.org/project/importlib-metadata/
-
-
-Usage from Sphinx
------------------
-
-It is discouraged to use ``setuptools_scm`` from Sphinx itself,
-instead use ``pkg_resources`` after editable/real installation:
-
-.. code:: python
-
-    # contents of docs/conf.py
-    from pkg_resources import get_distribution
-    release = get_distribution('myproject').version
-    # for example take major/minor
-    version = '.'.join(release.split('.')[:2])
-
-The underlying reason is, that services like *Read the Docs* sometimes change
-the working directory for good reasons and using the installed metadata
-prevents using needless volatile data there.
-
-Notable Plugins
-----------------
-
-`setuptools_scm_git_archive <https://pypi.python.org/pypi/setuptools_scm_git_archive>`_
-    Provides partial support for obtaining versions from git archives that
-    belong to tagged versions. The only reason for not including it in
-    ``setuptools_scm`` itself is Git/GitHub not supporting sufficient metadata
-    for untagged/followup commits, which is preventing a consistent UX.
-
-
-Default versioning scheme
---------------------------
-
-In the standard configuration ``setuptools_scm`` takes a look at three things:
-
-1. latest tag (with a version number)
-2. the distance to this tag (e.g. number of revisions since latest tag)
-3. workdir state (e.g. uncommitted changes since latest tag)
-
-and uses roughly the following logic to render the version:
-
-no distance and clean:
-    ``{tag}``
-distance and clean:
-    ``{next_version}.dev{distance}+{scm letter}{revision hash}``
-no distance and not clean:
-    ``{tag}+dYYYYMMDD``
-distance and not clean:
-    ``{next_version}.dev{distance}+{scm letter}{revision hash}.dYYYYMMDD``
-
-The next version is calculated by adding ``1`` to the last numeric component of
-the tag.
-
-For Git projects, the version relies on `git describe <https://git-scm.com/docs/git-describe>`_,
-so you will see an additional ``g`` prepended to the ``{revision hash}``.
-
-Semantic Versioning (SemVer)
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-Due to the default behavior it's necessary to always include a
-patch version (the ``3`` in ``1.2.3``), or else the automatic guessing
-will increment the wrong part of the SemVer (e.g. tag ``2.0`` results in
-``2.1.devX`` instead of ``2.0.1.devX``). So please make sure to tag
-accordingly.
-
-.. note::
-
-    Future versions of ``setuptools_scm`` will switch to `SemVer
-    <http://semver.org/>`_ by default hiding the the old behavior as an
-    configurable option.
-
-
-Builtin mechanisms for obtaining version numbers
---------------------------------------------------
-
-1. the SCM itself (git/hg)
-2. ``.hg_archival`` files (mercurial archives)
-3. ``PKG-INFO``
-
-.. note::
-
-    Git archives are not supported due to Git shortcomings
-
-
-File finders hook makes most of MANIFEST.in unnecessary
--------------------------------------------------------
-
-``setuptools_scm`` implements a `file_finders
-<https://setuptools.readthedocs.io/en/latest/setuptools.html#adding-support-for-revision-control-systems>`_
-entry point which returns all files tracked by your SCM. This eliminates
-the need for a manually constructed ``MANIFEST.in`` in most cases where this
-would be required when not using ``setuptools_scm``, namely:
-
-* To ensure all relevant files are packaged when running the ``sdist`` command.
-
-* When using `include_package_data <https://setuptools.readthedocs.io/en/latest/setuptools.html#including-data-files>`_
-  to include package data as part of the ``build`` or ``bdist_wheel``.
-
-``MANIFEST.in`` may still be used: anything defined there overrides the hook.
-This is mostly useful to exclude files tracked in your SCM from packages,
-although in principle it can be used to explicitly include non-tracked files
-too.
-
-
-Configuration parameters
-------------------------
-
-In order to configure the way ``use_scm_version`` works you can provide
-a mapping with options instead of a boolean value.
-
-The currently supported configuration keys are:
-
-:root:
-    Relative path to cwd, used for finding the SCM root; defaults to ``.``
-
-:version_scheme:
-    Configures how the local version number is constructed; either an
-    entrypoint name or a callable.
-
-:local_scheme:
-    Configures how the local component of the version is constructed; either an
-    entrypoint name or a callable.
-
-:write_to:
-    A path to a file that gets replaced with a file containing the current
-    version. It is ideal for creating a ``version.py`` file within the
-    package, typically used to avoid using `pkg_resources.get_distribution`
-    (which adds some overhead).
-
-    .. warning::
-
-      Only files with :code:`.py` and :code:`.txt` extensions have builtin
-      templates, for other file types it is necessary to provide
-      :code:`write_to_template`.
-
-:write_to_template:
-    A newstyle format string that is given the current version as
-    the ``version`` keyword argument for formatting.
-
-:relative_to:
-    A file from which the root can be resolved.
-    Typically called by a script or module that is not in the root of the
-    repository to point ``setuptools_scm`` at the root of the repository by
-    supplying ``__file__``.
-
-:tag_regex:
-   A Python regex string to extract the version part from any SCM tag.
-    The regex needs to contain either a single match group, or a group
-    named ``version``, that captures the actual version information.
-
-    Defaults to the value of ``setuptools_scm.config.DEFAULT_TAG_REGEX``
-    (see `config.py <src/setuptools_scm/config.py>`_).
-
-:parentdir_prefix_version:
-    If the normal methods for detecting the version (SCM version,
-    sdist metadata) fail, and the parent directory name starts with
-    ``parentdir_prefix_version``, then this prefix is stripped and the rest of
-    the parent directory name is matched with ``tag_regex`` to get a version
-    string.  If this parameter is unset (the default), then this fallback is
-    not used.
-
-    This is intended to cover GitHub's "release tarballs", which extract into
-    directories named ``projectname-tag/`` (in which case
-    ``parentdir_prefix_version`` can be set e.g. to ``projectname-``).
-
-:fallback_version:
-    A version string that will be used if no other method for detecting the
-    version worked (e.g., when using a tarball with no metadata). If this is
-    unset (the default), setuptools_scm will error if it fails to detect the
-    version.
-
-:parse:
-    A function that will be used instead of the discovered SCM for parsing the
-    version.
-    Use with caution, this is a function for advanced use, and you should be
-    familiar with the ``setuptools_scm`` internals to use it.
-
-:git_describe_command:
-    This command will be used instead the default ``git describe`` command.
-    Use with caution, this is a function for advanced use, and you should be
-    familiar with the ``setuptools_scm`` internals to use it.
-
-    Defaults to the value set by ``setuptools_scm.git.DEFAULT_DESCRIBE``
-    (see `git.py <src/setuptools_scm/git.py>`_).
-
-To use ``setuptools_scm`` in other Python code you can use the ``get_version``
-function:
-
-.. code:: python
-
-    from setuptools_scm import get_version
-    my_version = get_version()
-
-It optionally accepts the keys of the ``use_scm_version`` parameter as
-keyword arguments.
-
-Example configuration in ``setup.py`` format:
-
-.. code:: python
-
-    from setuptools import setup
-
-    setup(
-        use_scm_version={
-            'write_to': 'version.py',
-            'write_to_template': '__version__ = "{version}"',
-            'tag_regex': r'^(?P<prefix>v)?(?P<version>[^\+]+)(?P<suffix>.*)?$',
-        }
-    )
-
-Environment variables
----------------------
-
-:SETUPTOOLS_SCM_PRETEND_VERSION:
-    when defined and not empty,
-    its used as the primary source for the version number
-    in which case it will be a unparsed string
-
-:SETUPTOOLS_SCM_DEBUG:
-    when defined and not empty,
-    a lot of debug information will be printed as part of ``setuptools_scm``
-    operating
-
-Extending setuptools_scm
-------------------------
-
-``setuptools_scm`` ships with a few ``setuptools`` entrypoints based hooks to
-extend its default capabilities.
-
-Adding a new SCM
-~~~~~~~~~~~~~~~~
-
-``setuptools_scm`` provides two entrypoints for adding new SCMs:
-
-``setuptools_scm.parse_scm``
-    A function used to parse the metadata of the current workdir
-    using the name of the control directory/file of your SCM as the
-    entrypoint's name. E.g. for the built-in entrypoint for git the
-    entrypoint is named ``.git`` and references ``setuptools_scm.git:parse``
-
-  The return value MUST be a ``setuptools_scm.version.ScmVersion`` instance
-  created by the function ``setuptools_scm.version:meta``.
-
-``setuptools_scm.files_command``
-  Either a string containing a shell command that prints all SCM managed
-  files in its current working directory or a callable, that given a
-  pathname will return that list.
-
-  Also use then name of your SCM control directory as name of the entrypoint.
-
-Version number construction
-~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-``setuptools_scm.version_scheme``
-    Configures how the version number is constructed given a
-    ``setuptools_scm.version.ScmVersion`` instance and should return a string
-    representing the version.
-
-    Available implementations:
-
-    :guess-next-dev: Automatically guesses the next development version (default).
-        Guesses the upcoming release by incrementing the pre-release segment if present,
-        otherwise by incrementing the micro segment. Then appends :code:`.devN`.
-    :post-release: generates post release versions (adds :code:`.postN`)
-    :python-simplified-semver: Basic semantic versioning. Guesses the upcoming release
-        by incrementing the minor segment and setting the micro segment to zero if the
-        current branch contains the string ``'feature'``, otherwise by incrementing the
-        micro version. Then appends :code:`.devN`. Not compatible with pre-releases.
-    :release-branch-semver: Semantic versioning for projects with release branches. The
-        same as ``guess-next-dev`` (incrementing the pre-release or micro segment) if on
-        a release branch: a branch whose name (ignoring namespace) parses as a version
-        that matches the most recent tag up to the minor segment. Otherwise if on a
-        non-release branch, increments the minor segment and sets the micro segment to
-        zero, then appends :code:`.devN`.
-
-``setuptools_scm.local_scheme``
-    Configures how the local part of a version is rendered given a
-    ``setuptools_scm.version.ScmVersion`` instance and should return a string
-    representing the local version.
-    Dates and times are in Coordinated Universal Time (UTC), because as part
-    of the version, they should be location independent.
-
-    Available implementations:
-
-    :node-and-date: adds the node on dev versions and the date on dirty
-                    workdir (default)
-    :node-and-timestamp: like ``node-and-date`` but with a timestamp of
-                         the form ``{:%Y%m%d%H%M%S}`` instead
-    :dirty-tag: adds ``+dirty`` if the current workdir has changes
-    :no-local-version: omits local version, useful e.g. because pypi does
-                       not support it
-
-
-Importing in ``setup.py``
-~~~~~~~~~~~~~~~~~~~~~~~~~
-
-To support usage in ``setup.py`` passing a callable into ``use_scm_version``
-is supported.
-
-Within that callable, ``setuptools_scm`` is available for import.
-The callable must return the configuration.
-
-
-.. code:: python
-
-    # content of setup.py
-    import setuptools
-
-    def myversion():
-        from setuptools_scm.version import get_local_dirty_tag
-        def clean_scheme(version):
-            return get_local_dirty_tag(version) if version.dirty else '+clean'
-
-        return {'local_scheme': clean_scheme}
-
-    setup(
-        ...,
-        use_scm_version=myversion,
-        ...
-    )
-
-
-Note on testing non-installed versions
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-While the general advice is to test against a installed version,
-some environments require a test prior to install,
-
-.. code::
-
-  $ python setup.py egg_info
-  $ PYTHONPATH=$PWD:$PWD/src pytest
-
-
-Interaction with Enterprise Distributions
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-Some enterprise distributions like RHEL7 and others
-ship rather old setuptools versions due to various release management details.
-
-On such distributions one might observe errors like:
-
-:code:``setuptools_scm.version.SetuptoolsOutdatedWarning: your setuptools is too old (<12)``
-
-In those case its typically possible to build by using a sdist against ``setuptools_scm<2.0``.
-As those old setuptools versions lack sensible types for versions,
-modern setuptools_scm is unable to support them sensibly.
-
-In case the project you need to build can not be patched to either use old setuptools_scm,
-its still possible to install a more recent version of setuptools in order to handle the build
-and/or install the package by using wheels or eggs.
-
-
-
-
-Code of Conduct
----------------
-
-Everyone interacting in the ``setuptools_scm`` project's codebases, issue
-trackers, chat rooms, and mailing lists is expected to follow the
-`PyPA Code of Conduct`_.
-
-.. _PyPA Code of Conduct: https://www.pypa.io/en/latest/code-of-conduct/
-
-Security Contact
-================
-
-To report a security vulnerability, please use the
-`Tidelift security contact <https://tidelift.com/security>`_.
-Tidelift will coordinate the fix and disclosure.
diff --git a/_own_version_helper.py b/_own_version_helper.py
new file mode 100644 (file)
index 0000000..9ef23f5
--- /dev/null
@@ -0,0 +1,66 @@
+"""
+this module is a hack only in place to allow for setuptools
+to use the attribute for the versions
+
+it works only if the backend-path of the build-system section
+from pyproject.toml is respected
+"""
+from __future__ import annotations
+
+import logging
+from typing import Callable
+
+from setuptools import build_meta as build_meta  # noqa
+
+from setuptools_scm import _types as _t
+from setuptools_scm import Configuration
+from setuptools_scm import get_version
+from setuptools_scm import git
+from setuptools_scm import hg
+from setuptools_scm.fallbacks import parse_pkginfo
+from setuptools_scm.version import get_local_node_and_date
+from setuptools_scm.version import guess_next_dev_version
+from setuptools_scm.version import ScmVersion
+
+log = logging.getLogger("setuptools_scm")
+# todo: take fake entrypoints from pyproject.toml
+try_parse: list[Callable[[_t.PathT, Configuration], ScmVersion | None]] = [
+    parse_pkginfo,
+    git.parse,
+    hg.parse,
+    git.parse_archival,
+    hg.parse_archival,
+]
+
+
+def parse(root: str, config: Configuration) -> ScmVersion | None:
+    for maybe_parse in try_parse:
+        try:
+            parsed = maybe_parse(root, config)
+        except OSError as e:
+            log.warning("parse with %s failed with: %s", maybe_parse, e)
+        else:
+            if parsed is not None:
+                return parsed
+    else:
+        return None
+
+
+def scm_version() -> str:
+    return get_version(
+        relative_to=__file__,
+        parse=parse,
+        version_scheme=guess_next_dev_version,
+        local_scheme=get_local_node_and_date,
+    )
+
+
+version: str
+
+
+def __getattr__(name: str) -> str:
+    if name == "version":
+        global version
+        version = scm_version()
+        return version
+    raise AttributeError(name)
diff --git a/docs/config.md b/docs/config.md
new file mode 100644 (file)
index 0000000..0acdf6f
--- /dev/null
@@ -0,0 +1,156 @@
+# Configuration
+
+
+## configuration parameters
+
+Configuration parameters can be configured in `pyproject.toml` or `setup.py`.
+Callables or other Python objects have to be passed in `setup.py` (via the `use_scm_version` keyword argument).
+
+
+`root : Path | PathLike[str]`
+: Relative path to the SCM root, defaults to `.` and is relative to the file path passed in `relative_to`
+
+`version_scheme : str | Callable[[ScmVersion], str]`
+: Configures how the local version number is constructed; either an entrypoint name or a callable.
+
+`local_scheme : str | Callable[[ScmVersion], str]`
+: Configures how the local component of the version is constructed
+  either an entrypoint name or a callable.
+
+
+`version_file: Path | PathLike[str] | None = None`
+:   A path to a file that gets replaced with a file containing the current
+    version. It is ideal for creating a ``_version.py`` file within the
+    package, typically used to avoid using `pkg_resources.get_distribution`
+    (which adds some overhead).
+
+    !!! warning ""
+
+        Only files with `.py` and `.txt` extensions have builtin templates,
+        for other file types it is necessary to provide `write_to_template`.
+
+`version_file_template_template: str | None = None`
+:   A new-style format string that is given the current version as
+    the `version` keyword argument for formatting.
+
+`write_to: Pathlike[str] | Path | None = None`
+:  (deprecated) legacy option to create a version file relative to the scm root
+   it's broken for usage from a sdist and fixing it would be a fatal breaking change,
+   use `version_file` instead.
+
+`relative_to: Path|Pathlike[str] = "pyproject.toml"`
+:   A file/directory from which the root can be resolved.
+    Typically called by a script or module that is not in the root of the
+    repository to point `setuptools_scm` at the root of the repository by
+    supplying `__file__`.
+
+`tag_regex: str|Pattern[str]`
+:   A Python regex string to extract the version part from any SCM tag.
+    The regex needs to contain either a single match group, or a group
+    named `version`, that captures the actual version information.
+
+    Defaults to the value of [setuptools_scm._config.DEFAULT_TAG_REGEX][]
+
+`parentdir_prefix_version: str|None = None`
+:   If the normal methods for detecting the version (SCM version,
+    sdist metadata) fail, and the parent directory name starts with
+    `parentdir_prefix_version`, then this prefix is stripped and the rest of
+    the parent directory name is matched with `tag_regex` to get a version
+    string.  If this parameter is unset (the default), then this fallback is
+    not used.
+
+    This was intended to cover GitHub's "release tarballs",
+    which extract into directories named `projectname-tag/`
+    (in which case `parentdir_prefix_version` can be set e.g. to `projectname-`).
+
+`fallback_version: str | None = None`
+ :  A version string that will be used if no other method for detecting the
+    version worked (e.g., when using a tarball with no metadata). If this is
+    unset (the default), `setuptools_scm` will error if it fails to detect the
+    version.
+
+`parse: Callable[[Path, Config], ScmVersion] | None = None`
+:   A function that will be used instead of the discovered SCM
+    for parsing the version. Use with caution,
+    this is a function for advanced use and you should be
+    familiar with the `setuptools_scm` internals to use it.
+
+`git_describe_command`
+:   This command will be used instead the default `git describe --long` command.
+
+    Defaults to the value set by [setuptools_scm.git.DEFAULT_DESCRIBE][]
+
+`normalize`
+:   A boolean flag indicating if the version string should be normalized.
+    Defaults to `True`. Setting this to `False` is equivalent to setting
+    `version_cls` to [setuptools_scm.NonNormalizedVersion][]
+
+`version_cls: type|str = packaging.version.Version`
+:   An optional class used to parse, verify and possibly normalize the version
+    string. Its constructor should receive a single string argument, and its
+    `str` should return the normalized version string to use.
+    This option can also receive a class qualified name as a string.
+
+    The [setuptools_scm.NonNormalizedVersion][] convenience class is
+    provided to disable the normalization step done by
+    `packaging.version.Version`. If this is used while `setuptools_scm`
+    is integrated in a setuptools packaging process, the non-normalized
+    version number will appear in all files (see `version_file` note).
+
+    !!! note "normalization still applies to artifact filenames"
+        Setuptools will still normalize it to create the final distribution,
+        so as to stay compliant with the python packaging standards.
+
+
+## environment variables
+
+`SETUPTOOLS_SCM_PRETEND_VERSION`
+:   used as the primary source for the version number
+    in which case it will be an unparsed string
+
+    !!! warning "it is strongly recommended to use use distribution name specific pretend versions"
+
+
+`SETUPTOOLS_SCM_PRETEND_VERSION_FOR_${NORMALIZED_DIST_NAME}`
+:   used as the primary source for the version number
+    in which case it will be an unparsed string
+
+    the dist name normalization follows adapted PEP 503 semantics, with one or
+    more of ".-_" being replaced by a single "_", and the name being upper-cased
+
+    it takes precedence over ``SETUPTOOLS_SCM_PRETEND_VERSION``
+
+`SETUPTOOLS_SCM_DEBUG`
+:    enable the debug logging
+
+`SOURCE_DATE_EPOCH`
+:   used as the timestamp from which the
+    ``node-and-date`` and ``node-and-timestamp`` local parts are
+    derived, otherwise the current time is used
+    (https://reproducible-builds.org/docs/source-date-epoch/)
+
+`SETUPTOOLS_SCM_IGNORE_VCS_ROOTS`
+:   a ``os.pathsep`` separated list
+    of directory names to ignore for root finding
+
+
+
+
+
+## api reference
+
+### constants
+
+::: setuptools_scm._config.DEFAULT_TAG_REGEX
+    options:
+      heading_level: 4
+
+::: setuptools_scm.git.DEFAULT_DESCRIBE
+    options:
+      heading_level: 4
+
+
+### the configuration class
+::: setuptools_scm.Configuration
+    options:
+      heading_level: 4
diff --git a/docs/customizing.md b/docs/customizing.md
new file mode 100644 (file)
index 0000000..9236531
--- /dev/null
@@ -0,0 +1,77 @@
+# Customizing
+
+## providing project local version schemes
+
+As PEP 621 provides no way to specify local code as a build backend plugin,
+setuptools_scm has to piggyback on setuptools for passing functions over.
+
+To facilitate that one needs to write a `setup.py` file and
+pass partial setuptools_scm configuration in via the use_scm_version keyword.
+
+It's strongly recommended to experiment with using stock version schemes or creating plugins as package.
+(This recommendation will change if there ever is something like build-time entrypoints).
+
+
+``` { .python title="setup.py" file="docs/examples/version_scheme_code/setup.py" }
+# we presume installed build dependencies
+from __future__ import annotations
+
+from setuptools import setup
+
+from setuptools_scm import ScmVersion
+
+
+def myversion_func(version: ScmVersion) -> str:
+    from setuptools_scm.version import guess_next_version
+
+    return version.format_next_version(guess_next_version, "{guessed}b{distance}")
+
+
+setup(use_scm_version={"version_scheme": myversion_func})
+```
+
+
+``` { .python title="pyproject.toml" file="docs/examples/version_scheme_code/pyproject.toml" }
+[build-system]
+requires = [
+  "setuptools>=64",
+  "setuptools_scm>=8",
+  "wheel",
+]
+
+[project]
+name = "scm-example"
+dynamic = [
+  "version",
+]
+
+[tool.setuptools_scm]
+```
+
+- [ ] add a build block that adds example output
+- [ ] correct config after [entangled mkdocs bug] is fixed
+
+[entangled mkdocs bug]: https://github.com/entangled/mkdocs-plugin/issues/1
+
+
+
+
+##  Importing in setup.py
+
+With the pep 517/518 build backend, setuptools_scm is importable from `setup.py`
+
+``` { .python title="setup.py" }
+import setuptools
+from setuptools_scm.version import get_local_dirty_tag
+
+def clean_scheme(version):
+    return get_local_dirty_tag(version) if version.dirty else '+clean'
+
+setup(use_scm_version={'local_scheme': clean_scheme})
+```
+
+
+
+## alternative version classes
+
+::: setuptools_scm.NonNormalizedVersion
diff --git a/docs/examples/version_scheme_code/pyproject.toml b/docs/examples/version_scheme_code/pyproject.toml
new file mode 100644 (file)
index 0000000..05da2f6
--- /dev/null
@@ -0,0 +1,16 @@
+# ~/~ begin <<docs/customizing.md#docs/examples/version_scheme_code/pyproject.toml>>[init]
+[build-system]
+requires = [
+  "setuptools>=64",
+  "setuptools_scm>=8",
+  "wheel",
+]
+
+[project]
+name = "scm-example"
+dynamic = [
+  "version",
+]
+
+[tool.setuptools_scm]
+# ~/~ end
\ No newline at end of file
diff --git a/docs/examples/version_scheme_code/setup.py b/docs/examples/version_scheme_code/setup.py
new file mode 100644 (file)
index 0000000..69f903f
--- /dev/null
@@ -0,0 +1,17 @@
+# ~/~ begin <<docs/customizing.md#docs/examples/version_scheme_code/setup.py>>[init]
+# we presume installed build dependencies
+from __future__ import annotations
+
+from setuptools import setup
+
+from setuptools_scm import ScmVersion
+
+
+def myversion_func(version: ScmVersion) -> str:
+    from setuptools_scm.version import guess_next_version
+
+    return version.format_next_version(guess_next_version, "{guessed}b{distance}")
+
+
+setup(use_scm_version={"version_scheme": myversion_func})
+# ~/~ end
diff --git a/docs/extending.md b/docs/extending.md
new file mode 100644 (file)
index 0000000..957c762
--- /dev/null
@@ -0,0 +1,107 @@
+# Extending setuptools_scm
+
+`setuptools_scm` uses [entry-point][entry-point] based hooks to extend its default capabilities.
+
+[entry-point]: https://packaging.python.org/en/latest/specifications/entry-points/
+
+## Adding a new SCM
+
+`setuptools_scm` provides two entrypoints for adding new SCMs:
+
+`setuptools_scm.parse_scm`
+:   A function used to parse the metadata of the current workdir
+    using the name of the control directory/file of your SCM as the
+    entrypoint's name. E.g. for the built-in entrypoint for Git the
+    entrypoint is named `.git` and references `setuptools_scm.git:parse`
+
+    The return value MUST be a [`setuptools_scm.version.ScmVersion`][] instance
+    created by the function [`setuptools_scm.version.meta`][].
+
+`setuptools_scm.files_command`
+:  Either a string containing a shell command that prints all SCM managed
+   files in its current working directory or a callable, that given a
+   pathname will return that list.
+
+   Also uses then name of your SCM control directory as name of the entrypoint.
+
+
+### api reference for scm version objects
+
+::: setuptools_scm.version.ScmVersion
+    options:
+      show_root_heading: yes
+      heading_level: 4
+
+::: setuptools_scm.version.meta
+    options:
+      show_root_heading: yes
+      heading_level: 4
+
+## Version number construction
+
+
+
+
+
+### `setuptools_scm.version_scheme`
+Configures how the version number is constructed given a
+[ScmVersion][setuptools_scm.version.ScmVersion] instance and should return a string
+representing the version.
+
+### Available implementations
+
+`guess-next-dev (default)`
+:   Automatically guesses the next development version (default).
+    Guesses the upcoming release by incrementing the pre-release segment if present,
+    otherwise by incrementing the micro segment. Then appends :code:`.devN`.
+    In case the tag ends with `.dev0` the version is not bumped
+    and custom `.devN` versions will trigger a error.
+
+`post-release (deprecated)`
+:   Generates post release versions (adds `.postN`)
+    after review of the version number pep this is considered a bad idea
+    as post releases are intended to be chosen not autogenerated.
+
+    !!! warning "the recommended replacement is `no-guess-dev`"
+
+`python-simplified-semver`
+:   Basic semantic versioning.
+
+    Guesses the upcoming release by incrementing the minor segment
+    and setting the micro segment to zero if the current branch contains the string `feature`,
+    otherwise by incrementing the micro version. Then appending `.devN`.
+
+    This scheme is not compatible with pre-releases.
+
+`release-branch-semver`
+:   Semantic versioning for projects with release branches.
+    The same as `guess-next-dev` (incrementing the pre-release or micro segment)
+    however when on a release branch: a branch whose name (ignoring namespace) parses as a version
+    that matches the most recent tag up to the minor segment. Otherwise if on a
+    non-release branch, increments the minor segment and sets the micro segment to
+    zero, then appends `.devN`
+
+`no-guess-dev`
+: Does no next version guessing, just adds `.post1.devN`
+
+
+### `setuptools_scm.local_scheme`
+Configures how the local part of a version is rendered given a
+[ScmVersion][setuptools_scm.version.ScmVersion] instance and should return a string
+representing the local version.
+Dates and times are in Coordinated Universal Time (UTC), because as part
+of the version, they should be location independent.
+
+#### Available implementations
+
+`node-and-date (default)`
+: adds the node on dev versions and the date on dirty workdir
+
+`node-and-timestamp`
+: like `node-and-date` but with a timestamp of the form `%Y%m%d%H%M%S` instead
+
+`dirty-tag`
+: adds `+dirty` if the current workdir has changes
+
+`no-local-version`
+: omits local version, useful e.g. because pypi does not support it
diff --git a/docs/index.md b/docs/index.md
new file mode 100644 (file)
index 0000000..d1c3995
--- /dev/null
@@ -0,0 +1,44 @@
+# About
+
+
+`setuptools_scm` extracts Python package versions from `git` or `hg` metadata
+instead of declaring them as the version argument or in a SCM managed file.
+
+Additionally `setuptools_scm` provides setuptools with a list of
+files that are managed by the SCM (i.e. it automatically adds all
+the SCM-managed files to the sdist). Unwanted files must be excluded
+via `MANIFEST.in`.
+
+## basic usage
+
+### with setuptools
+
+Note: `setuptools_scm>=8` intentionally doesn't depend on setuptools to ease non-setuptools usage.
+Please ensure a recent version of setuptools (>=64) is installed.
+
+
+```toml
+# pyproject.toml
+[build-system]
+requires = [
+    "setuptools>=64",
+    "setuptools_scm>=8",
+    "wheel",
+]
+[project]
+name = "example"
+# Important: Remove any existing version declaration
+# version = "0.0.1"
+dynamic = ["version"]
+# more missing
+
+[tool.setuptools_scm]
+
+```
+
+
+### with hatch
+
+[Hatch-vcs](https://github.com/ofek/hatch-vcs) integrates with setuptools_scm
+but provides its own configuration options,
+please see its [documentation](https://github.com/ofek/hatch-vcs#readme)
diff --git a/docs/overrides.md b/docs/overrides.md
new file mode 100644 (file)
index 0000000..5114a84
--- /dev/null
@@ -0,0 +1,16 @@
+# Overrides
+
+## pretend versions
+
+setuptools_scm provides a mechanism to override the version number build time.
+
+the environment variable `SETUPTOOLS_SCM_PRETEND_VERSION` is used
+as the override source for the version number unparsed string.
+
+to be specific about the package this applies for, one can use `SETUPTOOLS_SCM_PRETEND_VERSION_FOR_${NORMALIZED_DIST_NAME}`
+where the dist name normalization follows adapted PEP 503 semantics.
+
+## config overrides
+
+setuptools_scm parses the environment variable `SETUPTOOLS_SCM_OVERRIDES_FOR_${NORMALIZED_DIST_NAME}`
+as a toml inline map to override the configuration data from `pyproject.toml`.
diff --git a/docs/usage.md b/docs/usage.md
new file mode 100644 (file)
index 0000000..8392796
--- /dev/null
@@ -0,0 +1,275 @@
+# Usage
+
+## at build time
+
+The preferred way to configure `setuptools_scm` is to author
+settings in the `tool.setuptools_scm` section of `pyproject.toml`.
+
+It's necessary to use a setuptools version released after 2022.
+
+```toml
+# pyproject.toml
+[build-system]
+requires = ["setuptools>=62", "setuptools_scm[toml]>=8.0"]
+
+[project]
+# version = "0.0.1"  # Remove any existing version parameter.
+dynamic = ["version"]
+
+[tool.setuptools_scm]
+# can be empty if no extra settings are needed, presence enables setuptools_scm
+```
+
+That will be sufficient to require `setuptools_scm` for projects
+that support PEP 518 ([pip](https://pypi.org/project/pip) and
+[pep517](https://pypi.org/project/pep517/)).
+Tools that still invoke `setup.py` must ensure build requirements are installed
+
+### version files
+
+```toml
+# pyproject.toml
+...
+[tool.setuptools_scm]
+version_file = "pkg/_version.py"
+```
+Where ``pkg`` is the name of your package.
+
+
+.. code-block:: shell
+
+    $ python -m setuptools_scm
+
+    # To explore other options, try:
+    $ python -m setuptools_scm --help
+
+
+## as cli tool
+
+If you need to confirm which version string is being generated
+or debug the configuration, you can install
+[setuptools-scm](https://github.com/pypa/setuptools_scm)
+directly in your working environment and run:
+
+```commandline
+$ python -m setuptools_scm # example from running local after changes
+7.1.1.dev149+g5197d0f.d20230727
+```
+
+ and to list all tracked by the scm:
+
+```commandline
+$ python -m setuptools_scm ls # output trimmed for brevity
+./LICENSE
+...
+./src/setuptools_scm/__init__.py
+./src/...
+...
+```
+
+!!! note "committed files only"
+
+    currently only committed files are listed, this might change in the future
+
+!!! warning "sdists/archives don't provide file lists"
+
+    currently there is no builtin mechanism
+    to safely transfer the file lists to sdists or obtaining them from archives
+    coordination for setuptools and hatch is ongoing
+
+## at runtime (strongly discouraged)
+
+the most simple **looking** way to use `setuptools_scm` at runtime is:
+
+```python
+from setuptools_scm import get_version
+version = get_version()
+```
+
+
+In order to use `setuptools_scm` from code that is one directory deeper
+than the project's root, you can use:
+
+```python
+from setuptools_scm import get_version
+version = get_version(root='..', relative_to=__file__)
+```
+
+
+## Python package metadata
+
+
+
+
+### version at runtime
+
+If you have opted not to hardcode the version number inside the package,
+you can retrieve it at runtime from PEP-0566_ metadata using
+``importlib.metadata`` from the standard library (added in Python 3.8)
+or the `importlib_metadata`_ backport:
+
+```python
+# contents of package_name/__init__.py
+from importlib.metadata import version, PackageNotFoundError
+
+try:
+    __version__ = version("package-name")
+except PackageNotFoundError:
+    # package is not installed
+    pass
+```
+
+.. _PEP-0566: https://www.python.org/dev/peps/pep-0566/
+.. _importlib_metadata: https://pypi.org/project/importlib-metadata/
+
+
+### Usage from Sphinx
+
+
+``` {.python file=docs/.entangled/sphinx_conf.py}
+from importlib.metadata import version as get_version
+release: str = get_version('setuptools_scm')
+# for example take major/minor
+version: str = ".".join(release.split('.')[:2])
+```
+
+The underlying reason is that services like *Read the Docs* sometimes change
+the working directory for good reasons and using the installed metadata
+prevents using needless volatile data there.
+
+
+## with Docker/Podman
+
+
+By default, Docker will not copy the `.git`  folder into your container.
+Therefore, builds with version inference might fail.
+Consequently, you can use the following snippet to infer the version from
+the host OS without copying the entire `.git` folder to your `Dockerfile`.
+
+```dockerfile
+RUN --mount=source=.git,target=.git,type=bind \
+    pip install --no-cache-dir -e .
+```
+However, this build step introduces a dependency to the state of your local
+`.git` folder the build cache and triggers the long-running pip install process on every build.
+To optimize build caching, one can use an environment variable to pretend a pseudo
+version that is used to cache the results of the pip install process:
+
+
+```dockerfile
+FROM python
+COPY pyproject.toml
+ARG PSEUDO_VERSION=1 # strongly recommended to update based on git describe
+RUN SETUPTOOLS_SCM_PRETEND_VERSION_FOR_MY_PACKAGE=${PSEUDO_VERSION} pip install -e .[test]
+RUN --mount=source=.git,target=.git,type=bind pip install -e .
+```
+
+Note that running this Dockerfile requires docker with BuildKit enabled
+[docs](https://github.com/moby/buildkit/blob/v0.8.3/frontend/dockerfile/docs/syntax.md).
+
+To avoid BuildKit and mounting of the .git folder altogether, one can also pass the desired
+version as a build argument.
+Note that `SETUPTOOLS_SCM_PRETEND_VERSION_FOR_${NORMALIZED_DIST_NAME}`
+is preferred over `SETUPTOOLS_SCM_PRETEND_VERSION`.
+
+
+
+## Default versioning scheme
+
+In the standard configuration `setuptools_scm` takes a look at three things:
+
+1. latest tag (with a version number)
+2. the distance to this tag (e.g. number of revisions since latest tag)
+3. workdir state (e.g. uncommitted changes since latest tag)
+
+and uses roughly the following logic to render the version:
+
+
+| distance | state     | format                                                               |
+|----------|-----------|----------------------------------------------------------------------|
+| no       | unchanged | `{tag}`                                                              |
+| yes      | unchanged | `{next_version}.dev{distance}+{scm letter}{revision hash}`           |
+| no       | changed   | `{tag}+dYYYYMMDD`                                                    |
+| yes      | changed   | `{next_version}.dev{distance}+{scm letter}{revision hash}.dYYYYMMDD` |
+
+where `{next_version}` is the next version number after the latest tag
+
+The next version is calculated by adding `1` to the last numeric component of
+the tag.
+
+For Git projects, the version relies on  [git describe](https://git-scm.com/docs/git-describe),
+so you will see an additional `g` prepended to the `{revision hash}`.
+
+
+!!! note
+
+    According to [PEP 440](https://peps.python.org/pep-0440/#local-version-identifiers>),
+    if a version includes a local component, the package cannot be published to public
+    package indexes like PyPI or TestPyPI. The disallowed version segments may
+    be seen in auto-publishing workflows or when a configuration mistake is made.
+
+    However, some package indexes such as devpi or other alternatives allow local
+    versions. Local version identifiers must comply with [PEP 440].
+
+## Semantic Versioning (SemVer)
+
+Due to the default behavior it's necessary to always include a
+patch version (the `3` in `1.2.3`), or else the automatic guessing
+will increment the wrong part of the SemVer (e.g. tag `2.0` results in
+`2.1.devX` instead of `2.0.1.devX`). So please make sure to tag
+accordingly.
+
+
+## Builtin mechanisms for obtaining version numbers
+
+1. the SCM itself (Git/Mercurial)
+2. `.hg_archival` files (Mercurial archives)
+3. `.git_archival.txt` files (Git archives, see subsection below)
+4. `PKG-INFO`
+
+
+### Git archives
+
+Git archives are supported, but a few changes to your repository are required.
+
+Ensure the content of the following files:
+
+```{ .text file=".git_archival.txt"}
+
+node: $Format:%H$
+node-date: $Format:%cI$
+describe-name: $Format:%(describe:tags=true,match=*[0-9]*)$
+ref-names: $Format:%D$
+```
+
+``` {.text file=".gitattributes"}
+.git_archival.txt  export-subst
+```
+
+Finally, don't forget to commit the two files:
+```commandline
+$ git add .git_archival.txt .gitattributes && git commit -m "add export config"
+```
+
+
+Note that if you are creating a `_version.py` file, note that it should not
+be kept in version control. It's strongly recommended to be put into gitignore.
+
+
+
+### File finders hook makes most of MANIFEST.in unnecessary
+
+`setuptools_scm` implements a [file_finders] entry point
+which returns all files tracked by your SCM.
+This eliminates the need for a manually constructed `MANIFEST.in` in most cases where this
+would be required when not using `setuptools_scm`, namely:
+
+* To ensure all relevant files are packaged when running the `sdist` command.
+  * When using [include_package_data] to include package data as part of the `build` or `bdist_wheel`.
+
+`MANIFEST.in` may still be used: anything defined there overrides the hook.
+This is mostly useful to exclude files tracked in your SCM from packages,
+although in principle it can be used to explicitly include non-tracked files too.
+
+[file_finders]: https://setuptools.pypa.io/en/latest/userguide/extension.html#adding-support-for-revision-control-systems
+[include_package_data]: https://setuptools.readthedocs.io/en/latest/setuptools.html#including-data-files
diff --git a/hatch.toml b/hatch.toml
new file mode 100644 (file)
index 0000000..aad1b87
--- /dev/null
@@ -0,0 +1,23 @@
+[envs.test]
+extras = ["test", "dev"]
+
+[envs.test.scripts]
+all = "pytest {args}"
+
+[[env.test.matrix]]
+python = ["3.8", "3.9", "3.10", "3.11"]
+
+
+[envs.docs]
+python = "3.11"
+extras = ["docs"]
+dependencies = ["scriv"]
+
+[envs.docs.scripts]
+build = "mkdocs build --clean --strict"
+serve = "mkdocs serve --dev-addr localhost:8000"
+init = "mkdocs {args}"
+sync = ["entangled sync"]
+
+changelog-create = "scriv create {args}"
+changelog-collect = "scriv collect {args}"
\ No newline at end of file
diff --git a/mkdocs.yml b/mkdocs.yml
new file mode 100644 (file)
index 0000000..79ad48a
--- /dev/null
@@ -0,0 +1,37 @@
+site_name: setuptools scm
+
+nav:
+  - index.md
+  - usage.md
+  - customizing.md
+  - config.md
+  - extending.md
+  - overrides.md
+theme:
+  name: material
+
+watch:
+- src/setuptools_scm
+- docs
+markdown_extensions:
+  - def_list
+  - admonition
+  - pymdownx.tasklist:
+      custom_checkbox: true
+
+  - pymdownx.superfences
+
+plugins:
+- entangled
+- search
+- mkdocstrings:
+    default_handler: python
+    handlers:
+      python:
+        paths: [ src ]
+
+        options:
+          separate_signature: true
+          show_signature_annotations: true
+          allow_inspection: true
+          show_root_heading: true
diff --git a/mypy.ini b/mypy.ini
new file mode 100644 (file)
index 0000000..ef383f7
--- /dev/null
+++ b/mypy.ini
@@ -0,0 +1,6 @@
+[mypy]
+python_version = 3.8
+warn_return_any = True
+warn_unused_configs = True
+mypy_path = $MYPY_CONFIG_FILE_DIR/src
+strict = true
diff --git a/packaging/python3-setuptools_scm.manifest b/packaging/python3-setuptools_scm.manifest
new file mode 100644 (file)
index 0000000..017d22d
--- /dev/null
@@ -0,0 +1,5 @@
+<manifest>
+ <request>
+    <domain name="_"/>
+ </request>
+</manifest>
diff --git a/packaging/python3-setuptools_scm.spec b/packaging/python3-setuptools_scm.spec
new file mode 100644 (file)
index 0000000..20c9776
--- /dev/null
@@ -0,0 +1,41 @@
+Name:           python3-setuptools_scm
+Version:        8.0.4
+Release:        1
+Summary:        setuptools_scm handles managing your Python package versions
+License:        MIT
+Group:          Development/Languages/Python
+Source:         setuptools_scm-%{version}.tar.gz
+Source1001:     %{name}.manifest
+Source1002:     setuptools_scm-8.0.4-py3-none-any.whl
+
+BuildRequires:  pkgconfig(python3)
+BuildRequires:  python3-pip
+BuildRequires:  python3-packaging
+BuildRequires:  python3-typing_extensions
+
+Requires:       python3-packaging
+Requires:       python3-typing_extensions
+
+BuildArch:      noarch
+
+%description
+setuptools_scm handles managing your Python package versions in SCM metadata instead of declaring them as the version argument or in a SCM managed file.
+
+%prep
+%setup -q -n setuptools_scm-%{version}
+
+%build
+cp %{SOURCE1001} .
+
+%install
+%{_bindir}/python3 -m pip install --root %{buildroot} %{SOURCE1002} 
+rm -f %{buildroot}/%{python3_sitelib}/setuptools_scm/.git_archival.txt
+
+%files
+%manifest %{name}.manifest
+%license LICENSE
+%{python3_sitelib}/setuptools_scm/*.py
+%{python3_sitelib}/setuptools_scm/__pycache__/*
+%{python3_sitelib}/setuptools_scm/_file_finders/*
+%{python3_sitelib}/setuptools_scm/_integration/*
+%{python3_sitelib}/setuptools_scm-%{version}.dist-info/*
diff --git a/packaging/setuptools_scm-8.0.4-py3-none-any.whl b/packaging/setuptools_scm-8.0.4-py3-none-any.whl
new file mode 100644 (file)
index 0000000..314c5d2
Binary files /dev/null and b/packaging/setuptools_scm-8.0.4-py3-none-any.whl differ
index f90d4d1eabf76c92aa417567410b83e26a03a40a..833400b36c9fdc94e2318c0068557a27d7bbfccb 100644 (file)
@@ -1,3 +1,135 @@
+
+
 [build-system]
-requires = ["setuptools>=34.4", "wheel"]
-build-backend = "setuptools.build_meta"
+build-backend = "_own_version_helper:build_meta"
+requires = [
+  "setuptools>=61",
+  'tomli; python_version <= "3.12"',
+]
+backend-path = [
+  ".",
+  "src",
+]
+
+[project]
+name = "setuptools-scm"
+description = "the blessed package to manage your versions by scm tags"
+readme = "README.md"
+license.file = "LICENSE"
+authors = [
+    {name="Ronny Pfannschmidt", email="opensource@ronnypfannschmidt.de"}
+]
+requires-python = ">=3.8"
+classifiers = [
+  "Development Status :: 5 - Production/Stable",
+  "Intended Audience :: Developers",
+  "License :: OSI Approved :: MIT License",
+  "Programming Language :: Python",
+  "Programming Language :: Python :: 3 :: Only",
+  "Programming Language :: Python :: 3.8",
+  "Programming Language :: Python :: 3.9",
+  "Programming Language :: Python :: 3.10",
+  "Programming Language :: Python :: 3.11",
+  "Programming Language :: Python :: 3.12",
+  "Topic :: Software Development :: Libraries",
+  "Topic :: Software Development :: Version Control",
+  "Topic :: System :: Software Distribution",
+  "Topic :: Utilities",
+]
+dynamic = [
+  "version",
+]
+dependencies = [
+  "packaging>=20",
+  "setuptools",
+  'tomli>=1; python_version <= "3.12"',
+  "typing-extensions",
+]
+[project.optional-dependencies]
+docs = [
+  "entangled_cli[rich]",
+  "mkdocs",
+  "mkdocs-entangled-plugin",
+  "mkdocs-material",
+  "mkdocstrings[python]",
+  "pygments",
+]
+rich = [
+  "rich",
+]
+test = [
+  "build",
+  "pytest",
+  "rich",
+  "wheel",
+]
+toml = [
+]
+[project.urls]
+documentation = "https://setuptools-scm.readthedocs.io/"
+repository = "https://github.com/pypa/setuptools_scm/"
+[project.entry-points."distutils.setup_keywords"]
+use_scm_version = "setuptools_scm._integration.setuptools:version_keyword"
+[project.entry-points."setuptools.file_finders"]
+setuptools_scm = "setuptools_scm._file_finders:find_files"
+[project.entry-points."setuptools.finalize_distribution_options"]
+setuptools_scm = "setuptools_scm._integration.setuptools:infer_version"
+[project.entry-points."setuptools_scm.files_command"]
+".git" = "setuptools_scm._file_finders.git:git_find_files"
+".hg" = "setuptools_scm._file_finders.hg:hg_find_files"
+[project.entry-points."setuptools_scm.files_command_fallback"]
+".git_archival.txt" = "setuptools_scm._file_finders.git:git_archive_find_files"
+".hg_archival.txt" = "setuptools_scm._file_finders.hg:hg_archive_find_files"
+[project.entry-points."setuptools_scm.local_scheme"]
+dirty-tag = "setuptools_scm.version:get_local_dirty_tag"
+no-local-version = "setuptools_scm.version:get_no_local_node"
+node-and-date = "setuptools_scm.version:get_local_node_and_date"
+node-and-timestamp = "setuptools_scm.version:get_local_node_and_timestamp"
+[project.entry-points."setuptools_scm.parse_scm"]
+".git" = "setuptools_scm.git:parse"
+".hg" = "setuptools_scm.hg:parse"
+[project.entry-points."setuptools_scm.parse_scm_fallback"]
+".git_archival.txt" = "setuptools_scm.git:parse_archival"
+".hg_archival.txt" = "setuptools_scm.hg:parse_archival"
+PKG-INFO = "setuptools_scm.fallbacks:parse_pkginfo"
+"pyproject.toml" = "setuptools_scm.fallbacks:fallback_version"
+"setup.py" = "setuptools_scm.fallbacks:fallback_version"
+[project.entry-points."setuptools_scm.version_scheme"]
+"calver-by-date" = "setuptools_scm.version:calver_by_date"
+"guess-next-dev" = "setuptools_scm.version:guess_next_dev_version"
+"no-guess-dev" = "setuptools_scm.version:no_guess_dev_version"
+"post-release" = "setuptools_scm.version:postrelease_version"
+"python-simplified-semver" = "setuptools_scm.version:simplified_semver_version"
+"release-branch-semver" = "setuptools_scm.version:release_branch_semver_version"
+
+[tool.setuptools.packages.find]
+where = ["src"]
+namespaces = false
+
+[tool.setuptools.dynamic]
+version = { attr = "_own_version_helper.version"}
+
+[tool.setuptools_scm]
+
+[tool.ruff]
+select = ["E", "F", "B", "U", "YTT", "C", "DTZ", "PYI", "PT"]
+ignore = ["B028"]
+
+[tool.pytest.ini_options]
+testpaths = ["testing"]
+filterwarnings = [
+  "error",
+  "ignore:.*tool\\.setuptools_scm.*",
+  "ignore:.*git archive did not support describe output.*:UserWarning",
+]
+log_level = "debug"
+log_cli_level = "info"
+# disable unraisable until investigated
+addopts = ["-p", "no:unraisableexception"]
+markers = [
+  "issue(id): reference to github issue",
+  "skip_commit: allows to skip committing in the helpers",
+]
+
+[tool.scriv]
+format = "md"
index 110f70087bead74a8b32d4ee6f4567cda363b21c..8bfd5a12f85b8fbb6c058cf67dd23da690835ea0 100644 (file)
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,75 +1,3 @@
-[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
deleted file mode 100644 (file)
index 0b3e0c7..0000000
--- a/setup.py
+++ /dev/null
@@ -1,53 +0,0 @@
-"""\
-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
deleted file mode 100644 (file)
index ea5bbd2..0000000
+++ /dev/null
@@ -1,615 +0,0 @@
-Metadata-Version: 2.1
-Name: setuptools-scm
-Version: 4.1.2
-Summary: the blessed package to manage your versions by scm tags
-Home-page: https://github.com/pypa/setuptools_scm/
-Author: Ronny Pfannschmidt
-Author-email: opensource@ronnypfannschmidt.de
-License: MIT
-Description: setuptools_scm
-        ===============
-        
-        ``setuptools_scm`` handles managing your Python package versions
-        in SCM metadata instead of declaring them as the version argument
-        or in a SCM managed file.
-        
-        Additionally ``setuptools_scm`` provides setuptools with a list of files that are managed by the SCM
-        (i.e. it automatically adds all of the SCM-managed files to the sdist).
-        Unwanted files must be excluded by discarding them via ``MANIFEST.in``.
-        
-        .. image:: https://travis-ci.org/pypa/setuptools_scm.svg?branch=master
-            :target: https://travis-ci.org/pypa/setuptools_scm
-        
-        .. image:: https://tidelift.com/badges/package/pypi/setuptools-scm
-           :target: https://tidelift.com/subscription/pkg/pypi-setuptools-scm?utm_source=pypi-setuptools-scm&utm_medium=readme
-        
-        
-        ``pyproject.toml`` usage
-        ------------------------
-        
-        The preferred way to configure ``setuptools_scm`` is to author
-        settings in a ``tool.setuptools_scm`` section of ``pyproject.toml``.
-        
-        This feature requires Setuptools 42 or later, released in Nov, 2019.
-        If your project needs to support build from sdist on older versions
-        of Setuptools, you will need to also implement the ``setup.py usage``
-        for those legacy environments.
-        
-        First, ensure that ``setuptools_scm`` is present during the project's
-        built step by specifying it as one of the build requirements.
-        
-        .. code:: toml
-        
-            # pyproject.toml
-            [build-system]
-            requires = ["setuptools>=42", "wheel", "setuptools_scm[toml]>=3.4"]
-        
-        Note that the ``toml`` extra must be supplied.
-        
-        That will be sufficient to require ``setuptools_scm`` for projects
-        that support PEP 518 (`pip <https://pypi.org/project/pip>`_ and
-        `pep517 <https://pypi.org/project/pep517/>`_). Many tools,
-        especially those that invoke ``setup.py`` for any reason, may
-        continue to rely on ``setup_requires``. For maximum compatibility
-        with those uses, consider also including a ``setup_requires`` directive
-        (described below in ``setup.py usage`` and ``setup.cfg``).
-        
-        To enable version inference, add this section to your pyproject.toml:
-        
-        .. code:: toml
-        
-            # pyproject.toml
-            [tool.setuptools_scm]
-        
-        Including this section is comparable to supplying
-        ``use_scm_version=True`` in ``setup.py``. Additionally,
-        include arbitrary keyword arguments in that section
-        to be supplied to ``get_version()``. For example:
-        
-        .. code:: toml
-        
-            # pyproject.toml
-        
-            [tool.setuptools_scm]
-            write_to = "pkg/version.py"
-        
-        
-        ``setup.py`` usage
-        ------------------
-        
-        The following settings are considered legacy behavior and
-        superseded by the ``pyproject.toml`` usage, but for maximal
-        compatibility, projects may also supply the configuration in
-        this older form.
-        
-        To use ``setuptools_scm`` just modify your project's ``setup.py`` file
-        like this:
-        
-        * Add ``setuptools_scm`` to the ``setup_requires`` parameter.
-        * Add the ``use_scm_version`` parameter and set it to ``True``.
-        
-        For example:
-        
-        .. code:: python
-        
-            from setuptools import setup
-            setup(
-                ...,
-                use_scm_version=True,
-                setup_requires=['setuptools_scm'],
-                ...,
-            )
-        
-        Arguments to ``get_version()`` (see below) may be passed as a dictionary to
-        ``use_scm_version``. For example:
-        
-        .. code:: python
-        
-            from setuptools import setup
-            setup(
-                ...,
-                use_scm_version = {
-                    "root": "..",
-                    "relative_to": __file__,
-                    "local_scheme": "node-and-timestamp"
-                },
-                setup_requires=['setuptools_scm'],
-                ...,
-            )
-        
-        You can confirm the version number locally via ``setup.py``:
-        
-        .. code-block:: shell
-        
-            $ python setup.py --version
-        
-        .. note::
-        
-           If you see unusual version numbers for packages but ``python setup.py
-           --version`` reports the expected version number, ensure ``[egg_info]`` is
-           not defined in ``setup.cfg``.
-        
-        
-        ``setup.cfg`` usage
-        -------------------
-        
-        If using `setuptools 30.3.0
-        <https://setuptools.readthedocs.io/en/latest/setuptools.html#configuring-setup-using-setup-cfg-files>`_
-        or greater, you can store ``setup_requires`` configuration in ``setup.cfg``.
-        However, ``use_scm_version`` must still be placed in ``setup.py``. For example:
-        
-        .. code:: python
-        
-            # setup.py
-            from setuptools import setup
-            setup(
-                use_scm_version=True,
-            )
-        
-        .. code:: ini
-        
-            # setup.cfg
-            [metadata]
-            ...
-        
-            [options]
-            setup_requires =
-              setuptools_scm
-            ...
-        
-        .. important::
-        
-            Ensure neither the ``[metadata]`` ``version`` option nor the ``[egg_info]``
-            section are defined, as these will interfere with ``setuptools_scm``.
-        
-        You may also need to define a ``pyproject.toml`` file (`PEP-0518
-        <https://www.python.org/dev/peps/pep-0518>`_) to ensure you have the required
-        version of ``setuptools``:
-        
-        .. code:: ini
-        
-            # pyproject.toml
-            [build-system]
-            requires = ["setuptools>=30.3.0", "wheel", "setuptools_scm"]
-        
-        For more information, refer to the `setuptools issue #1002
-        <https://github.com/pypa/setuptools/issues/1002>`_.
-        
-        
-        Programmatic usage
-        ------------------
-        
-        In order to use ``setuptools_scm`` from code that is one directory deeper
-        than the project's root, you can use:
-        
-        .. code:: python
-        
-            from setuptools_scm import get_version
-            version = get_version(root='..', relative_to=__file__)
-        
-        See `setup.py Usage`_ above for how to use this within ``setup.py``.
-        
-        
-        Retrieving package version at runtime
-        -------------------------------------
-        
-        If you have opted not to hardcode the version number inside the package,
-        you can retrieve it at runtime from PEP-0566_ metadata using
-        ``importlib.metadata`` from the standard library
-        or the `importlib_metadata`_ backport:
-        
-        .. code:: python
-        
-            from importlib.metadata import version, PackageNotFoundError
-        
-            try:
-                __version__ = version(__name__)
-            except PackageNotFoundError:
-                # package is not installed
-               pass
-        
-        Alternatively, you can use ``pkg_resources`` which is included in
-        ``setuptools``:
-        
-        .. code:: python
-        
-           from pkg_resources import get_distribution, DistributionNotFound
-        
-           try:
-               __version__ = get_distribution(__name__).version
-           except DistributionNotFound:
-                # package is not installed
-               pass
-        
-        This does place a runtime dependency on ``setuptools``.
-        
-        .. _PEP-0566: https://www.python.org/dev/peps/pep-0566/
-        .. _importlib_metadata: https://pypi.org/project/importlib-metadata/
-        
-        
-        Usage from Sphinx
-        -----------------
-        
-        It is discouraged to use ``setuptools_scm`` from Sphinx itself,
-        instead use ``pkg_resources`` after editable/real installation:
-        
-        .. code:: python
-        
-            # contents of docs/conf.py
-            from pkg_resources import get_distribution
-            release = get_distribution('myproject').version
-            # for example take major/minor
-            version = '.'.join(release.split('.')[:2])
-        
-        The underlying reason is, that services like *Read the Docs* sometimes change
-        the working directory for good reasons and using the installed metadata
-        prevents using needless volatile data there.
-        
-        Notable Plugins
-        ----------------
-        
-        `setuptools_scm_git_archive <https://pypi.python.org/pypi/setuptools_scm_git_archive>`_
-            Provides partial support for obtaining versions from git archives that
-            belong to tagged versions. The only reason for not including it in
-            ``setuptools_scm`` itself is Git/GitHub not supporting sufficient metadata
-            for untagged/followup commits, which is preventing a consistent UX.
-        
-        
-        Default versioning scheme
-        --------------------------
-        
-        In the standard configuration ``setuptools_scm`` takes a look at three things:
-        
-        1. latest tag (with a version number)
-        2. the distance to this tag (e.g. number of revisions since latest tag)
-        3. workdir state (e.g. uncommitted changes since latest tag)
-        
-        and uses roughly the following logic to render the version:
-        
-        no distance and clean:
-            ``{tag}``
-        distance and clean:
-            ``{next_version}.dev{distance}+{scm letter}{revision hash}``
-        no distance and not clean:
-            ``{tag}+dYYYYMMDD``
-        distance and not clean:
-            ``{next_version}.dev{distance}+{scm letter}{revision hash}.dYYYYMMDD``
-        
-        The next version is calculated by adding ``1`` to the last numeric component of
-        the tag.
-        
-        For Git projects, the version relies on `git describe <https://git-scm.com/docs/git-describe>`_,
-        so you will see an additional ``g`` prepended to the ``{revision hash}``.
-        
-        Semantic Versioning (SemVer)
-        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-        
-        Due to the default behavior it's necessary to always include a
-        patch version (the ``3`` in ``1.2.3``), or else the automatic guessing
-        will increment the wrong part of the SemVer (e.g. tag ``2.0`` results in
-        ``2.1.devX`` instead of ``2.0.1.devX``). So please make sure to tag
-        accordingly.
-        
-        .. note::
-        
-            Future versions of ``setuptools_scm`` will switch to `SemVer
-            <http://semver.org/>`_ by default hiding the the old behavior as an
-            configurable option.
-        
-        
-        Builtin mechanisms for obtaining version numbers
-        --------------------------------------------------
-        
-        1. the SCM itself (git/hg)
-        2. ``.hg_archival`` files (mercurial archives)
-        3. ``PKG-INFO``
-        
-        .. note::
-        
-            Git archives are not supported due to Git shortcomings
-        
-        
-        File finders hook makes most of MANIFEST.in unnecessary
-        -------------------------------------------------------
-        
-        ``setuptools_scm`` implements a `file_finders
-        <https://setuptools.readthedocs.io/en/latest/setuptools.html#adding-support-for-revision-control-systems>`_
-        entry point which returns all files tracked by your SCM. This eliminates
-        the need for a manually constructed ``MANIFEST.in`` in most cases where this
-        would be required when not using ``setuptools_scm``, namely:
-        
-        * To ensure all relevant files are packaged when running the ``sdist`` command.
-        
-        * When using `include_package_data <https://setuptools.readthedocs.io/en/latest/setuptools.html#including-data-files>`_
-          to include package data as part of the ``build`` or ``bdist_wheel``.
-        
-        ``MANIFEST.in`` may still be used: anything defined there overrides the hook.
-        This is mostly useful to exclude files tracked in your SCM from packages,
-        although in principle it can be used to explicitly include non-tracked files
-        too.
-        
-        
-        Configuration parameters
-        ------------------------
-        
-        In order to configure the way ``use_scm_version`` works you can provide
-        a mapping with options instead of a boolean value.
-        
-        The currently supported configuration keys are:
-        
-        :root:
-            Relative path to cwd, used for finding the SCM root; defaults to ``.``
-        
-        :version_scheme:
-            Configures how the local version number is constructed; either an
-            entrypoint name or a callable.
-        
-        :local_scheme:
-            Configures how the local component of the version is constructed; either an
-            entrypoint name or a callable.
-        
-        :write_to:
-            A path to a file that gets replaced with a file containing the current
-            version. It is ideal for creating a ``version.py`` file within the
-            package, typically used to avoid using `pkg_resources.get_distribution`
-            (which adds some overhead).
-        
-            .. warning::
-        
-              Only files with :code:`.py` and :code:`.txt` extensions have builtin
-              templates, for other file types it is necessary to provide
-              :code:`write_to_template`.
-        
-        :write_to_template:
-            A newstyle format string that is given the current version as
-            the ``version`` keyword argument for formatting.
-        
-        :relative_to:
-            A file from which the root can be resolved.
-            Typically called by a script or module that is not in the root of the
-            repository to point ``setuptools_scm`` at the root of the repository by
-            supplying ``__file__``.
-        
-        :tag_regex:
-           A Python regex string to extract the version part from any SCM tag.
-            The regex needs to contain either a single match group, or a group
-            named ``version``, that captures the actual version information.
-        
-            Defaults to the value of ``setuptools_scm.config.DEFAULT_TAG_REGEX``
-            (see `config.py <src/setuptools_scm/config.py>`_).
-        
-        :parentdir_prefix_version:
-            If the normal methods for detecting the version (SCM version,
-            sdist metadata) fail, and the parent directory name starts with
-            ``parentdir_prefix_version``, then this prefix is stripped and the rest of
-            the parent directory name is matched with ``tag_regex`` to get a version
-            string.  If this parameter is unset (the default), then this fallback is
-            not used.
-        
-            This is intended to cover GitHub's "release tarballs", which extract into
-            directories named ``projectname-tag/`` (in which case
-            ``parentdir_prefix_version`` can be set e.g. to ``projectname-``).
-        
-        :fallback_version:
-            A version string that will be used if no other method for detecting the
-            version worked (e.g., when using a tarball with no metadata). If this is
-            unset (the default), setuptools_scm will error if it fails to detect the
-            version.
-        
-        :parse:
-            A function that will be used instead of the discovered SCM for parsing the
-            version.
-            Use with caution, this is a function for advanced use, and you should be
-            familiar with the ``setuptools_scm`` internals to use it.
-        
-        :git_describe_command:
-            This command will be used instead the default ``git describe`` command.
-            Use with caution, this is a function for advanced use, and you should be
-            familiar with the ``setuptools_scm`` internals to use it.
-        
-            Defaults to the value set by ``setuptools_scm.git.DEFAULT_DESCRIBE``
-            (see `git.py <src/setuptools_scm/git.py>`_).
-        
-        To use ``setuptools_scm`` in other Python code you can use the ``get_version``
-        function:
-        
-        .. code:: python
-        
-            from setuptools_scm import get_version
-            my_version = get_version()
-        
-        It optionally accepts the keys of the ``use_scm_version`` parameter as
-        keyword arguments.
-        
-        Example configuration in ``setup.py`` format:
-        
-        .. code:: python
-        
-            from setuptools import setup
-        
-            setup(
-                use_scm_version={
-                    'write_to': 'version.py',
-                    'write_to_template': '__version__ = "{version}"',
-                    'tag_regex': r'^(?P<prefix>v)?(?P<version>[^\+]+)(?P<suffix>.*)?$',
-                }
-            )
-        
-        Environment variables
-        ---------------------
-        
-        :SETUPTOOLS_SCM_PRETEND_VERSION:
-            when defined and not empty,
-            its used as the primary source for the version number
-            in which case it will be a unparsed string
-        
-        :SETUPTOOLS_SCM_DEBUG:
-            when defined and not empty,
-            a lot of debug information will be printed as part of ``setuptools_scm``
-            operating
-        
-        Extending setuptools_scm
-        ------------------------
-        
-        ``setuptools_scm`` ships with a few ``setuptools`` entrypoints based hooks to
-        extend its default capabilities.
-        
-        Adding a new SCM
-        ~~~~~~~~~~~~~~~~
-        
-        ``setuptools_scm`` provides two entrypoints for adding new SCMs:
-        
-        ``setuptools_scm.parse_scm``
-            A function used to parse the metadata of the current workdir
-            using the name of the control directory/file of your SCM as the
-            entrypoint's name. E.g. for the built-in entrypoint for git the
-            entrypoint is named ``.git`` and references ``setuptools_scm.git:parse``
-        
-          The return value MUST be a ``setuptools_scm.version.ScmVersion`` instance
-          created by the function ``setuptools_scm.version:meta``.
-        
-        ``setuptools_scm.files_command``
-          Either a string containing a shell command that prints all SCM managed
-          files in its current working directory or a callable, that given a
-          pathname will return that list.
-        
-          Also use then name of your SCM control directory as name of the entrypoint.
-        
-        Version number construction
-        ~~~~~~~~~~~~~~~~~~~~~~~~~~~
-        
-        ``setuptools_scm.version_scheme``
-            Configures how the version number is constructed given a
-            ``setuptools_scm.version.ScmVersion`` instance and should return a string
-            representing the version.
-        
-            Available implementations:
-        
-            :guess-next-dev: Automatically guesses the next development version (default).
-                Guesses the upcoming release by incrementing the pre-release segment if present,
-                otherwise by incrementing the micro segment. Then appends :code:`.devN`.
-            :post-release: generates post release versions (adds :code:`.postN`)
-            :python-simplified-semver: Basic semantic versioning. Guesses the upcoming release
-                by incrementing the minor segment and setting the micro segment to zero if the
-                current branch contains the string ``'feature'``, otherwise by incrementing the
-                micro version. Then appends :code:`.devN`. Not compatible with pre-releases.
-            :release-branch-semver: Semantic versioning for projects with release branches. The
-                same as ``guess-next-dev`` (incrementing the pre-release or micro segment) if on
-                a release branch: a branch whose name (ignoring namespace) parses as a version
-                that matches the most recent tag up to the minor segment. Otherwise if on a
-                non-release branch, increments the minor segment and sets the micro segment to
-                zero, then appends :code:`.devN`.
-        
-        ``setuptools_scm.local_scheme``
-            Configures how the local part of a version is rendered given a
-            ``setuptools_scm.version.ScmVersion`` instance and should return a string
-            representing the local version.
-            Dates and times are in Coordinated Universal Time (UTC), because as part
-            of the version, they should be location independent.
-        
-            Available implementations:
-        
-            :node-and-date: adds the node on dev versions and the date on dirty
-                            workdir (default)
-            :node-and-timestamp: like ``node-and-date`` but with a timestamp of
-                                 the form ``{:%Y%m%d%H%M%S}`` instead
-            :dirty-tag: adds ``+dirty`` if the current workdir has changes
-            :no-local-version: omits local version, useful e.g. because pypi does
-                               not support it
-        
-        
-        Importing in ``setup.py``
-        ~~~~~~~~~~~~~~~~~~~~~~~~~
-        
-        To support usage in ``setup.py`` passing a callable into ``use_scm_version``
-        is supported.
-        
-        Within that callable, ``setuptools_scm`` is available for import.
-        The callable must return the configuration.
-        
-        
-        .. code:: python
-        
-            # content of setup.py
-            import setuptools
-        
-            def myversion():
-                from setuptools_scm.version import get_local_dirty_tag
-                def clean_scheme(version):
-                    return get_local_dirty_tag(version) if version.dirty else '+clean'
-        
-                return {'local_scheme': clean_scheme}
-        
-            setup(
-                ...,
-                use_scm_version=myversion,
-                ...
-            )
-        
-        
-        Note on testing non-installed versions
-        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-        
-        While the general advice is to test against a installed version,
-        some environments require a test prior to install,
-        
-        .. code::
-        
-          $ python setup.py egg_info
-          $ PYTHONPATH=$PWD:$PWD/src pytest
-        
-        
-        Interaction with Enterprise Distributions
-        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-        
-        Some enterprise distributions like RHEL7 and others
-        ship rather old setuptools versions due to various release management details.
-        
-        On such distributions one might observe errors like:
-        
-        :code:``setuptools_scm.version.SetuptoolsOutdatedWarning: your setuptools is too old (<12)``
-        
-        In those case its typically possible to build by using a sdist against ``setuptools_scm<2.0``.
-        As those old setuptools versions lack sensible types for versions,
-        modern setuptools_scm is unable to support them sensibly.
-        
-        In case the project you need to build can not be patched to either use old setuptools_scm,
-        its still possible to install a more recent version of setuptools in order to handle the build
-        and/or install the package by using wheels or eggs.
-        
-        
-        
-        
-        Code of Conduct
-        ---------------
-        
-        Everyone interacting in the ``setuptools_scm`` project's codebases, issue
-        trackers, chat rooms, and mailing lists is expected to follow the
-        `PyPA Code of Conduct`_.
-        
-        .. _PyPA Code of Conduct: https://www.pypa.io/en/latest/code-of-conduct/
-        
-        Security Contact
-        ================
-        
-        To report a security vulnerability, please use the
-        `Tidelift security contact <https://tidelift.com/security>`_.
-        Tidelift will coordinate the fix and disclosure.
-        
-Platform: UNKNOWN
-Classifier: Development Status :: 5 - Production/Stable
-Classifier: Intended Audience :: Developers
-Classifier: License :: OSI Approved :: MIT License
-Classifier: Programming Language :: Python
-Classifier: Programming Language :: Python :: 2.7
-Classifier: Programming Language :: Python :: 3
-Classifier: Programming Language :: Python :: 3.5
-Classifier: Programming Language :: Python :: 3.6
-Classifier: Programming Language :: Python :: 3.7
-Classifier: Programming Language :: Python :: 3.8
-Classifier: Topic :: Software Development :: Libraries
-Classifier: Topic :: Software Development :: Version Control
-Classifier: Topic :: System :: Software Distribution
-Classifier: Topic :: Utilities
-Requires-Python: !=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7
-Provides-Extra: toml
diff --git a/src/setuptools_scm.egg-info/SOURCES.txt b/src/setuptools_scm.egg-info/SOURCES.txt
deleted file mode 100644 (file)
index 3953c8e..0000000
+++ /dev/null
@@ -1,47 +0,0 @@
-.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
deleted file mode 100644 (file)
index 8b13789..0000000
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/src/setuptools_scm.egg-info/entry_points.txt b/src/setuptools_scm.egg-info/entry_points.txt
deleted file mode 100644 (file)
index 7e46afc..0000000
+++ /dev/null
@@ -1,35 +0,0 @@
-[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
deleted file mode 100644 (file)
index 1fad750..0000000
+++ /dev/null
@@ -1,4 +0,0 @@
-setuptools
-
-[toml]
-toml
diff --git a/src/setuptools_scm.egg-info/top_level.txt b/src/setuptools_scm.egg-info/top_level.txt
deleted file mode 100644 (file)
index cba8d88..0000000
+++ /dev/null
@@ -1 +0,0 @@
-setuptools_scm
diff --git a/src/setuptools_scm.egg-info/zip-safe b/src/setuptools_scm.egg-info/zip-safe
deleted file mode 100644 (file)
index 8b13789..0000000
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/src/setuptools_scm/.git_archival.txt b/src/setuptools_scm/.git_archival.txt
new file mode 100644 (file)
index 0000000..560411b
--- /dev/null
@@ -0,0 +1,4 @@
+node: e5986362078130fbefc5e7afb9760ba251c523b0
+node-date: 2023-12-18T16:30:45+09:00
+describe-name: %(describe:tags=true,match=*[0-9]*)
+ref-names: HEAD -> master, origin/master, origin/HEAD
index 6b22b28e022d5f1db673d7009f515ef406b787c4..aa40ab315ebfeef079a22aa5a42dfe97570e8100 100644 (file)
 """
-:copyright: 2010-2015 by Ronny Pfannschmidt
+:copyright: 2010-2023 by Ronny Pfannschmidt
 :license: MIT
 """
-import os
-import warnings
-
-from .config import (
-    Configuration,
-    DEFAULT_VERSION_SCHEME,
-    DEFAULT_LOCAL_SCHEME,
-    DEFAULT_TAG_REGEX,
-)
-from .utils import function_has_arg, string_types
-from .version import format_version, meta
-from .discover import iter_matching_entrypoints
-
-PRETEND_KEY = "SETUPTOOLS_SCM_PRETEND_VERSION"
-
-TEMPLATES = {
-    ".py": """\
-# coding: utf-8
-# file generated by setuptools_scm
-# don't change, don't track in version control
-version = {version!r}
-""",
-    ".txt": "{version}",
-}
-
-
-def version_from_scm(root):
-    warnings.warn(
-        "version_from_scm is deprecated please use get_version",
-        category=DeprecationWarning,
-    )
-    config = Configuration()
-    config.root = root
-    # TODO: Is it API?
-    return _version_from_entrypoints(config)
-
-
-def _call_entrypoint_fn(root, config, fn):
-    if function_has_arg(fn, "config"):
-        return fn(root, config=config)
-    else:
-        warnings.warn(
-            "parse functions are required to provide a named argument"
-            " 'config' in the future.",
-            category=PendingDeprecationWarning,
-            stacklevel=2,
-        )
-        return fn(root)
-
-
-def _version_from_entrypoints(config, fallback=False):
-    if fallback:
-        entrypoint = "setuptools_scm.parse_scm_fallback"
-        root = config.fallback_root
-    else:
-        entrypoint = "setuptools_scm.parse_scm"
-        root = config.absolute_root
-    for ep in iter_matching_entrypoints(root, entrypoint):
-        version = _call_entrypoint_fn(root, config, ep.load())
-
-        if version:
-            return version
-
-
-def dump_version(root, version, write_to, template=None):
-    assert isinstance(version, string_types)
-    if not write_to:
-        return
-    target = os.path.normpath(os.path.join(root, write_to))
-    ext = os.path.splitext(target)[1]
-    template = template or TEMPLATES.get(ext)
-
-    if template is None:
-        raise ValueError(
-            "bad file format: '{}' (of {}) \nonly *.txt and *.py are supported".format(
-                os.path.splitext(target)[1], target
-            )
-        )
-    with open(target, "w") as fp:
-        fp.write(template.format(version=version))
-
-
-def _do_parse(config):
-    pretended = os.environ.get(PRETEND_KEY)
-    if pretended:
-        # we use meta here since the pretended version
-        # must adhere to the pep to begin with
-        return meta(tag=pretended, preformatted=True, config=config)
-
-    if config.parse:
-        parse_result = _call_entrypoint_fn(config.absolute_root, config, config.parse)
-        if isinstance(parse_result, string_types):
-            raise TypeError(
-                "version parse result was a string\nplease return a parsed version"
-            )
-        version = parse_result or _version_from_entrypoints(config, fallback=True)
-    else:
-        # include fallbacks after dropping them from the main entrypoint
-        version = _version_from_entrypoints(config) or _version_from_entrypoints(
-            config, fallback=True
-        )
-
-    if version:
-        return version
-
-    raise LookupError(
-        "setuptools-scm was unable to detect version for %r.\n\n"
-        "Make sure you're either building from a fully intact git repository "
-        "or PyPI tarballs. Most other sources (such as GitHub's tarballs, a "
-        "git checkout without the .git folder) don't contain the necessary "
-        "metadata and will not work.\n\n"
-        "For example, if you're using pip, instead of "
-        "https://github.com/user/proj/archive/master.zip "
-        "use git+https://github.com/user/proj.git#egg=proj" % config.absolute_root
-    )
-
-
-def get_version(
-    root=".",
-    version_scheme=DEFAULT_VERSION_SCHEME,
-    local_scheme=DEFAULT_LOCAL_SCHEME,
-    write_to=None,
-    write_to_template=None,
-    relative_to=None,
-    tag_regex=DEFAULT_TAG_REGEX,
-    parentdir_prefix_version=None,
-    fallback_version=None,
-    fallback_root=".",
-    parse=None,
-    git_describe_command=None,
-):
-    """
-    If supplied, relative_to should be a file from which root may
-    be resolved. Typically called by a script or module that is not
-    in the root of the repository to direct setuptools_scm to the
-    root of the repository by supplying ``__file__``.
-    """
-
-    config = Configuration(**locals())
-    return _get_version(config)
-
-
-def _get_version(config):
-    parsed_version = _do_parse(config)
-
-    if parsed_version:
-        version_string = format_version(
-            parsed_version,
-            version_scheme=config.version_scheme,
-            local_scheme=config.local_scheme,
-        )
-        dump_version(
-            root=config.root,
-            version=version_string,
-            write_to=config.write_to,
-            template=config.write_to_template,
-        )
-
-        return version_string
+from __future__ import annotations
+
+from ._config import Configuration
+from ._config import DEFAULT_LOCAL_SCHEME  # soft deprecated
+from ._config import DEFAULT_VERSION_SCHEME  # soft deprecated
+from ._get_version_impl import _get_version  # soft deprecated
+from ._get_version_impl import get_version  # soft deprecated
+from ._integration.dump_version import dump_version  # soft deprecated
+from ._version_cls import NonNormalizedVersion
+from ._version_cls import Version
+from .version import ScmVersion
+
+
+# Public API
+__all__ = [
+    # soft deprecated imports, left for backward compatibility
+    "get_version",
+    "_get_version",
+    "dump_version",
+    "DEFAULT_VERSION_SCHEME",
+    "DEFAULT_LOCAL_SCHEME",
+    "Configuration",
+    "Version",
+    "ScmVersion",
+    "NonNormalizedVersion",
+]
index a464c51a1bc9e26a1bae1132950a8e2494dde1ce..dab6068af9d9c40006986703bcd9173e62e67f42 100644 (file)
@@ -1,17 +1,6 @@
-from __future__ import print_function
-import sys
-from setuptools_scm import get_version
-from setuptools_scm.integration import find_files
-from setuptools_scm.version import _warn_if_setuptools_outdated
-
-
-def main():
-    _warn_if_setuptools_outdated()
-    print("Guessed Version", get_version())
-    if "ls" in sys.argv:
-        for fname in find_files("."):
-            print(fname)
+from __future__ import annotations
 
+from ._cli import main
 
 if __name__ == "__main__":
     main()
diff --git a/src/setuptools_scm/_cli.py b/src/setuptools_scm/_cli.py
new file mode 100644 (file)
index 0000000..66099b1
--- /dev/null
@@ -0,0 +1,85 @@
+from __future__ import annotations
+
+import argparse
+import os
+import sys
+
+from setuptools_scm import Configuration
+from setuptools_scm._file_finders import find_files
+from setuptools_scm._get_version_impl import _get_version
+from setuptools_scm.discover import walk_potential_roots
+
+
+def main(args: list[str] | None = None) -> None:
+    opts = _get_cli_opts(args)
+    inferred_root: str = opts.root or "."
+
+    pyproject = opts.config or _find_pyproject(inferred_root)
+
+    try:
+        config = Configuration.from_file(
+            pyproject,
+            root=(os.path.abspath(opts.root) if opts.root is not None else None),
+        )
+    except (LookupError, FileNotFoundError) as ex:
+        # no pyproject.toml OR no [tool.setuptools_scm]
+        print(
+            f"Warning: could not use {os.path.relpath(pyproject)},"
+            " using default configuration.\n"
+            f" Reason: {ex}.",
+            file=sys.stderr,
+        )
+        config = Configuration(inferred_root)
+
+    version = _get_version(config, force_write_version_files=False)
+    if version is None:
+        raise SystemExit("ERROR: no version found for", opts)
+    if opts.strip_dev:
+        version = version.partition(".dev")[0]
+    print(version)
+
+    if opts.command == "ls":
+        for fname in find_files(config.root):
+            print(fname)
+
+
+def _get_cli_opts(args: list[str] | None) -> argparse.Namespace:
+    prog = "python -m setuptools_scm"
+    desc = "Print project version according to SCM metadata"
+    parser = argparse.ArgumentParser(prog, description=desc)
+    # By default, help for `--help` starts with lower case, so we keep the pattern:
+    parser.add_argument(
+        "-r",
+        "--root",
+        default=None,
+        help='directory managed by the SCM, default: inferred from config file, or "."',
+    )
+    parser.add_argument(
+        "-c",
+        "--config",
+        default=None,
+        metavar="PATH",
+        help="path to 'pyproject.toml' with setuptools_scm config, "
+        "default: looked up in the current or parent directories",
+    )
+    parser.add_argument(
+        "--strip-dev",
+        action="store_true",
+        help="remove the dev/local parts of the version before printing the version",
+    )
+    sub = parser.add_subparsers(title="extra commands", dest="command", metavar="")
+    # We avoid `metavar` to prevent printing repetitive information
+    desc = "List files managed by the SCM"
+    sub.add_parser("ls", help=desc[0].lower() + desc[1:], description=desc)
+    return parser.parse_args(args)
+
+
+def _find_pyproject(parent: str) -> str:
+    for directory in walk_potential_roots(os.path.abspath(parent)):
+        pyproject = os.path.join(directory, "pyproject.toml")
+        if os.path.isfile(pyproject):
+            return pyproject
+
+    return os.path.abspath(
+        "pyproject.toml"
+    )  # use default name to trigger the default errors
diff --git a/src/setuptools_scm/_config.py b/src/setuptools_scm/_config.py
new file mode 100644 (file)
index 0000000..5e5feb1
--- /dev/null
@@ -0,0 +1,151 @@
+""" configuration """
+from __future__ import annotations
+
+import dataclasses
+import os
+import re
+import warnings
+from pathlib import Path
+from typing import Any
+from typing import Pattern
+from typing import Protocol
+
+from . import _log
+from . import _types as _t
+from ._integration.pyproject_reading import (
+    get_args_for_pyproject as _get_args_for_pyproject,
+)
+from ._integration.pyproject_reading import read_pyproject as _read_pyproject
+from ._overrides import read_toml_overrides
+from ._version_cls import _validate_version_cls
+from ._version_cls import _VersionT
+from ._version_cls import Version as _Version
+
+log = _log.log.getChild("config")
+
+DEFAULT_TAG_REGEX = re.compile(
+    r"^(?:[\w-]+-)?(?P<version>[vV]?\d+(?:\.\d+){0,2}[^\+]*)(?:\+.*)?$"
+)
+"""default tag regex that tries to match PEP440 style versions
+with prefix consisting of dashed words"""
+
+DEFAULT_VERSION_SCHEME = "guess-next-dev"
+DEFAULT_LOCAL_SCHEME = "node-and-date"
+
+
+def _check_tag_regex(value: str | Pattern[str] | None) -> Pattern[str]:
+    if not value:
+        regex = DEFAULT_TAG_REGEX
+    else:
+        regex = re.compile(value)
+
+    group_names = regex.groupindex.keys()
+    if regex.groups == 0 or (regex.groups > 1 and "version" not in group_names):
+        warnings.warn(
+            "Expected tag_regex to contain a single match group or a group named"
+            " 'version' to identify the version part of any tag."
+        )
+
+    return regex
+
+
+class ParseFunction(Protocol):
+    def __call__(
+        self, root: _t.PathT, *, config: Configuration
+    ) -> _t.SCMVERSION | None:
+        ...
+
+
+def _check_absolute_root(root: _t.PathT, relative_to: _t.PathT | None) -> str:
+    log.debug("check absolute root=%s relative_to=%s", root, relative_to)
+    if relative_to:
+        if (
+            os.path.isabs(root)
+            and os.path.isabs(relative_to)
+            and not os.path.commonpath([root, relative_to]) == root
+        ):
+            warnings.warn(
+                f"absolute root path '{root}' overrides relative_to '{relative_to}'"
+            )
+        if os.path.isdir(relative_to):
+            warnings.warn(
+                "relative_to is expected to be a file,"
+                f" its the directory {relative_to}\n"
+                "assuming the parent directory was passed"
+            )
+            log.debug("dir %s", relative_to)
+            root = os.path.join(relative_to, root)
+        else:
+            log.debug("file %s", relative_to)
+            root = os.path.join(os.path.dirname(relative_to), root)
+    return os.path.abspath(root)
+
+
+@dataclasses.dataclass
+class Configuration:
+    """Global configuration model"""
+
+    relative_to: _t.PathT | None = None
+    root: _t.PathT = "."
+    version_scheme: _t.VERSION_SCHEME = DEFAULT_VERSION_SCHEME
+    local_scheme: _t.VERSION_SCHEME = DEFAULT_LOCAL_SCHEME
+    tag_regex: Pattern[str] = DEFAULT_TAG_REGEX
+    parentdir_prefix_version: str | None = None
+    fallback_version: str | None = None
+    fallback_root: _t.PathT = "."
+    write_to: _t.PathT | None = None
+    write_to_template: str | None = None
+    version_file: _t.PathT | None = None
+    version_file_template: str | None = None
+    parse: ParseFunction | None = None
+    git_describe_command: _t.CMD_TYPE | None = None
+    dist_name: str | None = None
+    version_cls: type[_VersionT] = _Version
+    search_parent_directories: bool = False
+
+    parent: _t.PathT | None = None
+
+    @property
+    def absolute_root(self) -> str:
+        return _check_absolute_root(self.root, self.relative_to)
+
+    @classmethod
+    def from_file(
+        cls,
+        name: str | os.PathLike[str] = "pyproject.toml",
+        dist_name: str | None = None,
+        _require_section: bool = True,
+        **kwargs: Any,
+    ) -> Configuration:
+        """
+        Read Configuration from pyproject.toml (or similar).
+        Raises exceptions when file is not found or toml is
+        not installed or the file has invalid format or does
+        not contain the [tool.setuptools_scm] section.
+        """
+
+        pyproject_data = _read_pyproject(Path(name), require_section=_require_section)
+        args = _get_args_for_pyproject(pyproject_data, dist_name, kwargs)
+
+        args.update(read_toml_overrides(args["dist_name"]))
+        relative_to = args.pop("relative_to", name)
+        return cls.from_data(relative_to=relative_to, data=args)
+
+    @classmethod
+    def from_data(
+        cls, relative_to: str | os.PathLike[str], data: dict[str, Any]
+    ) -> Configuration:
+        """
+        given configuration data
+        create a config instance after validating tag regex/version class
+        """
+        tag_regex = _check_tag_regex(data.pop("tag_regex", None))
+        version_cls = _validate_version_cls(
+            data.pop("version_cls", None), data.pop("normalize", True)
+        )
+        return cls(
+            relative_to,
+            version_cls=version_cls,
+            tag_regex=tag_regex,
+            **data,
+        )
diff --git a/src/setuptools_scm/_entrypoints.py b/src/setuptools_scm/_entrypoints.py
new file mode 100644 (file)
index 0000000..50c9182
--- /dev/null
@@ -0,0 +1,139 @@
+from __future__ import annotations
+
+import sys
+from typing import Any
+from typing import Callable
+from typing import cast
+from typing import Iterator
+from typing import overload
+from typing import TYPE_CHECKING
+
+from . import _log
+from . import version
+
+if TYPE_CHECKING:
+    from . import _types as _t
+    from ._config import Configuration, ParseFunction
+
+
+from importlib.metadata import EntryPoint as EntryPoint
+
+
+if sys.version_info[:2] < (3, 10):
+    from importlib.metadata import entry_points as legacy_entry_points
+
+    class EntryPoints:
+        _groupdata: list[EntryPoint]
+
+        def __init__(self, groupdata: list[EntryPoint]) -> None:
+            self._groupdata = groupdata
+
+        def select(self, name: str) -> EntryPoints:
+            return EntryPoints([x for x in self._groupdata if x.name == name])
+
+        def __iter__(self) -> Iterator[EntryPoint]:
+            return iter(self._groupdata)
+
+    def entry_points(group: str) -> EntryPoints:
+        return EntryPoints(legacy_entry_points()[group])
+
+else:
+    from importlib.metadata import entry_points, EntryPoints
+
+
+log = _log.log.getChild("entrypoints")
+
+
+def version_from_entrypoint(
+    config: Configuration, *, entrypoint: str, root: _t.PathT
+) -> version.ScmVersion | None:
+    from .discover import iter_matching_entrypoints
+
+    log.debug("version_from_ep %s in %s", entrypoint, root)
+    for ep in iter_matching_entrypoints(root, entrypoint, config):
+        fn: ParseFunction = ep.load()
+        maybe_version: version.ScmVersion | None = fn(root, config=config)
+        log.debug("%s found %r", ep, maybe_version)
+        if maybe_version is not None:
+            return maybe_version
+    return None
+
+
+def iter_entry_points(group: str, name: str | None = None) -> Iterator[EntryPoint]:
+    eps: EntryPoints = entry_points(group=group)
+    res = eps if name is None else eps.select(name=name)
+
+    return iter(res)
+
+
+def _get_ep(group: str, name: str) -> Any | None:
+    for ep in iter_entry_points(group, name):
+        log.debug("ep found: %s", ep.name)
+        return ep.load()
+    else:
+        return None
+
+
+def _get_from_object_reference_str(path: str, group: str) -> Any | None:
+    # todo: remove for importlib native spelling
+    ep = EntryPoint(path, path, group)
+    try:
+        return ep.load()
+    except (AttributeError, ModuleNotFoundError):
+        return None
+
+
+def _iter_version_schemes(
+    entrypoint: str,
+    scheme_value: _t.VERSION_SCHEMES,
+    _memo: set[object] | None = None,
+) -> Iterator[Callable[[version.ScmVersion], str]]:
+    if _memo is None:
+        _memo = set()
+    if isinstance(scheme_value, str):
+        scheme_value = cast(
+            "_t.VERSION_SCHEMES",
+            _get_ep(entrypoint, scheme_value)
+            or _get_from_object_reference_str(scheme_value, entrypoint),
+        )
+
+    if isinstance(scheme_value, (list, tuple)):
+        for variant in scheme_value:
+            if variant not in _memo:
+                _memo.add(variant)
+                yield from _iter_version_schemes(entrypoint, variant, _memo=_memo)
+    elif callable(scheme_value):
+        yield scheme_value
+
+
+@overload
+def _call_version_scheme(
+    version: version.ScmVersion,
+    entrypoint: str,
+    given_value: _t.VERSION_SCHEMES,
+    default: str,
+) -> str:
+    ...
+
+
+@overload
+def _call_version_scheme(
+    version: version.ScmVersion,
+    entrypoint: str,
+    given_value: _t.VERSION_SCHEMES,
+    default: None,
+) -> str | None:
+    ...
+
+
+def _call_version_scheme(
+    version: version.ScmVersion,
+    entrypoint: str,
+    given_value: _t.VERSION_SCHEMES,
+    default: str | None,
+) -> str | None:
+    for scheme in _iter_version_schemes(entrypoint, given_value):
+        result = scheme(version)
+        if result is not None:
+            return result
+    return default
diff --git a/src/setuptools_scm/_file_finders/__init__.py b/src/setuptools_scm/_file_finders/__init__.py
new file mode 100644 (file)
index 0000000..591aa90
--- /dev/null
@@ -0,0 +1,106 @@
+from __future__ import annotations
+
+import itertools
+import os
+from typing import Callable
+from typing import TYPE_CHECKING
+
+from .. import _log
+from .. import _types as _t
+from .._entrypoints import iter_entry_points
+
+if TYPE_CHECKING:
+    from typing_extensions import TypeGuard
+
+
+log = _log.log.getChild("file_finder")
+
+
+def scm_find_files(
+    path: _t.PathT,
+    scm_files: set[str],
+    scm_dirs: set[str],
+    force_all_files: bool = False,
+) -> list[str]:
+    """ setuptools compatible file finder that follows symlinks
+
+    - path: the root directory from which to search
+    - scm_files: set of scm controlled files and symlinks
+      (including symlinks to directories)
+    - scm_dirs: set of scm controlled directories
+      (including directories containing no scm controlled files)
+    - force_all_files: ignore ``scm_files`` and ``scm_dirs`` and list everything.
+
+    scm_files and scm_dirs must be absolute with symlinks resolved (realpath),
+    with normalized case (normcase)
+
+    Spec here: http://setuptools.readthedocs.io/en/latest/setuptools.html#\
+        adding-support-for-revision-control-systems
+    """
+    realpath = os.path.normcase(os.path.realpath(path))
+    seen: set[str] = set()
+    res: list[str] = []
+    for dirpath, dirnames, filenames in os.walk(realpath, followlinks=True):
+        # dirpath with symlinks resolved
+        realdirpath = os.path.normcase(os.path.realpath(dirpath))
+
+        def _link_not_in_scm(n: str, realdirpath: str = realdirpath) -> bool:
+            fn = os.path.join(realdirpath, os.path.normcase(n))
+            return os.path.islink(fn) and fn not in scm_files
+
+        if not force_all_files and realdirpath not in scm_dirs:
+            # directory not in scm, don't walk it's content
+            dirnames[:] = []
+            continue
+        if os.path.islink(dirpath) and not os.path.relpath(
+            realdirpath, realpath
+        ).startswith(os.pardir):
+            # a symlink to a directory not outside path:
+            # we keep it in the result and don't walk its content
+            res.append(os.path.join(path, os.path.relpath(dirpath, path)))
+            dirnames[:] = []
+            continue
+        if realdirpath in seen:
+            # symlink loop protection
+            dirnames[:] = []
+            continue
+        dirnames[:] = [
+            dn for dn in dirnames if force_all_files or not _link_not_in_scm(dn)
+        ]
+        for filename in filenames:
+            if not force_all_files and _link_not_in_scm(filename):
+                continue
+            # dirpath + filename with symlinks preserved
+            fullfilename = os.path.join(dirpath, filename)
+            is_tracked = os.path.normcase(os.path.realpath(fullfilename)) in scm_files
+            if force_all_files or is_tracked:
+                res.append(os.path.join(path, os.path.relpath(fullfilename, realpath)))
+        seen.add(realdirpath)
+    return res
+
+
+def is_toplevel_acceptable(toplevel: str | None) -> TypeGuard[str]:
+    """ """
+    if toplevel is None:
+        return False
+
+    ignored: list[str] = os.environ.get("SETUPTOOLS_SCM_IGNORE_VCS_ROOTS", "").split(
+        os.pathsep
+    )
+    ignored = [os.path.normcase(p) for p in ignored]
+
+    log.debug("toplevel: %r\n    ignored %s", toplevel, ignored)
+
+    return toplevel not in ignored
+
+
+def find_files(path: _t.PathT = "") -> list[str]:
+    for ep in itertools.chain(
+        iter_entry_points("setuptools_scm.files_command"),
+        iter_entry_points("setuptools_scm.files_command_fallback"),
+    ):
+        command: Callable[[_t.PathT], list[str]] = ep.load()
+        res: list[str] = command(path)
+        if res:
+            return res
+    return []
diff --git a/src/setuptools_scm/_file_finders/git.py b/src/setuptools_scm/_file_finders/git.py
new file mode 100644 (file)
index 0000000..873b4ba
--- /dev/null
@@ -0,0 +1,115 @@
+from __future__ import annotations
+
+import logging
+import os
+import subprocess
+import tarfile
+from typing import IO
+
+from . import is_toplevel_acceptable
+from . import scm_find_files
+from .. import _types as _t
+from .._run_cmd import run as _run
+from ..integration import data_from_mime
+
+log = logging.getLogger(__name__)
+
+
+def _git_toplevel(path: str) -> str | None:
+    try:
+        cwd = os.path.abspath(path or ".")
+        res = _run(["git", "rev-parse", "HEAD"], cwd=cwd)
+        if res.returncode:
+            # BAIL if there is no commit
+            log.error("listing git files failed - pretending there aren't any")
+            return None
+        res = _run(
+            ["git", "rev-parse", "--show-prefix"],
+            cwd=cwd,
+        )
+        if res.returncode:
+            return None
+        out = res.stdout[:-1]  # remove the trailing pathsep
+        if not out:
+            out = cwd
+        else:
+            # Here, ``out`` is a relative path to root of git.
+            # ``cwd`` is absolute path to current working directory.
+            # the below method removes the length of ``out`` from
+            # ``cwd``, which gives the git toplevel
+            assert cwd.replace("\\", "/").endswith(out), f"cwd={cwd!r}\nout={out!r}"
+            # In windows cwd contains ``\`` which should be replaced by ``/``
+            # for this assertion to work. Length of string isn't changed by replace
+            # ``\\`` is just and escape for `\`
+            out = cwd[: -len(out)]
+        log.debug("find files toplevel %s", out)
+        return os.path.normcase(os.path.realpath(out.strip()))
+    except subprocess.CalledProcessError:
+        # git returned error, we are not in a git repo
+        return None
+    except OSError:
+        # git command not found, probably
+        return None
+
+
+def _git_interpret_archive(fd: IO[bytes], toplevel: str) -> tuple[set[str], set[str]]:
+    with tarfile.open(fileobj=fd, mode="r|*") as tf:
+        git_files = set()
+        git_dirs = {toplevel}
+        for member in tf.getmembers():
+            name = os.path.normcase(member.name).replace("/", os.path.sep)
+            if member.type == tarfile.DIRTYPE:
+                git_dirs.add(name)
+            else:
+                git_files.add(name)
+        return git_files, git_dirs
+
+
+def _git_ls_files_and_dirs(toplevel: str) -> tuple[set[str], set[str]]:
+    # use git archive instead of git ls-file to honor
+    # export-ignore git attribute
+
+    cmd = ["git", "archive", "--prefix", toplevel + os.path.sep, "HEAD"]
+    proc = subprocess.Popen(
+        cmd, stdout=subprocess.PIPE, cwd=toplevel, stderr=subprocess.DEVNULL
+    )
+    assert proc.stdout is not None
+    try:
+        try:
+            return _git_interpret_archive(proc.stdout, toplevel)
+        finally:
+            # ensure we avoid resource warnings by cleaning up the process
+            proc.stdout.close()
+            proc.terminate()
+    except Exception:
+        if proc.wait() != 0:
+            log.error("listing git files failed - pretending there aren't any")
+        return set(), set()
+
+
+def git_find_files(path: _t.PathT = "") -> list[str]:
+    toplevel = _git_toplevel(os.fspath(path))
+    if not is_toplevel_acceptable(toplevel):
+        return []
+    fullpath = os.path.abspath(os.path.normpath(path))
+    if not fullpath.startswith(toplevel):
+        log.warning("toplevel mismatch computed %s vs resolved %s ", toplevel, fullpath)
+    git_files, git_dirs = _git_ls_files_and_dirs(toplevel)
+    return scm_find_files(path, git_files, git_dirs)
+
+
+def git_archive_find_files(path: _t.PathT = "") -> list[str]:
+    # This function assumes that ``path`` is obtained from a git archive
+    # and therefore all the files that should be ignored were already removed.
+    archival = os.path.join(path, ".git_archival.txt")
+    if not os.path.exists(archival):
+        return []
+
+    data = data_from_mime(archival)
+
+    if "$Format" in data.get("node", ""):
+        # Substitutions have not been performed, so not a reliable archive
+        return []
+
+    log.warning("git archive detected - fallback to listing all files")
+    return scm_find_files(path, set(), set(), force_all_files=True)
diff --git a/src/setuptools_scm/_file_finders/hg.py b/src/setuptools_scm/_file_finders/hg.py
new file mode 100644 (file)
index 0000000..f87ba06
--- /dev/null
@@ -0,0 +1,72 @@
+from __future__ import annotations
+
+import logging
+import os
+import subprocess
+
+from .. import _types as _t
+from .._file_finders import is_toplevel_acceptable
+from .._file_finders import scm_find_files
+from .._run_cmd import run as _run
+from ..integration import data_from_mime
+
+log = logging.getLogger(__name__)
+
+
+def _hg_toplevel(path: str) -> str | None:
+    try:
+        res = _run(
+            ["hg", "root"],
+            cwd=(path or "."),
+        )
+        res.check_returncode()
+        return os.path.normcase(os.path.realpath(res.stdout))
+    except subprocess.CalledProcessError:
+        # hg returned error, we are not in a mercurial repo
+        return None
+    except OSError:
+        # hg command not found, probably
+        return None
+
+
+def _hg_ls_files_and_dirs(toplevel: str) -> tuple[set[str], set[str]]:
+    hg_files: set[str] = set()
+    hg_dirs = {toplevel}
+    res = _run(["hg", "files"], cwd=toplevel)
+    if res.returncode:
+        return set(), set()
+    for name in res.stdout.splitlines():
+        name = os.path.normcase(name).replace("/", os.path.sep)
+        fullname = os.path.join(toplevel, name)
+        hg_files.add(fullname)
+        dirname = os.path.dirname(fullname)
+        while len(dirname) > len(toplevel) and dirname not in hg_dirs:
+            hg_dirs.add(dirname)
+            dirname = os.path.dirname(dirname)
+    return hg_files, hg_dirs
+
+
+def hg_find_files(path: str = "") -> list[str]:
+    toplevel = _hg_toplevel(path)
+    if not is_toplevel_acceptable(toplevel):
+        return []
+    assert toplevel is not None
+    hg_files, hg_dirs = _hg_ls_files_and_dirs(toplevel)
+    return scm_find_files(path, hg_files, hg_dirs)
+
+
+def hg_archive_find_files(path: _t.PathT = "") -> list[str]:
+    # This function assumes that ``path`` is obtained from a mercurial archive
+    # and therefore all the files that should be ignored were already removed.
+    archival = os.path.join(path, ".hg_archival.txt")
+    if not os.path.exists(archival):
+        return []
+
+    data = data_from_mime(archival)
+
+    if "node" not in data:
+        # Ensure file is valid
+        return []
+
+    log.warning("hg archive detected - fallback to listing all files")
+    return scm_find_files(path, set(), set(), force_all_files=True)
diff --git a/src/setuptools_scm/_get_version_impl.py b/src/setuptools_scm/_get_version_impl.py
new file mode 100644 (file)
index 0000000..2d9d947
--- /dev/null
@@ -0,0 +1,174 @@
+from __future__ import annotations
+
+import logging
+import re
+import warnings
+from pathlib import Path
+from typing import Any
+from typing import NoReturn
+from typing import Pattern
+
+from . import _config
+from . import _entrypoints
+from . import _run_cmd
+from . import _types as _t
+from ._config import Configuration
+from ._overrides import _read_pretended_version_for
+from ._version_cls import _validate_version_cls
+from .version import format_version as _format_version
+from .version import ScmVersion
+
+_log = logging.getLogger(__name__)
+
+
+def parse_scm_version(config: Configuration) -> ScmVersion | None:
+    try:
+        if config.parse is not None:
+            parse_result = config.parse(config.absolute_root, config=config)
+            if parse_result is not None and not isinstance(parse_result, ScmVersion):
+                raise TypeError(
+                    f"version parse result was {str!r}\n"
+                    "please return a parsed version (ScmVersion)"
+                )
+            return parse_result
+        else:
+            return _entrypoints.version_from_entrypoint(
+                config,
+                entrypoint="setuptools_scm.parse_scm",
+                root=config.absolute_root,
+            )
+    except _run_cmd.CommandNotFoundError as e:
+        _log.exception("command %s not found while parsing the scm, using fallbacks", e)
+        return None
+
+
+def parse_fallback_version(config: Configuration) -> ScmVersion | None:
+    return _entrypoints.version_from_entrypoint(
+        config,
+        entrypoint="setuptools_scm.parse_scm_fallback",
+        root=config.fallback_root,
+    )
+
+
+def parse_version(config: Configuration) -> ScmVersion | None:
+    return (
+        _read_pretended_version_for(config)
+        or parse_scm_version(config)
+        or parse_fallback_version(config)
+    )
+
+
+def write_version_files(
+    config: Configuration, version: str, scm_version: ScmVersion
+) -> None:
+    if config.write_to is not None:
+        from ._integration.dump_version import dump_version
+
+        dump_version(
+            root=config.root,
+            version=version,
+            scm_version=scm_version,
+            write_to=config.write_to,
+            template=config.write_to_template,
+        )
+    if config.version_file:
+        from ._integration.dump_version import write_version_to_path
+
+        version_file = Path(config.version_file)
+        assert not version_file.is_absolute(), f"{version_file=}"
+        # todo: use a better name than fallback root
+        assert config.relative_to is not None
+        target = Path(config.relative_to).parent.joinpath(version_file)
+        write_version_to_path(
+            target,
+            template=config.version_file_template,
+            version=version,
+            scm_version=scm_version,
+        )
+
+
+def _get_version(
+    config: Configuration, force_write_version_files: bool | None = None
+) -> str | None:
+    parsed_version = parse_version(config)
+    if parsed_version is None:
+        return None
+    version_string = _format_version(parsed_version)
+    if force_write_version_files is None:
+        force_write_version_files = True
+        warnings.warn(
+            "force_write_version_files ought to be set,"
+            " presuming the legacy True value",
+            DeprecationWarning,
+        )
+
+    if force_write_version_files:
+        write_version_files(config, version=version_string, scm_version=parsed_version)
+
+    return version_string
+
+
+def _version_missing(config: Configuration) -> NoReturn:
+    raise LookupError(
+        f"setuptools-scm was unable to detect version for {config.absolute_root}.\n\n"
+        "Make sure you're either building from a fully intact git repository "
+        "or PyPI tarballs. Most other sources (such as GitHub's tarballs, a "
+        "git checkout without the .git folder) don't contain the necessary "
+        "metadata and will not work.\n\n"
+        "For example, if you're using pip, instead of "
+        "https://github.com/user/proj/archive/master.zip "
+        "use git+https://github.com/user/proj.git#egg=proj"
+    )
+
+
+def get_version(
+    root: _t.PathT = ".",
+    version_scheme: _t.VERSION_SCHEME = _config.DEFAULT_VERSION_SCHEME,
+    local_scheme: _t.VERSION_SCHEME = _config.DEFAULT_LOCAL_SCHEME,
+    write_to: _t.PathT | None = None,
+    write_to_template: str | None = None,
+    version_file: _t.PathT | None = None,
+    version_file_template: str | None = None,
+    relative_to: _t.PathT | None = None,
+    tag_regex: str | Pattern[str] = _config.DEFAULT_TAG_REGEX,
+    parentdir_prefix_version: str | None = None,
+    fallback_version: str | None = None,
+    fallback_root: _t.PathT = ".",
+    parse: Any | None = None,
+    git_describe_command: _t.CMD_TYPE | None = None,
+    dist_name: str | None = None,
+    version_cls: Any | None = None,
+    normalize: bool = True,
+    search_parent_directories: bool = False,
+) -> str:
+    """
+    If supplied, relative_to should be a file from which root may
+    be resolved. Typically called by a script or module that is not
+    in the root of the repository to direct setuptools_scm to the
+    root of the repository by supplying ``__file__``.
+    """
+
+    version_cls = _validate_version_cls(version_cls, normalize)
+    del normalize
+    tag_regex = parse_tag_regex(tag_regex)
+    config = Configuration(**locals())
+    maybe_version = _get_version(config, force_write_version_files=True)
+
+    if maybe_version is None:
+        _version_missing(config)
+    return maybe_version
+
+
+def parse_tag_regex(tag_regex: str | Pattern[str]) -> Pattern[str]:
+    if isinstance(tag_regex, str):
+        if tag_regex == "":
+            warnings.warn(
+                DeprecationWarning(
+                    "empty regex for tag regex is invalid, using default"
+                )
+            )
+            return _config.DEFAULT_TAG_REGEX
+        else:
+            return re.compile(tag_regex)
+    else:
+        return tag_regex
diff --git a/src/setuptools_scm/_integration/__init__.py b/src/setuptools_scm/_integration/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/src/setuptools_scm/_integration/dump_version.py b/src/setuptools_scm/_integration/dump_version.py
new file mode 100644 (file)
index 0000000..d890243
--- /dev/null
@@ -0,0 +1,94 @@
+from __future__ import annotations
+
+import warnings
+from pathlib import Path
+
+from .. import _types as _t
+from .._log import log as parent_log
+from .._version_cls import _version_as_tuple
+from ..version import ScmVersion
+
+
+log = parent_log.getChild("dump_version")
+
+TEMPLATES = {
+    ".py": """\
+# file generated by setuptools_scm
+# don't change, don't track in version control
+TYPE_CHECKING = False
+if TYPE_CHECKING:
+    from typing import Tuple, Union
+    VERSION_TUPLE = Tuple[Union[int, str], ...]
+else:
+    VERSION_TUPLE = object
+
+version: str
+__version__: str
+__version_tuple__: VERSION_TUPLE
+version_tuple: VERSION_TUPLE
+
+__version__ = version = {version!r}
+__version_tuple__ = version_tuple = {version_tuple!r}
+""",
+    ".txt": "{version}",
+}
+
+
+def dump_version(
+    root: _t.PathT,
+    version: str,
+    write_to: _t.PathT,
+    template: str | None = None,
+    scm_version: ScmVersion | None = None,
+) -> None:
+    assert isinstance(version, str)
+    root = Path(root)
+    write_to = Path(write_to)
+    if write_to.is_absolute():
+        # trigger warning on escape
+        write_to.relative_to(root)
+        warnings.warn(
+            f"{write_to=!s} is a absolute path,"
+            " please switch to using a relative version file",
+            DeprecationWarning,
+        )
+        target = write_to
+    else:
+        target = Path(root).joinpath(write_to)
+    write_version_to_path(
+        target, template=template, version=version, scm_version=scm_version
+    )
+
+
+def _validate_template(target: Path, template: str | None) -> str:
+    if template == "":
+        warnings.warn(f"{template=} looks like a error, using default instead")
+        template = None
+    if template is None:
+        template = TEMPLATES.get(target.suffix)
+
+    if template is None:
+        raise ValueError(
+            f"bad file format: {target.suffix!r} (of {target})\n"
+            "only *.txt and *.py have a default template"
+        )
+    else:
+        return template
+
+
+def write_version_to_path(
+    target: Path, template: str | None, version: str, scm_version: ScmVersion | None
+) -> None:
+    final_template = _validate_template(target, template)
+    log.debug("dump %s into %s", version, target)
+    version_tuple = _version_as_tuple(version)
+    if scm_version is not None:
+        content = final_template.format(
+            version=version,
+            version_tuple=version_tuple,
+            scm_version=scm_version,
+        )
+    else:
+        content = final_template.format(version=version, version_tuple=version_tuple)
+
+    target.write_text(content, encoding="utf-8")
diff --git a/src/setuptools_scm/_integration/pyproject_reading.py b/src/setuptools_scm/_integration/pyproject_reading.py
new file mode 100644 (file)
index 0000000..c9818a2
--- /dev/null
@@ -0,0 +1,85 @@
+from __future__ import annotations
+
+import warnings
+from pathlib import Path
+from typing import NamedTuple
+
+from .. import _log
+from .setuptools import read_dist_name_from_setup_cfg
+from .toml import read_toml_content
+from .toml import TOML_RESULT
+
+
+log = _log.log.getChild("pyproject_reading")
+
+_ROOT = "root"
+
+
+class PyProjectData(NamedTuple):
+    path: Path
+    tool_name: str
+    project: TOML_RESULT
+    section: TOML_RESULT
+
+    @property
+    def project_name(self) -> str | None:
+        return self.project.get("name")
+
+
+def read_pyproject(
+    path: Path = Path("pyproject.toml"),
+    tool_name: str = "setuptools_scm",
+    require_section: bool = True,
+) -> PyProjectData:
+    defn = read_toml_content(path, None if require_section else {})
+    try:
+        section = defn.get("tool", {})[tool_name]
+    except LookupError as e:
+        error = f"{path} does not contain a tool.{tool_name} section"
+        if require_section:
+            raise LookupError(error) from e
+        else:
+            log.warning("toml section missing %r", error)
+            section = {}
+
+    project = defn.get("project", {})
+    return PyProjectData(path, tool_name, project, section)
+
+
+def get_args_for_pyproject(
+    pyproject: PyProjectData,
+    dist_name: str | None,
+    kwargs: TOML_RESULT,
+) -> TOML_RESULT:
+    """drops problematic details and figures the distribution name"""
+    section = pyproject.section.copy()
+    kwargs = kwargs.copy()
+    if "relative_to" in section:
+        relative = section.pop("relative_to")
+        warnings.warn(
+            f"{pyproject.path}: at [tool.{pyproject.tool_name}]\n"
+            f"ignoring value relative_to={relative!r}"
+            " as its always relative to the config file"
+        )
+    if "dist_name" in section:
+        if dist_name is None:
+            dist_name = section.pop("dist_name")
+        else:
+            assert dist_name == section["dist_name"]
+            section.pop("dist_name")
+    if dist_name is None:
+        # minimal pep 621 support for figuring the pretend keys
+        dist_name = pyproject.project_name
+    if dist_name is None:
+        dist_name = read_dist_name_from_setup_cfg()
+    if _ROOT in kwargs:
+        if kwargs[_ROOT] is None:
+            kwargs.pop(_ROOT, None)
+        elif _ROOT in section:
+            if section[_ROOT] != kwargs[_ROOT]:
+                warnings.warn(
+                    f"root {section[_ROOT]} is overridden"
+                    f" by the cli arg {kwargs[_ROOT]}"
+                )
+            section.pop(_ROOT, None)
+    return {"dist_name": dist_name, **section, **kwargs}
diff --git a/src/setuptools_scm/_integration/setuptools.py b/src/setuptools_scm/_integration/setuptools.py
new file mode 100644 (file)
index 0000000..f574d23
--- /dev/null
@@ -0,0 +1,121 @@
+from __future__ import annotations
+
+import logging
+import os
+import warnings
+from typing import Any
+from typing import Callable
+
+import setuptools
+
+from .. import _config
+
+log = logging.getLogger(__name__)
+
+
+def read_dist_name_from_setup_cfg(
+    input: str | os.PathLike[str] = "setup.cfg",
+) -> str | None:
+    # minimal effort to read dist_name off setup.cfg metadata
+    import configparser
+
+    parser = configparser.ConfigParser()
+    parser.read([input], encoding="utf-8")
+    dist_name = parser.get("metadata", "name", fallback=None)
+    return dist_name
+
+
+def _warn_on_old_setuptools(_version: str = setuptools.__version__) -> None:
+    if int(_version.split(".")[0]) < 61:
+        warnings.warn(
+            RuntimeWarning(
+                f"""
+ERROR: setuptools=={_version} is used in combination with setuptools_scm>=8.x
+
+Your build configuration is incomplete and previously worked by accident!
+setuptools_scm requires setuptools>=61
+
+Suggested workaround if applicable:
+ - migrating from the deprecated setup_requires mechanism to pep517/518
+   and using a pyproject.toml to declare build dependencies
+   which are reliably pre-installed before running the build tools
+"""
+            )
+        )
+
+
+def _assign_version(
+    dist: setuptools.Distribution, config: _config.Configuration
+) -> None:
+    from .._get_version_impl import _get_version, _version_missing
+
+    # todo: build time plugin
+    maybe_version = _get_version(config, force_write_version_files=True)
+
+    if maybe_version is None:
+        _version_missing(config)
+    else:
+        assert dist.metadata.version is None
+        dist.metadata.version = maybe_version
+
+
+_warn_on_old_setuptools()
+
+
+def _log_hookstart(hook: str, dist: setuptools.Distribution) -> None:
+    log.debug("%s %r", hook, vars(dist.metadata))
+
+
+def version_keyword(
+    dist: setuptools.Distribution,
+    keyword: str,
+    value: bool | dict[str, Any] | Callable[[], dict[str, Any]],
+) -> None:
+    overrides: dict[str, Any]
+    if value is True:
+        overrides = {}
+    elif callable(value):
+        overrides = value()
+    else:
+        assert isinstance(value, dict), "version_keyword expects a dict or True"
+        overrides = value
+
+    assert (
+        "dist_name" not in overrides
+    ), "dist_name may not be specified in the setup keyword "
+    dist_name: str | None = dist.metadata.name
+    _log_hookstart("version_keyword", dist)
+
+    if dist.metadata.version is not None:
+        warnings.warn(f"version of {dist_name} already set")
+        return
+
+    if dist_name is None:
+        dist_name = read_dist_name_from_setup_cfg()
+
+    config = _config.Configuration.from_file(
+        dist_name=dist_name,
+        _require_section=False,
+        **overrides,
+    )
+    _assign_version(dist, config)
+
+
+def infer_version(dist: setuptools.Distribution) -> None:
+    _log_hookstart("infer_version", dist)
+    log.debug("dist %s %s", id(dist), id(dist.metadata))
+    if dist.metadata.version is not None:
+        return  # metadata already added by hook
+    dist_name = dist.metadata.name
+    if dist_name is None:
+        dist_name = read_dist_name_from_setup_cfg()
+    if not os.path.isfile("pyproject.toml"):
+        return
+    if dist_name == "setuptools_scm":
+        return
+    try:
+        config = _config.Configuration.from_file(dist_name=dist_name)
+    except LookupError as e:
+        log.warning(e)
+    else:
+        _assign_version(dist, config)
diff --git a/src/setuptools_scm/_integration/toml.py b/src/setuptools_scm/_integration/toml.py
new file mode 100644 (file)
index 0000000..a08b7b8
--- /dev/null
@@ -0,0 +1,55 @@
+from __future__ import annotations
+
+import sys
+from pathlib import Path
+from typing import Any
+from typing import Callable
+from typing import cast
+from typing import Dict
+from typing import TYPE_CHECKING
+from typing import TypedDict
+
+if sys.version_info >= (3, 11):
+    from tomllib import loads as load_toml
+else:
+    from tomli import loads as load_toml
+
+if TYPE_CHECKING:
+    from typing_extensions import TypeAlias
+
+from .. import _log
+
+log = _log.log.getChild("toml")
+
+TOML_RESULT: TypeAlias = Dict[str, Any]
+TOML_LOADER: TypeAlias = Callable[[str], TOML_RESULT]
+
+
+def read_toml_content(path: Path, default: TOML_RESULT | None = None) -> TOML_RESULT:
+    try:
+        data = path.read_text(encoding="utf-8")
+    except FileNotFoundError:
+        if default is None:
+            raise
+        else:
+            log.debug("%s missing, presuming default %r", path, default)
+            return default
+    else:
+        return load_toml(data)
+
+
+class _CheatTomlData(TypedDict):
+    cheat: dict[str, Any]
+
+
+def load_toml_or_inline_map(data: str | None) -> dict[str, Any]:
+    """
+    load toml data - with a special hack if only a inline map is given
+    """
+    if not data:
+        return {}
+    elif data[0] == "{":
+        data = "cheat=" + data
+        loaded: _CheatTomlData = cast(_CheatTomlData, load_toml(data))
+        return loaded["cheat"]
+    return load_toml(data)
diff --git a/src/setuptools_scm/_log.py b/src/setuptools_scm/_log.py
new file mode 100644 (file)
index 0000000..1247d46
--- /dev/null
@@ -0,0 +1,85 @@
+"""
+logging helpers, supports vendoring
+"""
+from __future__ import annotations
+
+import contextlib
+import logging
+import os
+import sys
+from typing import IO
+from typing import Iterator
+from typing import Mapping
+
+log = logging.getLogger(__name__.rsplit(".", 1)[0])
+log.propagate = False
+
+
+class AlwaysStdErrHandler(logging.StreamHandler):  # type: ignore[type-arg]
+    def __init___(self) -> None:
+        super().__init__(sys.stderr)
+
+    @property  # type: ignore [override]
+    def stream(self) -> IO[str]:
+        return sys.stderr
+
+    @stream.setter
+    def stream(self, value: IO[str]) -> None:
+        assert value is sys.stderr
+
+
+def make_default_handler() -> logging.Handler:
+    try:
+        from rich.console import Console
+
+        console = Console(stderr=True)
+        from rich.logging import RichHandler
+
+        return RichHandler(console=console)
+    except ImportError:
+        handler = AlwaysStdErrHandler()
+        handler.setFormatter(logging.Formatter("%(levelname)s %(name)s %(message)s"))
+        return handler
+
+
+_default_handler = make_default_handler()
+
+log.addHandler(_default_handler)
+
+
+def _default_log_level(_env: Mapping[str, str] = os.environ) -> int:
+    val: str | None = _env.get("SETUPTOOLS_SCM_DEBUG")
+    return logging.WARN if val is None else logging.DEBUG
+
+
+log.setLevel(_default_log_level())
+
+
+@contextlib.contextmanager
+def defer_to_pytest() -> Iterator[None]:
+    log.propagate = True
+    old_level = log.level
+    log.setLevel(logging.NOTSET)
+    log.removeHandler(_default_handler)
+    try:
+        yield
+    finally:
+        log.addHandler(_default_handler)
+        log.propagate = False
+        log.setLevel(old_level)
+
+
+@contextlib.contextmanager
+def enable_debug(handler: logging.Handler = _default_handler) -> Iterator[None]:
+    log.addHandler(handler)
+    old_level = log.level
+    log.setLevel(logging.DEBUG)
+    old_handler_level = handler.level
+    handler.setLevel(logging.DEBUG)
+    try:
+        yield
+    finally:
+        log.setLevel(old_level)
+        handler.setLevel(old_handler_level)
+        if handler is not _default_handler:
+            log.removeHandler(handler)
diff --git a/src/setuptools_scm/_modify_version.py b/src/setuptools_scm/_modify_version.py
new file mode 100644 (file)
index 0000000..63c0dfd
--- /dev/null
@@ -0,0 +1,61 @@
+from __future__ import annotations
+
+import re
+
+from . import _types as _t
+
+
+def strip_local(version_string: str) -> str:
+    public, sep, local = version_string.partition("+")
+    return public
+
+
+def _add_post(version: str) -> str:
+    if "post" in version:
+        raise ValueError(
+            f"{version} already is a post release, refusing to guess the update"
+        )
+    return f"{version}.post1"
+
+
+def _bump_dev(version: str) -> str | None:
+    if ".dev" not in version:
+        return None
+
+    prefix, tail = version.rsplit(".dev", 1)
+    if tail != "0":
+        raise ValueError(
+            "choosing custom numbers for the `.devX` distance "
+            "is not supported.\n "
+            f"The {version} can't be bumped\n"
+            "Please drop the tag or create a new supported one ending in .dev0"
+        )
+    return prefix
+
+
+def _bump_regex(version: str) -> str:
+    match = re.match(r"(.*?)(\d+)$", version)
+    if match is None:
+        raise ValueError(
+            f"{version} does not end with a number to bump, "
+            "please correct or use a custom version scheme"
+        )
+    else:
+        prefix, tail = match.groups()
+        return f"{prefix}{int(tail) + 1}"
+
+
+def _format_local_with_time(version: _t.SCMVERSION, time_format: str) -> str:
+    if version.exact or version.node is None:
+        return version.format_choice(
+            "", "+d{time:{time_format}}", time_format=time_format
+        )
+    else:
+        return version.format_choice(
+            "+{node}", "+{node}.d{time:{time_format}}", time_format=time_format
+        )
+
+
+def _dont_guess_next_version(tag_version: _t.SCMVERSION) -> str:
+    version = strip_local(str(tag_version.tag))
+    return _bump_dev(version) or _add_post(version)
diff --git a/src/setuptools_scm/_overrides.py b/src/setuptools_scm/_overrides.py
new file mode 100644 (file)
index 0000000..792bfd2
--- /dev/null
@@ -0,0 +1,54 @@
+from __future__ import annotations
+
+import os
+import re
+from typing import Any
+
+from . import _config
+from . import _log
+from . import version
+from ._integration.toml import load_toml_or_inline_map
+
+log = _log.log.getChild("overrides")
+
+PRETEND_KEY = "SETUPTOOLS_SCM_PRETEND_VERSION"
+PRETEND_KEY_NAMED = PRETEND_KEY + "_FOR_{name}"
+
+
+def read_named_env(
+    *, tool: str = "SETUPTOOLS_SCM", name: str, dist_name: str | None
+) -> str | None:
+    """ """
+    if dist_name is not None:
+        # Normalize the dist name as per PEP 503.
+        normalized_dist_name = re.sub(r"[-_.]+", "-", dist_name)
+        env_var_dist_name = normalized_dist_name.replace("-", "_").upper()
+        val = os.environ.get(f"{tool}_{name}_FOR_{env_var_dist_name}")
+        if val is not None:
+            return val
+    return os.environ.get(f"{tool}_{name}")
+
+
+def _read_pretended_version_for(
+    config: _config.Configuration,
+) -> version.ScmVersion | None:
+    """read a a overridden version from the environment
+
+    tries ``SETUPTOOLS_SCM_PRETEND_VERSION``
+    and ``SETUPTOOLS_SCM_PRETEND_VERSION_FOR_$UPPERCASE_DIST_NAME``
+    """
+    log.debug("dist name: %s", config.dist_name)
+
+    pretended = read_named_env(name="PRETEND_VERSION", dist_name=config.dist_name)
+
+    if pretended:
+        # we use meta here since the pretended version
+        # must adhere to the pep to begin with
+        return version.meta(tag=pretended, preformatted=True, config=config)
+    else:
+        return None
+
+
+def read_toml_overrides(dist_name: str | None) -> dict[str, Any]:
+    data = read_named_env(name="OVERRIDES", dist_name=dist_name)
+    return load_toml_or_inline_map(data)
diff --git a/src/setuptools_scm/_run_cmd.py b/src/setuptools_scm/_run_cmd.py
new file mode 100644 (file)
index 0000000..1b80d28
--- /dev/null
@@ -0,0 +1,207 @@
+from __future__ import annotations
+
+import os
+import shlex
+import subprocess
+import textwrap
+import warnings
+from typing import Callable
+from typing import Final
+from typing import Mapping
+from typing import overload
+from typing import Sequence
+from typing import TYPE_CHECKING
+from typing import TypeVar
+
+from . import _log
+from . import _types as _t
+
+if TYPE_CHECKING:
+    BaseCompletedProcess = subprocess.CompletedProcess[str]
+else:
+    BaseCompletedProcess = subprocess.CompletedProcess
+
+# pick 40 seconds
+# unfortunately github CI for windows sometimes needs
+# up to 30 seconds to start a command
+
+BROKEN_TIMEOUT: Final[int] = 40
+
+log = _log.log.getChild("run_cmd")
+
+PARSE_RESULT = TypeVar("PARSE_RESULT")
+T = TypeVar("T")
+
+
+class CompletedProcess(BaseCompletedProcess):
+    @classmethod
+    def from_raw(
+        cls, input: BaseCompletedProcess, strip: bool = True
+    ) -> CompletedProcess:
+        return cls(
+            args=input.args,
+            returncode=input.returncode,
+            stdout=input.stdout.strip() if strip and input.stdout else input.stdout,
+            stderr=input.stderr.strip() if strip and input.stderr else input.stderr,
+        )
+
+    @overload
+    def parse_success(
+        self,
+        parse: Callable[[str], PARSE_RESULT],
+        default: None = None,
+        error_msg: str | None = None,
+    ) -> PARSE_RESULT | None:
+        ...
+
+    @overload
+    def parse_success(
+        self,
+        parse: Callable[[str], PARSE_RESULT],
+        default: T,
+        error_msg: str | None = None,
+    ) -> PARSE_RESULT | T:
+        ...
+
+    def parse_success(
+        self,
+        parse: Callable[[str], PARSE_RESULT],
+        default: T | None = None,
+        error_msg: str | None = None,
+    ) -> PARSE_RESULT | T | None:
+        if self.returncode:
+            if error_msg:
+                log.warning("%s %s", error_msg, self)
+            return default
+        else:
+            return parse(self.stdout)
+
+
+def no_git_env(env: Mapping[str, str]) -> dict[str, str]:
+    # adapted from pre-commit
+    # Too many bugs dealing with environment variables and GIT:
+    # https://github.com/pre-commit/pre-commit/issues/300
+    # In git 2.6.3 (maybe others), git exports GIT_WORK_TREE while running
+    # pre-commit hooks
+    # In git 1.9.1 (maybe others), git exports GIT_DIR and GIT_INDEX_FILE
+    # while running pre-commit hooks in submodules.
+    # GIT_DIR: Causes git clone to clone wrong thing
+    # GIT_INDEX_FILE: Causes 'error invalid object ...' during commit
+    for k, v in env.items():
+        if k.startswith("GIT_"):
+            log.debug("%s: %s", k, v)
+    return {
+        k: v
+        for k, v in env.items()
+        if not k.startswith("GIT_")
+        or k in ("GIT_EXEC_PATH", "GIT_SSH", "GIT_SSH_COMMAND")
+    }
+
+
+def avoid_pip_isolation(env: Mapping[str, str]) -> dict[str, str]:
+    """
+    pip build isolation can break Mercurial
+    (see https://github.com/pypa/pip/issues/10635)
+
+    pip uses PYTHONNOUSERSITE and a path in PYTHONPATH containing "pip-build-env-".
+    """
+    new_env = {k: v for k, v in env.items() if k != "PYTHONNOUSERSITE"}
+    if "PYTHONPATH" not in new_env:
+        return new_env
+
+    new_env["PYTHONPATH"] = os.pathsep.join(
+        [
+            path
+            for path in new_env["PYTHONPATH"].split(os.pathsep)
+            if "pip-build-env-" not in path
+        ]
+    )
+    return new_env
+
+
+def ensure_stripped_str(str_or_bytes: str | bytes) -> str:
+    if isinstance(str_or_bytes, str):
+        return str_or_bytes.strip()
+    else:
+        return str_or_bytes.decode("utf-8", "surrogateescape").strip()
+
+
+def run(
+    cmd: _t.CMD_TYPE,
+    cwd: _t.PathT,
+    *,
+    strip: bool = True,
+    trace: bool = True,
+    timeout: int = BROKEN_TIMEOUT,
+    check: bool = False,
+) -> CompletedProcess:
+    if isinstance(cmd, str):
+        cmd = shlex.split(cmd)
+    else:
+        cmd = [os.fspath(x) for x in cmd]
+    cmd_4_trace = " ".join(map(_unsafe_quote_for_display, cmd))
+    log.debug("at %s\n    $ %s ", cwd, cmd_4_trace)
+    res = subprocess.run(
+        cmd,
+        capture_output=True,
+        cwd=os.fspath(cwd),
+        env=dict(
+            avoid_pip_isolation(no_git_env(os.environ)),
+            # os.environ,
+            # try to disable i18n, but still allow UTF-8 encoded text.
+            LC_ALL="C.UTF-8",
+            LANGUAGE="",
+            HGPLAIN="1",
+        ),
+        text=True,
+        timeout=timeout,
+    )
+
+    res = CompletedProcess.from_raw(res, strip=strip)
+    if trace:
+        if res.stdout:
+            log.debug("out:\n%s", textwrap.indent(res.stdout, "    "))
+        if res.stderr:
+            log.debug("err:\n%s", textwrap.indent(res.stderr, "    "))
+        if res.returncode:
+            log.debug("ret: %s", res.returncode)
+    if check:
+        res.check_returncode()
+    return res
+
+
+def _unsafe_quote_for_display(item: _t.PathT) -> str:
+    # give better results than shlex.join in our cases
+    text = os.fspath(item)
+    return text if all(c not in text for c in " {[:") else f'"{text}"'
+
+
+def has_command(
+    name: str, args: Sequence[str] = ["version"], warn: bool = True
+) -> bool:
+    try:
+        p = run([name, *args], cwd=".", timeout=BROKEN_TIMEOUT)
+        if p.returncode != 0:
+            log.error(f"Command '{name}' returned non-zero. This is stderr:")
+            log.error(p.stderr)
+    except OSError as e:
+        log.warning("command %s missing: %s", name, e)
+        res = False
+    except subprocess.TimeoutExpired as e:
+        log.warning("command %s timed out %s", name, e)
+        res = False
+
+    else:
+        res = not p.returncode
+    if not res and warn:
+        warnings.warn("%r was not found" % name, category=RuntimeWarning)
+    return res
+
+
+class CommandNotFoundError(LookupError, FileNotFoundError):
+    pass
+
+
+def require_command(name: str) -> None:
+    if not has_command(name, warn=False):
+        raise CommandNotFoundError(name)
diff --git a/src/setuptools_scm/_types.py b/src/setuptools_scm/_types.py
new file mode 100644 (file)
index 0000000..df8fa94
--- /dev/null
@@ -0,0 +1,22 @@
+from __future__ import annotations
+
+import os
+from typing import Callable
+from typing import List
+from typing import Sequence
+from typing import Tuple
+from typing import TYPE_CHECKING
+from typing import Union
+
+
+if TYPE_CHECKING:
+    from typing_extensions import TypeAlias
+    from . import version
+
+PathT: TypeAlias = Union["os.PathLike[str]", str]
+
+CMD_TYPE: TypeAlias = Union[Sequence[PathT], str]
+
+VERSION_SCHEME: TypeAlias = Union[str, Callable[["version.ScmVersion"], str]]
+VERSION_SCHEMES: TypeAlias = Union[List[str], Tuple[str, ...], VERSION_SCHEME]
+SCMVERSION: TypeAlias = "version.ScmVersion"
diff --git a/src/setuptools_scm/_version_cls.py b/src/setuptools_scm/_version_cls.py
new file mode 100644 (file)
index 0000000..3fd4a32
--- /dev/null
@@ -0,0 +1,91 @@
+from __future__ import annotations
+
+from typing import cast
+from typing import Type
+from typing import Union
+
+try:
+    from packaging.version import InvalidVersion
+    from packaging.version import Version as Version
+except ImportError:
+    from setuptools.extern.packaging.version import InvalidVersion  # type: ignore
+    from setuptools.extern.packaging.version import Version as Version  # type: ignore
+from . import _log
+
+log = _log.log.getChild("version_cls")
+
+
+class NonNormalizedVersion(Version):
+    """A non-normalizing version handler.
+
+    You can use this class to preserve version verification but skip normalization.
+    For example you can use this to avoid git release candidate version tags
+    ("1.0.0-rc1") to be normalized to "1.0.0rc1". Only use this if you fully
+    trust the version tags.
+    """
+
+    def __init__(self, version: str) -> None:
+        # parse and validate using parent
+        super().__init__(version)
+
+        # store raw for str
+        self._raw_version = version
+
+    def __str__(self) -> str:
+        # return the non-normalized version (parent returns the normalized)
+        return self._raw_version
+
+    def __repr__(self) -> str:
+        # same pattern as parent
+        return f"<NonNormalizedVersion({self._raw_version!r})>"
+
+
+def _version_as_tuple(version_str: str) -> tuple[int | str, ...]:
+    try:
+        parsed_version = Version(version_str)
+    except InvalidVersion as e:
+        log.error("failed to parse version %s: %s", e, version_str)
+        return (version_str,)
+    else:
+        version_fields: tuple[int | str, ...] = parsed_version.release
+        if parsed_version.dev is not None:
+            version_fields += (f"dev{parsed_version.dev}",)
+        if parsed_version.local is not None:
+            version_fields += (parsed_version.local,)
+        return version_fields
+
+
+_VersionT = Union[Version, NonNormalizedVersion]
+
+
+def import_name(name: str) -> object:
+    import importlib
+
+    pkg_name, cls_name = name.rsplit(".", 1)
+    pkg = importlib.import_module(pkg_name)
+    return getattr(pkg, cls_name)
+
+
+def _validate_version_cls(
+    version_cls: type[_VersionT] | str | None, normalize: bool
+) -> type[_VersionT]:
+    if not normalize:
+        if version_cls is not None:
+            raise ValueError(
+                "Providing a custom `version_cls` is not permitted when "
+                "`normalize=False`"
+            )
+        return NonNormalizedVersion
+    else:
+        # Use `version_cls` if provided, default to packaging or pkg_resources
+        if version_cls is None:
+            return Version
+        elif isinstance(version_cls, str):
+            try:
+                return cast(Type[_VersionT], import_name(version_cls))
+            except:  # noqa
+                raise ValueError(
+                    f"Unable to import version_cls='{version_cls}'"
+                ) from None
+        else:
+            return version_cls
diff --git a/src/setuptools_scm/config.py b/src/setuptools_scm/config.py
deleted file mode 100644 (file)
index e7f4d72..0000000
+++ /dev/null
@@ -1,125 +0,0 @@
-""" configuration """
-from __future__ import print_function, unicode_literals
-import os
-import re
-import warnings
-
-from .utils import trace
-
-DEFAULT_TAG_REGEX = r"^(?:[\w-]+-)?(?P<version>[vV]?\d+(?:\.\d+){0,2}[^\+]*)(?:\+.*)?$"
-DEFAULT_VERSION_SCHEME = "guess-next-dev"
-DEFAULT_LOCAL_SCHEME = "node-and-date"
-
-
-def _check_tag_regex(value):
-    if not value:
-        value = DEFAULT_TAG_REGEX
-    regex = re.compile(value)
-
-    group_names = regex.groupindex.keys()
-    if regex.groups == 0 or (regex.groups > 1 and "version" not in group_names):
-        warnings.warn(
-            "Expected tag_regex to contain a single match group or a group named"
-            " 'version' to identify the version part of any tag."
-        )
-
-    return regex
-
-
-def _check_absolute_root(root, relative_to):
-    if relative_to:
-        if os.path.isabs(root) and not root.startswith(relative_to):
-            warnings.warn(
-                "absolute root path '%s' overrides relative_to '%s'"
-                % (root, relative_to)
-            )
-        root = os.path.join(os.path.dirname(relative_to), root)
-    return os.path.abspath(root)
-
-
-class Configuration(object):
-    """ Global configuration model """
-
-    def __init__(
-        self,
-        relative_to=None,
-        root=".",
-        version_scheme=DEFAULT_VERSION_SCHEME,
-        local_scheme=DEFAULT_LOCAL_SCHEME,
-        write_to=None,
-        write_to_template=None,
-        tag_regex=DEFAULT_TAG_REGEX,
-        parentdir_prefix_version=None,
-        fallback_version=None,
-        fallback_root=".",
-        parse=None,
-        git_describe_command=None,
-    ):
-        # TODO:
-        self._relative_to = relative_to
-        self._root = "."
-
-        self.root = root
-        self.version_scheme = version_scheme
-        self.local_scheme = local_scheme
-        self.write_to = write_to
-        self.write_to_template = write_to_template
-        self.parentdir_prefix_version = parentdir_prefix_version
-        self.fallback_version = fallback_version
-        self.fallback_root = fallback_root
-        self.parse = parse
-        self.tag_regex = tag_regex
-        self.git_describe_command = git_describe_command
-
-    @property
-    def fallback_root(self):
-        return self._fallback_root
-
-    @fallback_root.setter
-    def fallback_root(self, value):
-        self._fallback_root = os.path.abspath(value)
-
-    @property
-    def absolute_root(self):
-        return self._absolute_root
-
-    @property
-    def relative_to(self):
-        return self._relative_to
-
-    @relative_to.setter
-    def relative_to(self, value):
-        self._absolute_root = _check_absolute_root(self._root, value)
-        self._relative_to = value
-        trace("root", repr(self._absolute_root))
-
-    @property
-    def root(self):
-        return self._root
-
-    @root.setter
-    def root(self, value):
-        self._absolute_root = _check_absolute_root(value, self._relative_to)
-        self._root = value
-        trace("root", repr(self._absolute_root))
-
-    @property
-    def tag_regex(self):
-        return self._tag_regex
-
-    @tag_regex.setter
-    def tag_regex(self, value):
-        self._tag_regex = _check_tag_regex(value)
-
-    @classmethod
-    def from_file(cls, name="pyproject.toml"):
-        """
-        Read Configuration from pyproject.toml (or similar).
-        Raises exceptions when file is not found or toml is
-        not installed or the file has invalid format or does
-        not contain the [tool.setuptools_scm] section.
-        """
-        with open(name) as strm:
-            defn = __import__("toml").load(strm)
-        section = defn.get("tool", {})["setuptools_scm"]
-        return cls(**section)
index 019f1c5b995373decb22679312ed1154081b71d4..b12b2f1234216291b3c8b4532d44899cd1420c7c 100644 (file)
@@ -1,13 +1,69 @@
+from __future__ import annotations
+
 import os
-from pkg_resources import iter_entry_points
-from .utils import trace
-
-
-def iter_matching_entrypoints(path, entrypoint):
-    trace("looking for ep", entrypoint, path)
-    for ep in iter_entry_points(entrypoint):
-        if os.path.exists(os.path.join(path, ep.name)):
-            if os.path.isabs(ep.name):
-                trace("ignoring bad ep", ep)
-            trace("found ep", ep)
-            yield ep
+from pathlib import Path
+from typing import Iterable
+from typing import Iterator
+
+from . import _entrypoints
+from . import _log
+from . import _types as _t
+from ._config import Configuration
+
+log = _log.log.getChild("discover")
+
+
+def walk_potential_roots(root: _t.PathT, search_parents: bool = True) -> Iterator[Path]:
+    """
+    Iterate though a path and each of its parents.
+    :param root: File path.
+    :param search_parents: If ``False`` the parents are not considered.
+    """
+    root = Path(root)
+    yield root
+    if search_parents:
+        yield from root.parents
+
+
+def match_entrypoint(root: _t.PathT, name: str) -> bool:
+    """
+    Consider a ``root`` as entry-point.
+    :param root: File path.
+    :param name: Subdirectory name.
+    :return: ``True`` if a subdirectory ``name`` exits in ``root``.
+    """
+
+    if os.path.exists(os.path.join(root, name)):
+        if not os.path.isabs(name):
+            return True
+        log.debug("ignoring bad ep %s", name)
+
+    return False
+
+
+# blocked entrypints from legacy plugins
+_BLOCKED_EP_TARGETS = {"setuptools_scm_git_archive:parse"}
+
+
+def iter_matching_entrypoints(
+    root: _t.PathT, entrypoint: str, config: Configuration
+) -> Iterable[_entrypoints.EntryPoint]:
+    """
+    Consider different entry-points in ``root`` and optionally its parents.
+    :param root: File path.
+    :param entrypoint: Entry-point to consider.
+    :param config: Configuration,
+        read ``search_parent_directories``, write found parent to ``parent``.
+    """
+
+    log.debug("looking for ep %s in %s", entrypoint, root)
+    from ._entrypoints import iter_entry_points
+
+    for wd in walk_potential_roots(root, config.search_parent_directories):
+        for ep in iter_entry_points(entrypoint):
+            if ep.value in _BLOCKED_EP_TARGETS:
+                continue
+            if match_entrypoint(wd, ep.name):
+                log.debug("found ep %s in %s", ep, wd)
+                config.parent = wd
+                yield ep
diff --git a/src/setuptools_scm/fallbacks.py b/src/setuptools_scm/fallbacks.py
new file mode 100644 (file)
index 0000000..e1ea60c
--- /dev/null
@@ -0,0 +1,44 @@
+from __future__ import annotations
+
+import logging
+import os
+from pathlib import Path
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+    from . import _types as _t
+from . import Configuration
+from .integration import data_from_mime
+from .version import meta
+from .version import ScmVersion
+from .version import tag_to_version
+
+log = logging.getLogger(__name__)
+
+_UNKNOWN = "UNKNOWN"
+
+
+def parse_pkginfo(root: _t.PathT, config: Configuration) -> ScmVersion | None:
+    pkginfo = Path(root) / "PKG-INFO"
+    log.debug("pkginfo %s", pkginfo)
+    data = data_from_mime(pkginfo)
+    version = data.get("Version", _UNKNOWN)
+    if version != _UNKNOWN:
+        return meta(version, preformatted=True, config=config)
+    else:
+        return None
+
+
+def fallback_version(root: _t.PathT, config: Configuration) -> ScmVersion | None:
+    if config.parentdir_prefix_version is not None:
+        _, parent_name = os.path.split(os.path.abspath(root))
+        if parent_name.startswith(config.parentdir_prefix_version):
+            version = tag_to_version(
+                parent_name[len(config.parentdir_prefix_version) :], config
+            )
+            if version is not None:
+                return meta(str(version), preformatted=True, config=config)
+    if config.fallback_version is not None:
+        log.debug("FALLBACK %s", config.fallback_version)
+        return meta(config.fallback_version, preformatted=True, config=config)
+    return None
diff --git a/src/setuptools_scm/file_finder.py b/src/setuptools_scm/file_finder.py
deleted file mode 100644 (file)
index 77ec146..0000000
+++ /dev/null
@@ -1,55 +0,0 @@
-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
deleted file mode 100644 (file)
index 9aa6245..0000000
+++ /dev/null
@@ -1,69 +0,0 @@
-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
deleted file mode 100644 (file)
index 2aa1e16..0000000
+++ /dev/null
@@ -1,47 +0,0 @@
-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)
index afefa348091824da007dd9d24e7f056fd6b492e5..d511961cbff32bad244c1e4e0bcd5918f0b30f27 100644 (file)
-from .config import Configuration
-from .utils import do_ex, trace, has_command
-from .version import meta
+from __future__ import annotations
 
-from os.path import isfile, join
+import dataclasses
+import logging
+import os
+import re
+import shlex
 import warnings
+from datetime import date
+from datetime import datetime
+from datetime import timezone
+from os.path import samefile
+from pathlib import Path
+from typing import Callable
+from typing import Sequence
+from typing import TYPE_CHECKING
+
+from . import _types as _t
+from . import Configuration
+from . import discover
+from ._run_cmd import CompletedProcess as _CompletedProcess
+from ._run_cmd import require_command as _require_command
+from ._run_cmd import run as _run
+from .integration import data_from_mime
+from .scm_workdir import Workdir
+from .version import meta
+from .version import ScmVersion
+from .version import tag_to_version
+
+if TYPE_CHECKING:
+    from . import hg_git
+log = logging.getLogger(__name__)
+
+REF_TAG_RE = re.compile(r"(?<=\btag: )([^,]+)\b")
+DESCRIBE_UNSUPPORTED = "%(describe"
+
+# If testing command in shell make sure to quote the match argument like
+# '*[0-9]*' as it will expand before being sent to git if there are any matching
+# files in current directory.
+DEFAULT_DESCRIBE = [
+    "git",
+    "describe",
+    "--dirty",
+    "--tags",
+    "--long",
+    "--match",
+    "*[0-9]*",
+]
+
+
+def run_git(
+    args: Sequence[str | os.PathLike[str]],
+    repo: Path,
+    *,
+    check: bool = False,
+    timeout: int = 20,
+) -> _CompletedProcess:
+    return _run(
+        ["git", "--git-dir", repo / ".git", *args],
+        cwd=repo,
+        check=check,
+        timeout=timeout,
+    )
+
+
+class GitWorkdir(Workdir):
+    """experimental, may change at any time"""
 
+    @classmethod
+    def from_potential_worktree(cls, wd: _t.PathT) -> GitWorkdir | None:
+        wd = Path(wd).resolve()
+        real_wd = run_git(["rev-parse", "--show-prefix"], wd).parse_success(parse=str)
+        if real_wd is None:
+            return None
+        else:
+            real_wd = real_wd[:-1]  # remove the trailing pathsep
 
-try:
-    from os.path import samefile
-except ImportError:
-    from .win_py31_compat import samefile
-
-
-DEFAULT_DESCRIBE = "git describe --dirty --tags --long --match *.*"
-
-
-class GitWorkdir(object):
-    """experimental, may change at any time"""
+        if not real_wd:
+            real_wd = os.fspath(wd)
+        else:
+            str_wd = os.fspath(wd)
+            assert str_wd.replace("\\", "/").endswith(real_wd)
+            # In windows wd contains ``\`` which should be replaced by ``/``
+            # for this assertion to work.  Length of string isn't changed by replace
+            # ``\\`` is just and escape for `\`
+            real_wd = str_wd[: -len(real_wd)]
+        log.debug("real root %s", real_wd)
+        if not samefile(real_wd, wd):
+            return None
 
-    def __init__(self, path):
-        self.path = path
+        return cls(Path(real_wd))
 
-    def do_ex(self, cmd):
-        return do_ex(cmd, cwd=self.path)
+    def is_dirty(self) -> bool:
+        return run_git(
+            ["status", "--porcelain", "--untracked-files=no"], self.path
+        ).parse_success(
+            parse=bool,
+            default=False,
+        )
 
-    @classmethod
-    def from_potential_worktree(cls, wd):
-        real_wd, _, ret = do_ex("git rev-parse --show-toplevel", wd)
-        if ret:
-            return
-        trace("real root", real_wd)
-        if not samefile(real_wd, wd):
-            return
+    def get_branch(self) -> str | None:
+        return run_git(
+            ["rev-parse", "--abbrev-ref", "HEAD"],
+            self.path,
+        ).parse_success(
+            parse=str,
+            error_msg="branch err (abbrev-err)",
+        ) or run_git(
+            ["symbolic-ref", "--short", "HEAD"],
+            self.path,
+        ).parse_success(
+            parse=str,
+            error_msg="branch err (symbolic-ref)",
+        )
 
-        return cls(real_wd)
+    def get_head_date(self) -> date | None:
+        def parse_timestamp(timestamp_text: str) -> date | None:
+            if "%c" in timestamp_text:
+                log.warning("git too old -> timestamp is %r", timestamp_text)
+                return None
+            return datetime.fromisoformat(timestamp_text).date()
+
+        res = run_git(
+            [
+                *("-c", "log.showSignature=false"),
+                *("log", "-n", "1", "HEAD"),
+                "--format=%cI",
+            ],
+            self.path,
+        )
+        return res.parse_success(
+            parse=parse_timestamp,
+            error_msg="logging the iso date for head failed",
+        )
 
-    def is_dirty(self):
-        out, _, _ = self.do_ex("git status --porcelain --untracked-files=no")
-        return bool(out)
+    def is_shallow(self) -> bool:
+        return self.path.joinpath(".git/shallow").is_file()
 
-    def get_branch(self):
-        branch, err, ret = self.do_ex("git rev-parse --abbrev-ref HEAD")
-        if ret:
-            trace("branch err", branch, err, ret)
-            return
-        return branch
+    def fetch_shallow(self) -> None:
+        run_git(["fetch", "--unshallow"], self.path, check=True, timeout=240)
 
-    def is_shallow(self):
-        return isfile(join(self.path, ".git/shallow"))
+    def node(self) -> str | None:
+        def _unsafe_short_node(node: str) -> str:
+            return node[:7]
 
-    def fetch_shallow(self):
-        self.do_ex("git fetch --unshallow")
+        return run_git(
+            ["rev-parse", "--verify", "--quiet", "HEAD"], self.path
+        ).parse_success(
+            parse=_unsafe_short_node,
+        )
 
-    def node(self):
-        rev_node, _, ret = self.do_ex("git rev-parse --verify --quiet HEAD")
-        if not ret:
-            return rev_node[:7]
+    def count_all_nodes(self) -> int:
+        res = run_git(["rev-list", "HEAD"], self.path)
+        return res.stdout.count("\n") + 1
 
-    def count_all_nodes(self):
-        revs, _, _ = self.do_ex("git rev-list HEAD")
-        return revs.count("\n") + 1
+    def default_describe(self) -> _CompletedProcess:
+        return run_git(DEFAULT_DESCRIBE[1:], self.path)
 
 
-def warn_on_shallow(wd):
+def warn_on_shallow(wd: GitWorkdir) -> None:
     """experimental, may change at any time"""
     if wd.is_shallow():
-        warnings.warn('"{}" is shallow and may cause errors'.format(wd.path))
+        warnings.warn(f'"{wd.path}" is shallow and may cause errors')
 
 
-def fetch_on_shallow(wd):
+def fetch_on_shallow(wd: GitWorkdir) -> None:
     """experimental, may change at any time"""
     if wd.is_shallow():
-        warnings.warn('"%s" was shallow, git fetch was used to rectify')
+        warnings.warn(f'"{wd.path}" was shallow, git fetch was used to rectify')
         wd.fetch_shallow()
 
 
-def fail_on_shallow(wd):
+def fail_on_shallow(wd: GitWorkdir) -> None:
     """experimental, may change at any time"""
     if wd.is_shallow():
         raise ValueError(
-            "%r is shallow, please correct with " '"git fetch --unshallow"' % wd.path
+            f'{wd.path} is shallow, please correct with "git fetch --unshallow"'
         )
 
 
+def get_working_directory(config: Configuration, root: _t.PathT) -> GitWorkdir | None:
+    """
+    Return the working directory (``GitWorkdir``).
+    """
+
+    if config.parent:  # todo broken
+        return GitWorkdir.from_potential_worktree(config.parent)
+
+    for potential_root in discover.walk_potential_roots(
+        root, search_parents=config.search_parent_directories
+    ):
+        potential_wd = GitWorkdir.from_potential_worktree(potential_root)
+        if potential_wd is not None:
+            return potential_wd
+
+    return GitWorkdir.from_potential_worktree(root)
+
+
 def parse(
-    root, describe_command=DEFAULT_DESCRIBE, pre_parse=warn_on_shallow, config=None
-):
+    root: _t.PathT,
+    config: Configuration,
+    describe_command: str | list[str] | None = None,
+    pre_parse: Callable[[GitWorkdir], None] = warn_on_shallow,
+) -> ScmVersion | None:
     """
     :param pre_parse: experimental pre_parse action, may change at any time
     """
-    if not config:
-        config = Configuration(root=root)
+    _require_command("git")
+    wd = get_working_directory(config, root)
+    if wd:
+        return _git_parse_inner(
+            config, wd, describe_command=describe_command, pre_parse=pre_parse
+        )
+    else:
+        return None
 
-    if not has_command("git"):
-        return
 
-    wd = GitWorkdir.from_potential_worktree(config.absolute_root)
-    if wd is None:
-        return
-    if pre_parse:
-        pre_parse(wd)
+def version_from_describe(
+    wd: GitWorkdir | hg_git.GitWorkdirHgClient,
+    config: Configuration,
+    describe_command: _t.CMD_TYPE | None,
+) -> ScmVersion | None:
+    pass
 
-    if config.git_describe_command:
+    if config.git_describe_command is not None:
         describe_command = config.git_describe_command
 
-    out, unused_err, ret = wd.do_ex(describe_command)
-    if ret:
-        # If 'git git_describe_command' failed, try to get the information otherwise.
-        rev_node = wd.node()
-        dirty = wd.is_dirty()
+    if describe_command is not None:
+        if isinstance(describe_command, str):
+            describe_command = shlex.split(describe_command)
+            # todo: figure how to ensure git with gitdir gets correctly invoked
+        if describe_command[0] == "git":
+            describe_res = run_git(describe_command[1:], wd.path)
+        else:
+            describe_res = _run(describe_command, wd.path)
+    else:
+        describe_res = wd.default_describe()
 
-        if rev_node is None:
-            return meta("0.0", distance=0, dirty=dirty, config=config)
+    def parse_describe(output: str) -> ScmVersion:
+        tag, distance, node, dirty = _git_parse_describe(output)
+        return meta(tag=tag, distance=distance, dirty=dirty, node=node, config=config)
 
-        return meta(
-            "0.0",
-            distance=wd.count_all_nodes(),
-            node="g" + rev_node,
-            dirty=dirty,
-            branch=wd.get_branch(),
-            config=config,
-        )
-    else:
-        tag, number, node, dirty = _git_parse_describe(out)
-
-        branch = wd.get_branch()
-        if number:
-            return meta(
-                tag,
-                config=config,
-                distance=number,
-                node=node,
-                dirty=dirty,
-                branch=branch,
-            )
+    return describe_res.parse_success(parse=parse_describe)
+
+
+def _git_parse_inner(
+    config: Configuration,
+    wd: GitWorkdir | hg_git.GitWorkdirHgClient,
+    pre_parse: None | (Callable[[GitWorkdir | hg_git.GitWorkdirHgClient], None]) = None,
+    describe_command: _t.CMD_TYPE | None = None,
+) -> ScmVersion:
+    if pre_parse:
+        pre_parse(wd)
+
+    version = version_from_describe(wd, config, describe_command)
+
+    if version is None:
+        # If 'git git_describe_command' failed, try to get the information otherwise.
+        tag = config.version_cls("0.0")
+        node = wd.node()
+        if node is None:
+            distance = 0
+            dirty = True
         else:
-            return meta(tag, config=config, node=node, dirty=dirty, branch=branch)
+            distance = wd.count_all_nodes()
+            node = "g" + node
+            dirty = wd.is_dirty()
+        version = meta(
+            tag=tag, distance=distance, dirty=dirty, node=node, config=config
+        )
+    branch = wd.get_branch()
+    node_date = wd.get_head_date() or datetime.now(timezone.utc).date()
+    return dataclasses.replace(version, branch=branch, node_date=node_date)
 
 
-def _git_parse_describe(describe_output):
+def _git_parse_describe(
+    describe_output: str,
+) -> tuple[str, int, str | None, bool]:
     # 'describe_output' looks e.g. like 'v1.5.0-0-g4060507' or
     # 'v1.15.1rc1-37-g9bd1298-dirty'.
+    # It may also just be a bare tag name if this is a tagged commit and we are
+    # parsing a .git_archival.txt file.
 
     if describe_output.endswith("-dirty"):
         dirty = True
@@ -148,6 +287,50 @@ def _git_parse_describe(describe_output):
     else:
         dirty = False
 
-    tag, number, node = describe_output.rsplit("-", 2)
-    number = int(number)
+    split = describe_output.rsplit("-", 2)
+    if len(split) < 3:  # probably a tagged commit
+        tag = describe_output
+        number = 0
+        node = None
+    else:
+        tag, number_, node = split
+        number = int(number_)
     return tag, number, node, dirty
+
+
+def archival_to_version(
+    data: dict[str, str], config: Configuration
+) -> ScmVersion | None:
+    node: str | None
+    log.debug("data %s", data)
+    archival_describe = data.get("describe-name", DESCRIBE_UNSUPPORTED)
+    if DESCRIBE_UNSUPPORTED in archival_describe:
+        warnings.warn("git archive did not support describe output")
+    else:
+        tag, number, node, _ = _git_parse_describe(archival_describe)
+        return meta(
+            tag,
+            config=config,
+            distance=number,
+            node=node,
+        )
+
+    for ref in REF_TAG_RE.findall(data.get("ref-names", "")):
+        version = tag_to_version(ref, config)
+        if version is not None:
+            return meta(version, config=config)
+    else:
+        node = data.get("node")
+        if node is None:
+            return None
+        elif "$FORMAT" in node.upper():
+            warnings.warn("unprocessed git archival found (no export subst applied)")
+            return None
+        else:
+            return meta("0.0", node=node, config=config)
+
+
+def parse_archival(root: _t.PathT, config: Configuration) -> ScmVersion | None:
+    archival = os.path.join(root, ".git_archival.txt")
+    data = data_from_mime(archival)
+    return archival_to_version(data, config=config)
diff --git a/src/setuptools_scm/hacks.py b/src/setuptools_scm/hacks.py
deleted file mode 100644 (file)
index 349d26f..0000000
+++ /dev/null
@@ -1,37 +0,0 @@
-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)
index d699d45ccae8b3e8b8149934e5c0a52f793ec882..522dfb669175ac82bc744e873e2e09ea922077a9 100644 (file)
+from __future__ import annotations
+
+import datetime
+import logging
 import os
-from .config import Configuration
-from .utils import do, trace, data_from_mime, has_command
-from .version import meta, tags_to_versions
-
-
-def _hg_tagdist_normalize_tagcommit(config, tag, dist, node, branch):
-    dirty = node.endswith("+")
-    node = "h" + node.strip("+")
-
-    # Detect changes since the specified tag
-    revset = (
-        "(branch(.)"  # look for revisions in this branch only
-        " and tag({tag!r})::."  # after the last tag
-        # ignore commits that only modify .hgtags and nothing else:
-        " and (merge() or file('re:^(?!\\.hgtags).*$'))"
-        " and not tag({tag!r}))"  # ignore the tagged commit itself
-    ).format(tag=tag)
-    if tag != "0.0":
-        commits = do(
-            ["hg", "log", "-r", revset, "--template", "{node|short}"],
-            config.absolute_root,
-        )
-    else:
-        commits = True
-    trace("normalize", locals())
-    if commits or dirty:
-        return meta(
-            tag, distance=dist, node=node, dirty=dirty, branch=branch, config=config
+from pathlib import Path
+from typing import TYPE_CHECKING
+
+from . import Configuration
+from ._version_cls import Version
+from .integration import data_from_mime
+from .scm_workdir import Workdir
+from .version import meta
+from .version import ScmVersion
+from .version import tag_to_version
+
+if TYPE_CHECKING:
+    from . import _types as _t
+
+from ._run_cmd import run as _run, require_command as _require_command
+
+log = logging.getLogger(__name__)
+
+
+class HgWorkdir(Workdir):
+    @classmethod
+    def from_potential_worktree(cls, wd: _t.PathT) -> HgWorkdir | None:
+        res = _run(["hg", "root"], wd)
+        if res.returncode:
+            return None
+        return cls(Path(res.stdout))
+
+    def get_meta(self, config: Configuration) -> ScmVersion | None:
+        node: str
+        tags_str: str
+        bookmark: str
+        node_date_str: str
+        node, tags_str, bookmark, node_date_str = self.hg_log(
+            ".", "{node}\n{tag}\n{bookmark}\n{date|shortdate}"
+        ).split("\n")
+
+        # TODO: support bookmarks and topics (but nowadays bookmarks are
+        # mainly used to emulate Git branches, which is already supported with
+        # the dedicated class GitWorkdirHgClient)
+
+        branch, dirty_str, dirty_date = _run(
+            ["hg", "id", "-T", "{branch}\n{if(dirty, 1, 0)}\n{date|shortdate}"],
+            cwd=self.path,
+            check=True,
+        ).stdout.split("\n")
+        dirty = bool(int(dirty_str))
+        node_date = datetime.date.fromisoformat(dirty_date if dirty else node_date_str)
+
+        if node == "0" * len(node):
+            log.debug("initial node %s", self.path)
+            return meta(
+                Version("0.0"),
+                config=config,
+                dirty=dirty,
+                branch=branch,
+                node_date=node_date,
+            )
+
+        node = "h" + node[:7]
+
+        tags = tags_str.split()
+        if "tip" in tags:
+            # tip is not a real tag
+            tags.remove("tip")
+
+        if tags:
+            tag = tag_to_version(tags[0], config)
+            if tag:
+                return meta(tag, dirty=dirty, branch=branch, config=config)
+
+        try:
+            tag_str = self.get_latest_normalizable_tag()
+            if tag_str is None:
+                dist = self.get_distance_revs("")
+            else:
+                dist = self.get_distance_revs(tag_str)
+
+            if tag_str == "null" or tag_str is None:
+                tag = Version("0.0")
+                dist += 1
+            else:
+                tag = tag_to_version(tag_str, config=config)
+                assert tag is not None
+
+            if self.check_changes_since_tag(tag_str) or dirty:
+                return meta(
+                    tag,
+                    distance=dist,
+                    node=node,
+                    dirty=dirty,
+                    branch=branch,
+                    config=config,
+                    node_date=node_date,
+                )
+            else:
+                return meta(tag, config=config, node_date=node_date)
+
+        except ValueError as e:
+            log.exception("error %s", e)
+            pass  # unpacking failed, old hg
+
+        return None
+
+    def hg_log(self, revset: str, template: str) -> str:
+        cmd = ["hg", "log", "-r", revset, "-T", template]
+
+        return _run(cmd, cwd=self.path, check=True).stdout
+
+    def get_latest_normalizable_tag(self) -> str | None:
+        # Gets all tags containing a '.' (see #229) from oldest to newest
+        outlines = self.hg_log(
+            revset="ancestors(.) and tag('re:\\.')",
+            template="{tags}{if(tags, '\n', '')}",
+        ).split()
+        if not outlines:
+            return None
+        tag = outlines[-1].split()[-1]
+        return tag
+
+    def get_distance_revs(self, rev1: str, rev2: str = ".") -> int:
+        revset = f"({rev1}::{rev2})"
+        out = self.hg_log(revset, ".")
+        return len(out) - 1
+
+    def check_changes_since_tag(self, tag: str | None) -> bool:
+        if tag == "0.0" or tag is None:
+            return True
+
+        revset = (
+            "(branch(.)"  # look for revisions in this branch only
+            f" and tag({tag!r})::."  # after the last tag
+            # ignore commits that only modify .hgtags and nothing else:
+            " and (merge() or file('re:^(?!\\.hgtags).*$'))"
+            f" and not tag({tag!r}))"  # ignore the tagged commit itself
         )
-    else:
-        return meta(tag, config=config)
-
-
-def parse(root, config=None):
-    if not config:
-        config = Configuration(root=root)
-
-    if not has_command("hg"):
-        return
-    identity_data = do("hg id -i -b -t", config.absolute_root).split()
-    if not identity_data:
-        return
-    node = identity_data.pop(0)
-    branch = identity_data.pop(0)
-    if "tip" in identity_data:
-        # tip is not a real tag
-        identity_data.remove("tip")
-    tags = tags_to_versions(identity_data)
-    dirty = node[-1] == "+"
-    if tags:
-        return meta(tags[0], dirty=dirty, branch=branch, config=config)
-
-    if node.strip("+") == "0" * 12:
-        trace("initial node", config.absolute_root)
-        return meta("0.0", config=config, dirty=dirty, branch=branch)
-
-    try:
-        tag = get_latest_normalizable_tag(config.absolute_root)
-        dist = get_graph_distance(config.absolute_root, tag)
-        if tag == "null":
-            tag = "0.0"
-            dist = int(dist) + 1
-        return _hg_tagdist_normalize_tagcommit(config, tag, dist, node, branch)
-    except ValueError:
-        pass  # unpacking failed, old hg
-
-
-def get_latest_normalizable_tag(root):
-    # Gets all tags containing a '.' (see #229) from oldest to newest
-    cmd = [
-        "hg",
-        "log",
-        "-r",
-        "ancestors(.) and tag('re:\\.')",
-        "--template",
-        "{tags}\n",
-    ]
-    outlines = do(cmd, root).split()
-    if not outlines:
-        return "null"
-    tag = outlines[-1].split()[-1]
-    return tag
-
-
-def get_graph_distance(root, rev1, rev2="."):
-    cmd = ["hg", "log", "-q", "-r", "{}::{}".format(rev1, rev2)]
-    out = do(cmd, root)
-    return len(out.strip().splitlines()) - 1
-
-
-def archival_to_version(data, config=None):
-    trace("data", data)
+
+        return bool(self.hg_log(revset, "."))
+
+
+def parse(root: _t.PathT, config: Configuration) -> ScmVersion | None:
+    _require_command("hg")
+    if os.path.exists(os.path.join(root, ".hg/git")):
+        res = _run(["hg", "path"], root)
+        if not res.returncode:
+            for line in res.stdout.split("\n"):
+                if line.startswith("default ="):
+                    path = Path(line.split()[2])
+                    if path.name.endswith(".git") or (path / ".git").exists():
+                        from .git import _git_parse_inner
+                        from .hg_git import GitWorkdirHgClient
+
+                        wd_hggit = GitWorkdirHgClient.from_potential_worktree(root)
+                        if wd_hggit:
+                            return _git_parse_inner(config, wd_hggit)
+
+    wd = HgWorkdir.from_potential_worktree(config.absolute_root)
+
+    if wd is None:
+        return None
+
+    return wd.get_meta(config)
+
+
+def archival_to_version(data: dict[str, str], config: Configuration) -> ScmVersion:
+    log.debug("data %s", data)
     node = data.get("node", "")[:12]
     if node:
         node = "h" + node
@@ -99,15 +176,15 @@ def archival_to_version(data, config=None):
     elif "latesttag" in data:
         return meta(
             data["latesttag"],
-            distance=data["latesttagdistance"],
+            distance=int(data["latesttagdistance"]),
             node=node,
             config=config,
         )
     else:
-        return meta("0.0", node=node, config=config)
+        return meta(config.version_cls("0.0"), node=node, config=config)
 
 
-def parse_archival(root, config=None):
+def parse_archival(root: _t.PathT, config: Configuration) -> ScmVersion:
     archival = os.path.join(root, ".hg_archival.txt")
     data = data_from_mime(archival)
     return archival_to_version(data, config=config)
diff --git a/src/setuptools_scm/hg_git.py b/src/setuptools_scm/hg_git.py
new file mode 100644 (file)
index 0000000..b6c3036
--- /dev/null
@@ -0,0 +1,155 @@
+from __future__ import annotations
+
+import logging
+import os
+from contextlib import suppress
+from datetime import date
+from pathlib import Path
+
+from . import _types as _t
+from ._run_cmd import CompletedProcess as _CompletedProcess
+from ._run_cmd import require_command
+from ._run_cmd import run as _run
+from .git import GitWorkdir
+from .hg import HgWorkdir
+
+log = logging.getLogger(__name__)
+
+_FAKE_GIT_DESCRIBE_ERROR = _CompletedProcess(
+    "fake git describe output for hg",
+    1,
+    "<>hg git failed to describe",
+)
+
+
+class GitWorkdirHgClient(GitWorkdir, HgWorkdir):
+    COMMAND = "hg"
+
+    @classmethod
+    def from_potential_worktree(cls, wd: _t.PathT) -> GitWorkdirHgClient | None:
+        require_command("hg")
+        res = _run(["hg", "root"], cwd=wd).parse_success(parse=Path)
+        if res is None:
+            return None
+        return cls(res)
+
+    def is_dirty(self) -> bool:
+        res = _run(["hg", "id", "-T", "{dirty}"], cwd=self.path, check=True)
+        return bool(res.stdout)
+
+    def get_branch(self) -> str | None:
+        res = _run(["hg", "id", "-T", "{bookmarks}"], cwd=self.path)
+        if res.returncode:
+            log.info("branch err %s", res)
+            return None
+        return res.stdout
+
+    def get_head_date(self) -> date | None:
+        return _run('hg log -r . -T "{shortdate(date)}"', cwd=self.path).parse_success(
+            parse=date.fromisoformat, error_msg="head date err"
+        )
+
+    def is_shallow(self) -> bool:
+        return False
+
+    def fetch_shallow(self) -> None:
+        pass
+
+    def get_hg_node(self) -> str | None:
+        res = _run('hg log -r . -T "{node}"', cwd=self.path)
+        if res.returncode:
+            return None
+        else:
+            return res.stdout
+
+    def _hg2git(self, hg_node: str) -> str | None:
+        with suppress(FileNotFoundError):
+            with open(os.path.join(self.path, ".hg/git-mapfile")) as map_items:
+                for item in map_items:
+                    if hg_node in item:
+                        git_node, hg_node = item.split()
+                        return git_node
+        return None
+
+    def node(self) -> str | None:
+        hg_node = self.get_hg_node()
+        if hg_node is None:
+            return None
+
+        git_node = self._hg2git(hg_node)
+
+        if git_node is None:
+            # trying again after hg -> git
+            _run(["hg", "gexport"], cwd=self.path)
+            git_node = self._hg2git(hg_node)
+
+            if git_node is None:
+                log.debug("Cannot get git node so we use hg node %s", hg_node)
+
+                if hg_node == "0" * len(hg_node):
+                    # mimic Git behavior
+                    return None
+
+                return hg_node
+
+        return git_node[:7]
+
+    def count_all_nodes(self) -> int:
+        res = _run(["hg", "log", "-r", "ancestors(.)", "-T", "."], cwd=self.path)
+        return len(res.stdout)
+
+    def default_describe(self) -> _CompletedProcess:
+        """
+        Tentative to reproduce the output of
+
+        `git describe --dirty --tags --long --match *[0-9]*`
+
+        """
+        res = _run(
+            [
+                "hg",
+                "log",
+                "-r",
+                "(reverse(ancestors(.)) and tag(r're:v?[0-9].*'))",
+                "-T",
+                "{tags}{if(tags, ' ', '')}",
+            ],
+            cwd=self.path,
+        )
+        if res.returncode:
+            return _FAKE_GIT_DESCRIBE_ERROR
+        hg_tags: list[str] = res.stdout.split()
+
+        if not hg_tags:
+            return _FAKE_GIT_DESCRIBE_ERROR
+
+        with self.path.joinpath(".hg/git-tags").open() as fp:
+            git_tags: dict[str, str] = dict(line.split()[::-1] for line in fp)
+
+        tag: str
+        for hg_tag in hg_tags:
+            if hg_tag in git_tags:
+                tag = hg_tag
+                break
+        else:
+            logging.warning("tag not found hg=%s git=%s", hg_tags, git_tags)
+            return _FAKE_GIT_DESCRIBE_ERROR
+
+        res = _run(["hg", "log", "-r", f"'{tag}'::.", "-T", "."], cwd=self.path)
+        if res.returncode:
+            return _FAKE_GIT_DESCRIBE_ERROR
+        distance = len(res.stdout) - 1
+
+        node = self.node()
+        assert node is not None
+        desc = f"{tag}-{distance}-g{node}"
+
+        if self.is_dirty():
+            desc += "-dirty"
+        log.debug("faked describe %r", desc)
+        return _CompletedProcess(
+            ["setuptools-scm", "faked", "describe"],
+            returncode=0,
+            stdout=desc,
+            stderr="",
+        )
index c623db7b996379ac5ece0a50e78d018e18db998b..390b0a7062e39b54b616818a1b2ad65fa4e0a12a 100644 (file)
@@ -1,48 +1,30 @@
-from pkg_resources import iter_entry_points
-
-from .version import _warn_if_setuptools_outdated
-from .utils import do, trace_exception
-from . import _get_version, Configuration
-
-
-def version_keyword(dist, keyword, value):
-    _warn_if_setuptools_outdated()
-    if not value:
-        return
-    if value is True:
-        value = {}
-    if getattr(value, "__call__", None):
-        value = value()
-    config = Configuration(**value)
-    dist.metadata.version = _get_version(config)
-
-
-def find_files(path=""):
-    for ep in iter_entry_points("setuptools_scm.files_command"):
-        command = ep.load()
-        if isinstance(command, str):
-            # this technique is deprecated
-            res = do(ep.load(), path or ".").splitlines()
-        else:
-            res = command(path)
-        if res:
-            return res
-    return []
-
-
-def _args_from_toml(name="pyproject.toml"):
-    # todo: more sensible config initialization
-    # move this elper back to config and unify it with the code from get_config
-
-    with open(name) as strm:
-        defn = __import__("toml").load(strm)
-    return defn.get("tool", {})["setuptools_scm"]
-
-
-def infer_version(dist):
-
-    try:
-        config = Configuration.from_file()
-    except Exception:
-        return trace_exception()
-    dist.metadata.version = _get_version(config)
+from __future__ import annotations
+
+import logging
+import textwrap
+from pathlib import Path
+
+from . import _types as _t
+
+log = logging.getLogger(__name__)
+
+
+def data_from_mime(path: _t.PathT, content: None | str = None) -> dict[str, str]:
+    """return a mapping from mime/pseudo-mime content
+    :param path: path to the mime file
+    :param content: content of the mime file, if None, read from path
+    :rtype: dict[str, str]
+
+    """
+
+    if content is None:
+        content = Path(path).read_text(encoding="utf-8")
+    log.debug("mime %s content:\n%s", path, textwrap.indent(content, "    "))
+
+    from email.parser import HeaderParser
+
+    parser = HeaderParser()
+    message = parser.parsestr(content)
+    data = dict(message.items())
+    log.debug("mime %s data:\n%s", path, data)
+    return data
diff --git a/src/setuptools_scm/scm_workdir.py b/src/setuptools_scm/scm_workdir.py
new file mode 100644 (file)
index 0000000..9879549
--- /dev/null
@@ -0,0 +1,15 @@
+from __future__ import annotations
+
+from dataclasses import dataclass
+from pathlib import Path
+
+from ._config import Configuration
+from .version import ScmVersion
+
+
+@dataclass()
+class Workdir:
+    path: Path
+
+    def run_describe(self, config: Configuration) -> ScmVersion:
+        raise NotImplementedError(self.run_describe)
diff --git a/src/setuptools_scm/utils.py b/src/setuptools_scm/utils.py
deleted file mode 100644 (file)
index c31007a..0000000
+++ /dev/null
@@ -1,163 +0,0 @@
-"""
-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
index a3c8b94812f3a619d83470e543dddbc55157f02a..f43e14b675a94345ea6db196c86eff3c7e39e8e0 100644 (file)
-from __future__ import print_function
-import datetime
-import warnings
-import re
-
-from .config import Configuration
-from .utils import trace, string_types, utc
+from __future__ import annotations
 
-from pkg_resources import iter_entry_points
-
-from pkg_resources import parse_version as pkg_parse_version
+import dataclasses
+import logging
+import os
+import re
+import warnings
+from datetime import date
+from datetime import datetime
+from datetime import timezone
+from typing import Any
+from typing import Callable
+from typing import Match
+from typing import TYPE_CHECKING
 
-SEMVER_MINOR = 2
-SEMVER_PATCH = 3
-SEMVER_LEN = 3
+from . import _entrypoints
+from . import _modify_version
 
+if TYPE_CHECKING:
+    from typing_extensions import Concatenate
+    from typing_extensions import ParamSpec
 
-def _parse_version_tag(tag, config):
-    tagstring = tag if not isinstance(tag, string_types) else str(tag)
-    match = config.tag_regex.match(tagstring)
+    _P = ParamSpec("_P")
 
-    result = None
-    if match:
-        if len(match.groups()) == 1:
-            key = 1
-        else:
-            key = "version"
+from typing import TypedDict
 
-        result = {
-            "version": match.group(key),
-            "prefix": match.group(0)[: match.start(key)],
-            "suffix": match.group(0)[match.end(key) :],
-        }
 
-    trace("tag '{}' parsed to {}".format(tag, result))
-    return result
+from ._version_cls import Version as PkgVersion, _VersionT
+from . import _version_cls as _v
+from . import _config
 
+log = logging.getLogger(__name__)
 
-def _get_version_class():
-    modern_version = pkg_parse_version("1.0")
-    if isinstance(modern_version, tuple):
-        return None
-    else:
-        return type(modern_version)
 
+SEMVER_MINOR = 2
+SEMVER_PATCH = 3
+SEMVER_LEN = 3
 
-VERSION_CLASS = _get_version_class()
 
+class _TagDict(TypedDict):
+    version: str
+    prefix: str
+    suffix: str
 
-class SetuptoolsOutdatedWarning(Warning):
-    pass
 
+def _parse_version_tag(
+    tag: str | object, config: _config.Configuration
+) -> _TagDict | None:
+    match = config.tag_regex.match(str(tag))
 
-# append so integrators can disable the warning
-warnings.simplefilter("error", SetuptoolsOutdatedWarning, append=True)
+    if match:
+        key: str | int = 1 if len(match.groups()) == 1 else "version"
+        full = match.group(0)
+        log.debug("%r %r %s", tag, config.tag_regex, match)
+        log.debug(
+            "key %s data %s, %s, %r", key, match.groupdict(), match.groups(), full
+        )
+        result = _TagDict(
+            version=match.group(key),
+            prefix=full[: match.start(key)],
+            suffix=full[match.end(key) :],
+        )
 
+        log.debug("tag %r parsed to %r", tag, result)
+        assert result["version"]
+        return result
+    else:
+        log.debug("tag %r did not parse", tag)
 
-def _warn_if_setuptools_outdated():
-    if VERSION_CLASS is None:
-        warnings.warn("your setuptools is too old (<12)", SetuptoolsOutdatedWarning)
+        return None
 
 
-def callable_or_entrypoint(group, callable_or_name):
-    trace("ep", (group, callable_or_name))
+def callable_or_entrypoint(group: str, callable_or_name: str | Any) -> Any:
+    log.debug("ep %r %r", group, callable_or_name)
 
     if callable(callable_or_name):
         return callable_or_name
+    from ._entrypoints import iter_entry_points
 
     for ep in iter_entry_points(group, callable_or_name):
-        trace("ep found:", ep.name)
+        log.debug("ep found: %s", ep.name)
         return ep.load()
 
 
-def tag_to_version(tag, config=None):
+def tag_to_version(
+    tag: _VersionT | str, config: _config.Configuration
+) -> _VersionT | None:
     """
     take a tag that might be prefixed with a keyword and return only the version part
-    :param config: optional configuration object
     """
-    trace("tag", tag)
+    log.debug("tag %s", tag)
 
-    if not config:
-        config = Configuration()
-
-    tagdict = _parse_version_tag(tag, config)
-    if not isinstance(tagdict, dict) or not tagdict.get("version", None):
-        warnings.warn("tag {!r} no version found".format(tag))
+    tag_dict = _parse_version_tag(tag, config)
+    if tag_dict is None or not tag_dict.get("version", None):
+        warnings.warn(f"tag {tag!r} no version found")
         return None
 
-    version = tagdict["version"]
-    trace("version pre parse", version)
+    version_str = tag_dict["version"]
+    log.debug("version pre parse %s", version_str)
 
-    if tagdict.get("suffix", ""):
-        warnings.warn(
-            "tag {!r} will be stripped of its suffix '{}'".format(
-                tag, tagdict["suffix"]
-            )
-        )
+    if suffix := tag_dict.get("suffix", ""):
+        warnings.warn(f"tag {tag!r} will be stripped of its suffix {suffix!r}")
 
-    if VERSION_CLASS is not None:
-        version = pkg_parse_version(version)
-        trace("version", repr(version))
+    version: _VersionT = config.version_cls(version_str)
+    log.debug("version=%r", version)
 
     return version
 
 
-def tags_to_versions(tags, config=None):
-    """
-    take tags that might be prefixed with a keyword and return only the version part
-    :param tags: an iterable of tags
-    :param config: optional configuration object
+def _source_epoch_or_utc_now() -> datetime:
+    if "SOURCE_DATE_EPOCH" in os.environ:
+        date_epoch = int(os.environ["SOURCE_DATE_EPOCH"])
+        return datetime.fromtimestamp(date_epoch, timezone.utc)
+    else:
+        return datetime.now(timezone.utc)
+
+
+@dataclasses.dataclass
+class ScmVersion:
+    """represents a parsed version from scm"""
+
+    tag: _v.Version | _v.NonNormalizedVersion | str
+    """the related tag or preformatted version string"""
+    config: _config.Configuration
+    """the configuration used to parse the version"""
+    distance: int = 0
+    """the number of commits since the tag"""
+    node: str | None = None
+    """the shortened node id"""
+    dirty: bool = False
+    """whether the working copy had uncommitted changes"""
+    preformatted: bool = False
+    """whether the version string was preformatted"""
+    branch: str | None = None
+    """the branch name if any"""
+    node_date: date | None = None
+    """the date of the commit if available"""
+    time: datetime = dataclasses.field(default_factory=_source_epoch_or_utc_now)
+    """the current time or source epoch time
+    only set for unit-testing version schemes
+    for real usage it must be `now(utc)` or `SOURCE_EPOCH`
     """
-    result = []
-    for tag in tags:
-        tag = tag_to_version(tag, config=config)
-        if tag:
-            result.append(tag)
-    return result
-
-
-class ScmVersion(object):
-    def __init__(
-        self,
-        tag_version,
-        distance=None,
-        node=None,
-        dirty=False,
-        preformatted=False,
-        branch=None,
-        config=None,
-        **kw
-    ):
-        if kw:
-            trace("unknown args", kw)
-        self.tag = tag_version
-        if dirty and distance is None:
-            distance = 0
-        self.distance = distance
-        self.node = node
-        self.time = datetime.datetime.now(utc)
-        self._extra = kw
-        self.dirty = dirty
-        self.preformatted = preformatted
-        self.branch = branch
-        self.config = config
 
     @property
-    def extra(self):
-        warnings.warn(
-            "ScmVersion.extra is deprecated and will be removed in future",
-            category=DeprecationWarning,
-            stacklevel=2,
+    def exact(self) -> bool:
+        """returns true checked out exactly on a tag and no local changes apply"""
+        return self.distance == 0 and not self.dirty
+
+    def __repr__(self) -> str:
+        return (
+            f"<ScmVersion {self.tag} dist={self.distance} "
+            f"node={self.node} dirty={self.dirty} branch={self.branch}>"
         )
-        return self._extra
 
-    @property
-    def exact(self):
-        return self.distance is None
-
-    def __repr__(self):
-        return self.format_with(
-            "<ScmVersion {tag} d={distance} n={node} d={dirty} b={branch}>"
-        )
-
-    def format_with(self, fmt, **kw):
+    def format_with(self, fmt: str, **kw: object) -> str:
+        """format a given format string with attributes of this object"""
         return fmt.format(
             time=self.time,
             tag=self.tag,
@@ -169,81 +161,86 @@ class ScmVersion(object):
             node=self.node,
             dirty=self.dirty,
             branch=self.branch,
-            **kw
+            node_date=self.node_date,
+            **kw,
         )
 
-    def format_choice(self, clean_format, dirty_format, **kw):
+    def format_choice(self, clean_format: str, dirty_format: str, **kw: object) -> str:
+        """given `clean_format` and `dirty_format`
+
+        choose one based on `self.dirty` and format it using `self.format_with`"""
+
         return self.format_with(dirty_format if self.dirty else clean_format, **kw)
 
-    def format_next_version(self, guess_next, fmt="{guessed}.dev{distance}", **kw):
-        guessed = guess_next(self.tag, **kw)
+    def format_next_version(
+        self,
+        guess_next: Callable[Concatenate[ScmVersion, _P], str],
+        fmt: str = "{guessed}.dev{distance}",
+        *k: _P.args,
+        **kw: _P.kwargs,
+    ) -> str:
+        guessed = guess_next(self, *k, **kw)
         return self.format_with(fmt, guessed=guessed)
 
 
-def _parse_tag(tag, preformatted, config):
+def _parse_tag(
+    tag: _VersionT | str, preformatted: bool, config: _config.Configuration
+) -> _VersionT | str:
     if preformatted:
         return tag
-    if VERSION_CLASS is None or not isinstance(tag, VERSION_CLASS):
-        tag = tag_to_version(tag, config)
-    return tag
+    elif not isinstance(tag, config.version_cls):
+        version = tag_to_version(tag, config)
+        assert version is not None
+        return version
+    else:
+        return tag
 
 
 def meta(
-    tag,
-    distance=None,
-    dirty=False,
-    node=None,
-    preformatted=False,
-    branch=None,
-    config=None,
-    **kw
-):
-    if not config:
-        warnings.warn(
-            "meta invoked without explicit configuration,"
-            " will use defaults where required."
-        )
+    tag: str | _VersionT,
+    *,
+    distance: int = 0,
+    dirty: bool = False,
+    node: str | None = None,
+    preformatted: bool = False,
+    branch: str | None = None,
+    config: _config.Configuration,
+    node_date: date | None = None,
+) -> ScmVersion:
     parsed_version = _parse_tag(tag, preformatted, config)
-    trace("version", tag, "->", parsed_version)
-    assert parsed_version is not None, "cant parse version %s" % tag
+    log.info("version %s -> %s", tag, parsed_version)
+    assert parsed_version is not None, "Can't parse version %s" % tag
     return ScmVersion(
-        parsed_version, distance, node, dirty, preformatted, branch, config, **kw
+        parsed_version,
+        distance=distance,
+        node=node,
+        dirty=dirty,
+        preformatted=preformatted,
+        branch=branch,
+        config=config,
+        node_date=node_date,
     )
 
 
-def guess_next_version(tag_version):
-    version = _strip_local(str(tag_version))
-    return _bump_dev(version) or _bump_regex(version)
-
-
-def _strip_local(version_string):
-    public, sep, local = version_string.partition("+")
-    return public
-
+def guess_next_version(tag_version: ScmVersion) -> str:
+    version = _modify_version.strip_local(str(tag_version.tag))
+    return _modify_version._bump_dev(version) or _modify_version._bump_regex(version)
 
-def _bump_dev(version):
-    if ".dev" not in version:
-        return
 
-    prefix, tail = version.rsplit(".dev", 1)
-    assert tail == "0", "own dev numbers are unsupported"
-    return prefix
-
-
-def _bump_regex(version):
-    prefix, tail = re.match(r"(.*?)(\d+)$", version).groups()
-    return "%s%d" % (prefix, int(tail) + 1)
-
-
-def guess_next_dev_version(version):
+def guess_next_dev_version(version: ScmVersion) -> str:
     if version.exact:
         return version.format_with("{tag}")
     else:
         return version.format_next_version(guess_next_version)
 
 
-def guess_next_simple_semver(version, retain, increment=True):
-    parts = [int(i) for i in str(version).split(".")[:retain]]
+def guess_next_simple_semver(
+    version: ScmVersion, retain: int, increment: bool = True
+) -> str:
+    try:
+        parts = [int(i) for i in str(version.tag).split(".")[:retain]]
+    except ValueError:
+        raise ValueError(f"{version} can't be parsed as numeric version") from None
     while len(parts) < retain:
         parts.append(0)
     if increment:
@@ -253,9 +250,9 @@ def guess_next_simple_semver(version, retain, increment=True):
     return ".".join(str(i) for i in parts)
 
 
-def simplified_semver_version(version):
+def simplified_semver_version(version: ScmVersion) -> str:
     if version.exact:
-        return guess_next_simple_semver(version.tag, retain=SEMVER_LEN, increment=False)
+        return guess_next_simple_semver(version, retain=SEMVER_LEN, increment=False)
     else:
         if version.branch is not None and "feature" in version.branch:
             return version.format_next_version(
@@ -267,18 +264,24 @@ def simplified_semver_version(version):
             )
 
 
-def release_branch_semver_version(version):
+def release_branch_semver_version(version: ScmVersion) -> str:
     if version.exact:
         return version.format_with("{tag}")
     if version.branch is not None:
         # Does the branch name (stripped of namespace) parse as a version?
-        branch_ver = _parse_version_tag(version.branch.split("/")[-1], version.config)
-        if branch_ver is not None:
+        branch_ver_data = _parse_version_tag(
+            version.branch.split("/")[-1], version.config
+        )
+        if branch_ver_data is not None:
+            branch_ver = branch_ver_data["version"]
+            if branch_ver[0] == "v":
+                # Allow branches that start with 'v', similar to Version.
+                branch_ver = branch_ver[1:]
             # Does the branch version up to the minor part match the tag? If not it
             # might be like, an issue number or something and not a version number, so
             # we only want to use it if it matches.
             tag_ver_up_to_minor = str(version.tag).split(".")[:SEMVER_MINOR]
-            branch_ver_up_to_minor = branch_ver["version"].split(".")[:SEMVER_MINOR]
+            branch_ver_up_to_minor = branch_ver.split(".")[:SEMVER_MINOR]
             if branch_ver_up_to_minor == tag_ver_up_to_minor:
                 # We're in a release/maintenance branch, next is a patch/rc/beta bump:
                 return version.format_next_version(guess_next_version)
@@ -286,64 +289,152 @@ def release_branch_semver_version(version):
     return version.format_next_version(guess_next_simple_semver, retain=SEMVER_MINOR)
 
 
-def release_branch_semver(version):
+def release_branch_semver(version: ScmVersion) -> str:
     warnings.warn(
-        "release_branch_semver is deprecated and will be removed in future. "
-        "Use release_branch_semver_version instead",
+        "release_branch_semver is deprecated and will be removed in the future. "
+        "Use release_branch_semver_version instead",
         category=DeprecationWarning,
         stacklevel=2,
     )
     return release_branch_semver_version(version)
 
 
-def _format_local_with_time(version, time_format):
+def no_guess_dev_version(version: ScmVersion) -> str:
+    if version.exact:
+        return version.format_with("{tag}")
+    else:
+        return version.format_next_version(_modify_version._dont_guess_next_version)
+
+
+_DATE_REGEX = re.compile(
+    r"""
+    ^(?P<date>
+        (?P<prefix>[vV]?)
+        (?P<year>\d{2}|\d{4})(?:\.\d{1,2}){2})
+        (?:\.(?P<patch>\d*))?$
+    """,
+    re.VERBOSE,
+)
+
+
+def date_ver_match(ver: str) -> Match[str] | None:
+    return _DATE_REGEX.match(ver)
+
 
-    if version.exact or version.node is None:
-        return version.format_choice(
-            "", "+d{time:{time_format}}", time_format=time_format
+def guess_next_date_ver(
+    version: ScmVersion,
+    node_date: date | None = None,
+    date_fmt: str | None = None,
+    version_cls: type | None = None,
+) -> str:
+    """
+    same-day -> patch +1
+    other-day -> today
+
+    distance is always added as .devX
+    """
+    match = date_ver_match(str(version.tag))
+    if match is None:
+        warnings.warn(
+            f"{version} does not correspond to a valid versioning date, "
+            "assuming legacy version"
         )
+        if date_fmt is None:
+            date_fmt = "%y.%m.%d"
     else:
-        return version.format_choice(
-            "+{node}", "+{node}.d{time:{time_format}}", time_format=time_format
+        # deduct date format if not provided
+        if date_fmt is None:
+            date_fmt = "%Y.%m.%d" if len(match.group("year")) == 4 else "%y.%m.%d"
+        if prefix := match.group("prefix"):
+            if not date_fmt.startswith(prefix):
+                date_fmt = prefix + date_fmt
+
+    today = version.time.date()
+    head_date = node_date or today
+    # compute patch
+    if match is None:
+        tag_date = today
+    else:
+        tag_date = (
+            datetime.strptime(match.group("date"), date_fmt)
+            .replace(tzinfo=timezone.utc)
+            .date()
         )
+    if tag_date == head_date:
+        patch = "0" if match is None else (match.group("patch") or "0")
+        patch = int(patch) + 1
+    else:
+        if tag_date > head_date and match is not None:
+            # warn on future times
+            warnings.warn(
+                f"your previous tag  ({tag_date})"
+                f" is ahead your node date ({head_date})"
+            )
+        patch = 0
+    next_version = "{node_date:{date_fmt}}.{patch}".format(
+        node_date=head_date, date_fmt=date_fmt, patch=patch
+    )
+    # rely on the Version object to ensure consistency (e.g. remove leading 0s)
+    if version_cls is None:
+        version_cls = PkgVersion
+    next_version = str(version_cls(next_version))
+    return next_version
+
+
+def calver_by_date(version: ScmVersion) -> str:
+    if version.exact and not version.dirty:
+        return version.format_with("{tag}")
+    # TODO: move the release-X check to a new scheme
+    if version.branch is not None and version.branch.startswith("release-"):
+        branch_ver = _parse_version_tag(version.branch.split("-")[-1], version.config)
+        if branch_ver is not None:
+            ver = branch_ver["version"]
+            match = date_ver_match(ver)
+            if match:
+                return ver
+    return version.format_next_version(
+        guess_next_date_ver,
+        node_date=version.node_date,
+        version_cls=version.config.version_cls,
+    )
 
 
-def get_local_node_and_date(version):
-    return _format_local_with_time(version, time_format="%Y%m%d")
+def get_local_node_and_date(version: ScmVersion) -> str:
+    return _modify_version._format_local_with_time(version, time_format="%Y%m%d")
 
 
-def get_local_node_and_timestamp(version, fmt="%Y%m%d%H%M%S"):
-    return _format_local_with_time(version, time_format=fmt)
+def get_local_node_and_timestamp(version: ScmVersion) -> str:
+    return _modify_version._format_local_with_time(version, time_format="%Y%m%d%H%M%S")
 
 
-def get_local_dirty_tag(version):
+def get_local_dirty_tag(version: ScmVersion) -> str:
     return version.format_choice("", "+dirty")
 
 
-def get_no_local_node(_):
+def get_no_local_node(version: ScmVersion) -> str:
     return ""
 
 
-def postrelease_version(version):
+def postrelease_version(version: ScmVersion) -> str:
     if version.exact:
         return version.format_with("{tag}")
     else:
         return version.format_with("{tag}.post{distance}")
 
 
-def format_version(version, **config):
-    trace("scm version", version)
-    trace("config", config)
+def format_version(version: ScmVersion) -> str:
+    log.debug("scm version %s", version)
+    log.debug("config %s", version.config)
     if version.preformatted:
+        assert isinstance(version.tag, str)
         return version.tag
-    version_scheme = callable_or_entrypoint(
-        "setuptools_scm.version_scheme", config["version_scheme"]
+    main_version = _entrypoints._call_version_scheme(
+        version, "setuptools_scm.version_scheme", version.config.version_scheme, None
     )
-    local_scheme = callable_or_entrypoint(
-        "setuptools_scm.local_scheme", config["local_scheme"]
+    log.debug("version %s", main_version)
+    assert main_version is not None
+    local_version = _entrypoints._call_version_scheme(
+        version, "setuptools_scm.local_scheme", version.config.local_scheme, "+unknown"
     )
-    main_version = version_scheme(version)
-    trace("version", main_version)
-    local_version = local_scheme(version)
-    trace("local_version", local_version)
-    return version_scheme(version) + local_scheme(version)
+    log.debug("local_version %s", local_version)
+    return main_version + local_version
diff --git a/src/setuptools_scm/win_py31_compat.py b/src/setuptools_scm/win_py31_compat.py
deleted file mode 100644 (file)
index 82a11eb..0000000
+++ /dev/null
@@ -1,214 +0,0 @@
-"""
-Backport of os.path.samefile for Python prior to 3.2
-on Windows from jaraco.windows 3.8.
-
-DON'T EDIT THIS FILE!
-
-Instead, file tickets and PR's with `jaraco.windows
-<https://github.com/jaraco/jaraco.windows>`_ and request
-a port to setuptools_scm.
-"""
-
-import os
-import nt
-import posixpath
-import ctypes.wintypes
-import sys
-import __builtin__ as builtins
-
-
-##
-# From jaraco.windows.error
-
-def format_system_message(errno):
-       """
-       Call FormatMessage with a system error number to retrieve
-       the descriptive error message.
-       """
-       # first some flags used by FormatMessageW
-       ALLOCATE_BUFFER = 0x100
-       FROM_SYSTEM = 0x1000
-
-       # Let FormatMessageW allocate the buffer (we'll free it below)
-       # Also, let it know we want a system error message.
-       flags = ALLOCATE_BUFFER | FROM_SYSTEM
-       source = None
-       message_id = errno
-       language_id = 0
-       result_buffer = ctypes.wintypes.LPWSTR()
-       buffer_size = 0
-       arguments = None
-       bytes = ctypes.windll.kernel32.FormatMessageW(
-               flags,
-               source,
-               message_id,
-               language_id,
-               ctypes.byref(result_buffer),
-               buffer_size,
-               arguments,
-       )
-       # note the following will cause an infinite loop if GetLastError
-       #  repeatedly returns an error that cannot be formatted, although
-       #  this should not happen.
-       handle_nonzero_success(bytes)
-       message = result_buffer.value
-       ctypes.windll.kernel32.LocalFree(result_buffer)
-       return message
-
-
-class WindowsError(builtins.WindowsError):
-       """
-       More info about errors at
-       http://msdn.microsoft.com/en-us/library/ms681381(VS.85).aspx
-       """
-
-       def __init__(self, value=None):
-               if value is None:
-                       value = ctypes.windll.kernel32.GetLastError()
-               strerror = format_system_message(value)
-               if sys.version_info > (3, 3):
-                       args = 0, strerror, None, value
-               else:
-                       args = value, strerror
-               super(WindowsError, self).__init__(*args)
-
-       @property
-       def message(self):
-               return self.strerror
-
-       @property
-       def code(self):
-               return self.winerror
-
-       def __str__(self):
-               return self.message
-
-       def __repr__(self):
-               return '{self.__class__.__name__}({self.winerror})'.format(**vars())
-
-
-def handle_nonzero_success(result):
-       if result == 0:
-               raise WindowsError()
-
-
-##
-# From jaraco.windows.api.filesystem
-
-FILE_FLAG_OPEN_REPARSE_POINT = 0x00200000
-FILE_FLAG_BACKUP_SEMANTICS = 0x2000000
-OPEN_EXISTING = 3
-FILE_ATTRIBUTE_NORMAL = 0x80
-FILE_READ_ATTRIBUTES = 0x80
-INVALID_HANDLE_VALUE = ctypes.wintypes.HANDLE(-1).value
-
-
-class BY_HANDLE_FILE_INFORMATION(ctypes.Structure):
-       _fields_ = [
-               ('file_attributes', ctypes.wintypes.DWORD),
-               ('creation_time', ctypes.wintypes.FILETIME),
-               ('last_access_time', ctypes.wintypes.FILETIME),
-               ('last_write_time', ctypes.wintypes.FILETIME),
-               ('volume_serial_number', ctypes.wintypes.DWORD),
-               ('file_size_high', ctypes.wintypes.DWORD),
-               ('file_size_low', ctypes.wintypes.DWORD),
-               ('number_of_links', ctypes.wintypes.DWORD),
-               ('file_index_high', ctypes.wintypes.DWORD),
-               ('file_index_low', ctypes.wintypes.DWORD),
-       ]
-
-       @property
-       def file_size(self):
-               return (self.file_size_high << 32) + self.file_size_low
-
-       @property
-       def file_index(self):
-               return (self.file_index_high << 32) + self.file_index_low
-
-
-class SECURITY_ATTRIBUTES(ctypes.Structure):
-       _fields_ = (
-               ('length', ctypes.wintypes.DWORD),
-               ('p_security_descriptor', ctypes.wintypes.LPVOID),
-               ('inherit_handle', ctypes.wintypes.BOOLEAN),
-       )
-
-
-LPSECURITY_ATTRIBUTES = ctypes.POINTER(SECURITY_ATTRIBUTES)
-
-
-CreateFile = ctypes.windll.kernel32.CreateFileW
-CreateFile.argtypes = (
-       ctypes.wintypes.LPWSTR,
-       ctypes.wintypes.DWORD,
-       ctypes.wintypes.DWORD,
-       LPSECURITY_ATTRIBUTES,
-       ctypes.wintypes.DWORD,
-       ctypes.wintypes.DWORD,
-       ctypes.wintypes.HANDLE,
-)
-CreateFile.restype = ctypes.wintypes.HANDLE
-
-GetFileInformationByHandle = ctypes.windll.kernel32.GetFileInformationByHandle
-GetFileInformationByHandle.restype = ctypes.wintypes.BOOL
-GetFileInformationByHandle.argtypes = (
-       ctypes.wintypes.HANDLE,
-       ctypes.POINTER(BY_HANDLE_FILE_INFORMATION),
-)
-
-
-##
-# From jaraco.windows.filesystem
-
-def compat_stat(path):
-       """
-       Generate stat as found on Python 3.2 and later.
-       """
-       stat = os.stat(path)
-       info = get_file_info(path)
-       # rewrite st_ino, st_dev, and st_nlink based on file info
-       return nt.stat_result(
-               (stat.st_mode,) +
-               (info.file_index, info.volume_serial_number, info.number_of_links) +
-               stat[4:]
-       )
-
-
-def samefile(f1, f2):
-       """
-       Backport of samefile from Python 3.2 with support for Windows.
-       """
-       return posixpath.samestat(compat_stat(f1), compat_stat(f2))
-
-
-def get_file_info(path):
-       # open the file the same way CPython does in posixmodule.c
-       desired_access = FILE_READ_ATTRIBUTES
-       share_mode = 0
-       security_attributes = None
-       creation_disposition = OPEN_EXISTING
-       flags_and_attributes = (
-               FILE_ATTRIBUTE_NORMAL |
-               FILE_FLAG_BACKUP_SEMANTICS |
-               FILE_FLAG_OPEN_REPARSE_POINT
-       )
-       template_file = None
-
-       handle = CreateFile(
-               path,
-               desired_access,
-               share_mode,
-               security_attributes,
-               creation_disposition,
-               flags_and_attributes,
-               template_file,
-       )
-
-       if handle == INVALID_HANDLE_VALUE:
-               raise WindowsError()
-
-       info = BY_HANDLE_FILE_INFORMATION()
-       res = GetFileInformationByHandle(handle, info)
-       handle_nonzero_success(res)
-
-       return info
diff --git a/testing/Dockerfile.busted-buster b/testing/Dockerfile.busted-buster
new file mode 100644 (file)
index 0000000..872833f
--- /dev/null
@@ -0,0 +1,3 @@
+FROM debian:buster
+RUN apt-get update -q && apt-get install -yq python3-pip python3-setuptools
+RUN printf "[easy_install]\nallow_hosts=localhost\nfind_links=/dist\n" > /root/.pydistutils.cfg
diff --git a/testing/Dockerfile.rawhide-git b/testing/Dockerfile.rawhide-git
new file mode 100644 (file)
index 0000000..d9f4ddc
--- /dev/null
@@ -0,0 +1,7 @@
+FROM registry.fedoraproject.org/fedora:rawhide
+RUN dnf install git -y
+RUN git --version
+USER  1000:1000
+VOLUME /repo
+WORKDIR /repo
+ENTRYPOINT mkdir git-archived && git archive HEAD -o git-archived/archival.tar.gz
diff --git a/testing/__init__.py b/testing/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/testing/check_self_install.py b/testing/check_self_install.py
deleted file mode 100644 (file)
index de3ac79..0000000
+++ /dev/null
@@ -1,5 +0,0 @@
-import pkg_resources
-import setuptools_scm
-
-dist = pkg_resources.get_distribution("setuptools_scm")
-assert dist.version == setuptools_scm.get_version(), dist.version
index 5cb65f632daa62623e8903e5aa78490d2c96d5c9..05ab5344e248aa6d04046613a765cfa2c7be351e 100644 (file)
+from __future__ import annotations
+
+import contextlib
 import os
-import itertools
+from pathlib import Path
+from types import TracebackType
+from typing import Any
+from typing import Iterator
+
 import pytest
-import six
+from typing_extensions import Self
+
+from .wd_wrapper import WorkDir
+from setuptools_scm._run_cmd import run
+
+
+def pytest_configure() -> None:
+    # 2009-02-13T23:31:30+00:00
+    os.environ["SOURCE_DATE_EPOCH"] = "1234567890"
+    os.environ["SETUPTOOLS_SCM_DEBUG"] = "1"
+
 
-os.environ["SETUPTOOLS_SCM_DEBUG"] = "1"
-VERSION_PKGS = ["setuptools", "setuptools_scm"]
+VERSION_PKGS = ["setuptools", "setuptools_scm", "packaging", "build", "wheel"]
 
 
-def pytest_report_header():
-    import pkg_resources
+def pytest_report_header() -> list[str]:
+    from importlib.metadata import version
 
     res = []
     for pkg in VERSION_PKGS:
-        version = pkg_resources.get_distribution(pkg).version
+        pkg_version = version(pkg)
         path = __import__(pkg).__file__
-        res.append("{} version {} from {!r}".format(pkg, version, path))
+        res.append(f"{pkg} version {pkg_version} from {path!r}")
     return res
 
 
-class Wd(object):
-    commit_command = None
-    add_command = None
+def pytest_addoption(parser: Any) -> None:
+    group = parser.getgroup("setuptools_scm")
+    group.addoption(
+        "--test-legacy", dest="scm_test_virtualenv", default=False, action="store_true"
+    )
 
-    def __repr__(self):
-        return "<WD {cwd}>".format(cwd=self.cwd)
 
-    def __init__(self, cwd):
-        self.cwd = cwd
-        self.__counter = itertools.count()
+class DebugMode(contextlib.AbstractContextManager):  # type: ignore[type-arg]
+    from setuptools_scm import _log as __module
 
-    def __call__(self, cmd, **kw):
-        if kw:
-            cmd = cmd.format(**kw)
-        from setuptools_scm.utils import do
+    def __init__(self) -> None:
+        self.__stack = contextlib.ExitStack()
 
-        return do(cmd, self.cwd)
+    def __enter__(self) -> Self:
+        self.enable()
+        return self
 
-    def write(self, name, value, **kw):
-        filename = self.cwd / name
-        if kw:
-            value = value.format(**kw)
-        if isinstance(value, six.text_type):
-            filename.write_text(value)
-        else:
-            filename.write_bytes(value)
-        return filename
+    def __exit__(
+        self,
+        exc_type: type[BaseException] | None,
+        exc_val: BaseException | None,
+        exc_tb: TracebackType | None,
+    ) -> None:
+        self.disable()
 
-    def _reason(self, given_reason):
-        if given_reason is None:
-            return "number-{c}".format(c=next(self.__counter))
-        else:
-            return given_reason
+    def enable(self) -> None:
+        self.__stack.enter_context(self.__module.defer_to_pytest())
 
-    def add_and_commit(self, reason=None):
-        self(self.add_command)
-        self.commit(reason)
+    def disable(self) -> None:
+        self.__stack.close()
 
-    def commit(self, reason=None):
-        reason = self._reason(reason)
-        self(self.commit_command, reason=reason)
 
-    def commit_testfile(self, reason=None):
-        reason = self._reason(reason)
-        self.write("test.txt", "test {reason}", reason=reason)
-        self(self.add_command)
-        self.commit(reason=reason)
+@pytest.fixture(autouse=True)
+def debug_mode() -> Iterator[DebugMode]:
+    with DebugMode() as debug_mode:
+        yield debug_mode
 
-    def get_version(self, **kw):
-        __tracebackhide__ = True
-        from setuptools_scm import get_version
 
-        version = get_version(root=str(self.cwd), fallback_root=str(self.cwd), **kw)
-        print(version)
-        return version
+@pytest.fixture()
+def wd(tmp_path: Path) -> WorkDir:
+    target_wd = tmp_path.resolve() / "wd"
+    target_wd.mkdir()
+    return WorkDir(target_wd)
 
-    @property
-    def version(self):
-        __tracebackhide__ = True
-        return self.get_version()
 
+@pytest.fixture()
+def repositories_hg_git(tmp_path: Path) -> tuple[WorkDir, WorkDir]:
+    tmp_path = tmp_path.resolve()
+    path_git = tmp_path / "repo_git"
+    path_git.mkdir()
 
-@pytest.yield_fixture(autouse=True)
-def debug_mode():
-    from setuptools_scm import utils
+    wd = WorkDir(path_git)
+    wd("git init")
+    wd("git config user.email test@example.com")
+    wd('git config user.name "a test"')
+    wd.add_command = "git add ."
+    wd.commit_command = "git commit -m test-{reason}"
 
-    utils.DEBUG = True
-    yield
-    utils.DEBUG = False
+    path_hg = tmp_path / "repo_hg"
+    run(["hg", "clone", path_git, path_hg, "--config", "extensions.hggit="], tmp_path)
+    assert path_hg.exists()
 
+    with open(path_hg / ".hg/hgrc", "a") as file:
+        file.write("[extensions]\nhggit =\n")
 
-@pytest.fixture
-def wd(tmp_path):
-    target_wd = tmp_path.resolve() / "wd"
-    target_wd.mkdir()
-    return Wd(target_wd)
+    wd_hg = WorkDir(path_hg)
+    wd_hg.add_command = "hg add ."
+    wd_hg.commit_command = 'hg commit -m test-{reason} -u test -d "0 0"'
+
+    return wd_hg, wd
index c5104f5c821e6138d6e23e42eaecca3d71b595f0..26f87b132889e9cd9e7307e70e1e2815fb94a176 100644 (file)
+from __future__ import annotations
+
 import os
 import sys
-import py
+from datetime import date
+from pathlib import Path
+
 import pytest
 
 import setuptools_scm
+from setuptools_scm import Configuration
 from setuptools_scm import dump_version
-from setuptools_scm.utils import data_from_mime, do
+from setuptools_scm._run_cmd import run
+from setuptools_scm.integration import data_from_mime
+from setuptools_scm.version import meta
+from setuptools_scm.version import ScmVersion
+from testing.wd_wrapper import WorkDir
+
+
+c = Configuration()
+
+template = """\
+__version__ = version = {version!r}
+__version_tuple__ = version_tuple = {version_tuple!r}
+__sha__ = {scm_version.node!r}
+"""
 
 
-@pytest.mark.parametrize("cmd", ["ls", "dir"])
-def test_do(cmd, tmpdir):
-    if not py.path.local.sysfind(cmd):
-        pytest.skip(cmd + " not found")
-    do(cmd, str(tmpdir))
+def test_run_plain(tmp_path: Path) -> None:
+    run([sys.executable, "-c", "print(1)"], cwd=tmp_path)
 
 
-def test_data_from_mime(tmpdir):
-    tmpfile = tmpdir.join("test.archival")
-    tmpfile.write("name: test\nrevision: 1")
+def test_data_from_mime(tmp_path: Path) -> None:
+    tmpfile = tmp_path.joinpath("test.archival")
+    tmpfile.write_text("name: test\nrevision: 1")
 
     res = data_from_mime(str(tmpfile))
     assert res == {"name": "test", "revision": "1"}
 
 
-def test_version_from_pkginfo(wd, monkeypatch):
+def test_version_from_pkginfo(wd: WorkDir) -> None:
     wd.write("PKG-INFO", "Version: 0.1")
 
-    assert wd.version == "0.1"
+    assert wd.get_version() == "0.1"
 
     # replicate issue 167
     assert wd.get_version(version_scheme="1.{0.distance}.0".format) == "0.1"
 
 
-def assert_root(monkeypatch, expected_root):
+def assert_root(monkeypatch: pytest.MonkeyPatch, expected_root: str) -> None:
     """
     Patch version_from_scm to simply assert that root is expected root
     """
 
-    def assertion(config):
+    def assertion(config: Configuration) -> ScmVersion:
         assert config.absolute_root == expected_root
+        return ScmVersion("1.0", config=config)
 
-    monkeypatch.setattr(setuptools_scm, "_do_parse", assertion)
+    monkeypatch.setattr(setuptools_scm._get_version_impl, "parse_version", assertion)
 
 
-def test_root_parameter_creation(monkeypatch):
+def test_root_parameter_creation(monkeypatch: pytest.MonkeyPatch) -> None:
     assert_root(monkeypatch, os.getcwd())
     setuptools_scm.get_version()
 
 
-def test_version_from_scm(wd):
-    with pytest.warns(DeprecationWarning, match=".*version_from_scm.*"):
-        setuptools_scm.version_from_scm(str(wd))
+def test_root_parameter_pass_by(
+    monkeypatch: pytest.MonkeyPatch, tmp_path: Path
+) -> None:
+    assert_root(monkeypatch, os.fspath(tmp_path))
+    setuptools_scm.get_version(root=os.fspath(tmp_path))
+    setuptools_scm.get_version(
+        os.fspath(tmp_path)
+    )  # issue 669 - posarg difference between Configuration and get_version
 
 
-def test_root_parameter_pass_by(monkeypatch, tmpdir):
-    assert_root(monkeypatch, tmpdir)
-    setuptools_scm.get_version(root=tmpdir.strpath)
+def test_parentdir_prefix(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
+    monkeypatch.delenv("SETUPTOOLS_SCM_DEBUG")
+    p = tmp_path.joinpath("projectname-v12.34")
+    p.mkdir()
+    p.joinpath("setup.py").write_text(
+        """from setuptools import setup
+setup(use_scm_version={"parentdir_prefix_version": "projectname-"})
+"""
+    )
+    res = run([sys.executable, "setup.py", "--version"], p)
+    assert res.stdout == "12.34"
 
 
-def test_parentdir_prefix(tmpdir, monkeypatch):
+def test_fallback(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
     monkeypatch.delenv("SETUPTOOLS_SCM_DEBUG")
-    p = tmpdir.ensure("projectname-v12.34", dir=True)
-    p.join("setup.py").write(
+    p = tmp_path / "sub/package"
+    p.mkdir(parents=True)
+    p.joinpath("setup.py").write_text(
         """from setuptools import setup
-setup(use_scm_version={"parentdir_prefix_version": "projectname-"})
+setup(use_scm_version={"fallback_version": "12.34"})
 """
     )
-    res = do((sys.executable, "setup.py", "--version"), p)
-    assert res == "12.34"
+    res = run([sys.executable, "setup.py", "--version"], p)
+    assert res.stdout == "12.34"
 
 
-def test_fallback(tmpdir, monkeypatch):
+def test_empty_pretend_version(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
     monkeypatch.delenv("SETUPTOOLS_SCM_DEBUG")
-    p = tmpdir.ensure("sub/package", dir=1)
-    p.join("setup.py").write(
+    monkeypatch.setenv("SETUPTOOLS_SCM_PRETEND_VERSION", "")
+    p = tmp_path / "sub/package"
+    p.mkdir(parents=True)
+    p.joinpath("setup.py").write_text(
         """from setuptools import setup
 setup(use_scm_version={"fallback_version": "12.34"})
 """
     )
-    res = do((sys.executable, "setup.py", "--version"), p)
-    assert res == "12.34"
+    res = run([sys.executable, "setup.py", "--version"], p)
+    assert res.stdout == "12.34"
+
+
+def test_empty_pretend_version_named(
+    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
+) -> None:
+    monkeypatch.delenv("SETUPTOOLS_SCM_DEBUG")
+    monkeypatch.setenv("SETUPTOOLS_SCM_PRETEND_VERSION", "1.23")
+    monkeypatch.setenv("SETUPTOOLS_SCM_PRETEND_VERSION_FOR_MYSCM", "")
+    p = tmp_path.joinpath("sub/package")
+    p.mkdir(parents=True)
+
+    p.joinpath("setup.py").write_text(
+        """from setuptools import setup
+setup(name="myscm", use_scm_version={"fallback_version": "12.34"})
+"""
+    )
+    res = run([sys.executable, "setup.py", "--version"], p)
+    assert res.stdout == "12.34"
+
+
+def test_get_version_blank_tag_regex() -> None:
+    with pytest.warns(
+        DeprecationWarning, match="empty regex for tag regex is invalid, using default"
+    ):
+        setuptools_scm.get_version(tag_regex="")
 
 
 @pytest.mark.parametrize(
     "version", ["1.0", "1.2.3.dev1+ge871260", "1.2.3.dev15+ge871260.d20180625", "2345"]
 )
-def test_pretended(version, monkeypatch):
-    monkeypatch.setenv(setuptools_scm.PRETEND_KEY, version)
+def test_pretended(version: str, monkeypatch: pytest.MonkeyPatch) -> None:
+    monkeypatch.setenv(setuptools_scm._overrides.PRETEND_KEY, version)
     assert setuptools_scm.get_version() == version
 
 
-def test_root_relative_to(monkeypatch, tmpdir):
-    assert_root(monkeypatch, tmpdir.join("alt").strpath)
-    __file__ = tmpdir.join("module/file.py").strpath
-    setuptools_scm.get_version(root="../alt", relative_to=__file__)
+def test_root_relative_to(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
+    tmp_path.joinpath("setup.cfg").touch()
+    assert_root(monkeypatch, str(tmp_path / "alt"))
+    module = tmp_path / "module/file.py"
+    module.parent.mkdir()
+    module.touch()
 
+    setuptools_scm.get_version(
+        root="../alt",
+        relative_to=str(module),
+    )
+    with pytest.warns(UserWarning, match="relative_to is expected to be a file.*"):
+        setuptools_scm.get_version(
+            root="../alt",
+            relative_to=str(module.parent),
+        )
+
+
+def test_dump_version(tmp_path: Path) -> None:
+    version = "1.0"
+    scm_version = meta(version, config=c)
+    dump_version(tmp_path, version, "first.txt", scm_version=scm_version)
+
+    def read(name: str) -> str:
+        return tmp_path.joinpath(name).read_text()
+
+    assert read("first.txt") == "1.0"
+
+    version = "1.0.dev42"
+    scm_version = meta("1.0", distance=42, config=c)
+    dump_version(tmp_path, version, "first.py", scm_version=scm_version)
+    lines = read("first.py").splitlines()
+    assert lines[-2:] == [
+        "__version__ = version = '1.0.dev42'",
+        "__version_tuple__ = version_tuple = (1, 0, 'dev42')",
+    ]
+
+    version = "1.0.1+g4ac9d2c"
+    scm_version = meta("1.0.1", node="g4ac9d2c", config=c)
+    dump_version(
+        tmp_path, version, "second.py", scm_version=scm_version, template=template
+    )
+    lines = read("second.py").splitlines()
+    assert "__version__ = version = '1.0.1+g4ac9d2c'" in lines
+    assert "__version_tuple__ = version_tuple = (1, 0, 1, 'g4ac9d2c')" in lines
+    assert "__sha__ = 'g4ac9d2c'" in lines
+
+    version = "1.2.3.dev18+gb366d8b.d20210415"
+    scm_version = meta(
+        "1.2.3", node="gb366d8b", distance=18, node_date=date(2021, 4, 15), config=c
+    )
+    dump_version(
+        tmp_path, version, "third.py", scm_version=scm_version, template=template
+    )
+    lines = read("third.py").splitlines()
+    assert "__version__ = version = '1.2.3.dev18+gb366d8b.d20210415'" in lines
+    assert (
+        "__version_tuple__ = version_tuple = (1, 2, 3, 'dev18', 'gb366d8b.d20210415')"
+        in lines
+    )
+    assert "__sha__ = 'gb366d8b'" in lines
 
-def test_dump_version(tmpdir):
-    sp = tmpdir.strpath
-
-    dump_version(sp, "1.0", "first.txt")
-    assert tmpdir.join("first.txt").read() == "1.0"
-    dump_version(sp, "1.0", "first.py")
-    content = tmpdir.join("first.py").read()
-    assert repr("1.0") in content
     import ast
 
-    ast.parse(content)
+    ast.parse(read("third.py"))
 
 
-def test_parse_plain_fails(recwarn):
-    def parse(root):
+def test_parse_plain_fails(recwarn: pytest.WarningsRecorder) -> None:
+    def parse(root: object) -> str:
         return "tricked you"
 
     with pytest.raises(TypeError):
         setuptools_scm.get_version(parse=parse)
+
+
+def test_custom_version_cls() -> None:
+    """Test that `normalize` and `version_cls` work as expected"""
+
+    class MyVersion:
+        def __init__(self, tag_str: str) -> None:
+            self.version = tag_str
+
+        def __repr__(self) -> str:
+            return f"hello,{self.version}"
+
+    # you can not use normalize=False and version_cls at the same time
+    with pytest.raises(
+        ValueError,
+        match="Providing a custom `version_cls`"
+        " is not permitted when `normalize=False`",
+    ):
+        setuptools_scm.get_version(normalize=False, version_cls=MyVersion)
+
+    # TODO unfortunately with PRETEND_KEY the preformatted flag becomes True
+    #  which bypasses our class. which other mechanism would be ok to use here
+    #  to create a test?
+    # monkeypatch.setenv(setuptools_scm.PRETEND_KEY, "1.0.1")
+    # assert setuptools_scm.get_version(version_cls=MyVersion) == "1"
+
+
+def test_internal_get_version_warns_for_version_files(tmp_path: Path) -> None:
+    tmp_path.joinpath("PKG-INFO").write_text("Version: 0.1")
+    c = Configuration(root=tmp_path, fallback_root=tmp_path)
+    with pytest.warns(
+        DeprecationWarning,
+        match="force_write_version_files ought to be set,"
+        " presuming the legacy True value",
+    ):
+        ver = setuptools_scm._get_version(c)
+    assert ver == "0.1"
+
+    # force write won't write as no version file is configured
+    assert setuptools_scm._get_version(c, force_write_version_files=False) == ver
+
+    assert setuptools_scm._get_version(c, force_write_version_files=True) == ver
diff --git a/testing/test_cli.py b/testing/test_cli.py
new file mode 100644 (file)
index 0000000..cc5a0ef
--- /dev/null
@@ -0,0 +1,58 @@
+from __future__ import annotations
+
+import io
+from contextlib import redirect_stdout
+
+import pytest
+
+from .conftest import DebugMode
+from .test_git import wd as wd_fixture  # NOQA evil fixture reuse
+from .wd_wrapper import WorkDir
+from setuptools_scm._cli import main
+
+
+PYPROJECT_TOML = "pyproject.toml"
+PYPROJECT_SIMPLE = "[tool.setuptools_scm]"
+PYPROJECT_ROOT = '[tool.setuptools_scm]\nroot=".."'
+
+
+def get_output(args: list[str]) -> str:
+    with redirect_stdout(io.StringIO()) as out:
+        main(args)
+    return out.getvalue()
+
+
+warns_cli_root_override = pytest.warns(
+    UserWarning, match="root .. is overridden by the cli arg ."
+)
+warns_absolute_root_override = pytest.warns(
+    UserWarning, match="absolute root path '.*' overrides relative_to '.*'"
+)
+
+exits_with_not_found = pytest.raises(SystemExit, match="no version found for")
+
+
+def test_cli_find_pyproject(
+    wd: WorkDir, monkeypatch: pytest.MonkeyPatch, debug_mode: DebugMode
+) -> None:
+    debug_mode.disable()
+    wd.commit_testfile()
+    wd.write(PYPROJECT_TOML, PYPROJECT_SIMPLE)
+    monkeypatch.chdir(wd.cwd)
+
+    out = get_output([])
+    assert out.startswith("0.1.dev1+")
+
+    with exits_with_not_found:
+        get_output(["--root=.."])
+
+    wd.write(PYPROJECT_TOML, PYPROJECT_ROOT)
+    with exits_with_not_found:
+        print(get_output(["-c", PYPROJECT_TOML]))
+
+    with exits_with_not_found, warns_absolute_root_override:
+        get_output(["-c", PYPROJECT_TOML, "--root=.."])
+
+    with warns_cli_root_override:
+        out = get_output(["-c", PYPROJECT_TOML, "--root=."])
+    assert out.startswith("0.1.dev1+")
index 49f1d7ae50ad7816b2fe22f85d0e4a40ce6ce255..668ddd028848d38af4a396d69a54b5011d4061a1 100644 (file)
@@ -1,12 +1,16 @@
-from __future__ import unicode_literals
+from __future__ import annotations
 
-from setuptools_scm.config import Configuration
 import re
+import textwrap
+from pathlib import Path
+
 import pytest
 
+from setuptools_scm import Configuration
+
 
 @pytest.mark.parametrize(
-    "tag, expected_version",
+    ("tag", "expected_version"),
     [
         ("apache-arrow-0.9.0", "0.9.0"),
         ("arrow-0.9.0", "0.9.0"),
@@ -19,7 +23,7 @@ import pytest
         ("V1.1", "V1.1"),
     ],
 )
-def test_tag_regex(tag, expected_version):
+def test_tag_regex(tag: str, expected_version: str) -> None:
     config = Configuration()
     match = config.tag_regex.match(tag)
     assert match
@@ -27,13 +31,69 @@ def test_tag_regex(tag, expected_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")
+def test_config_from_pyproject(tmp_path: Path) -> None:
+    fn = tmp_path / "pyproject.toml"
+    fn.write_text(
+        textwrap.dedent(
+            """
+            [tool.setuptools_scm]
+            [project]
+            description = "Factory ⸻ A code generator 🏭"
+            authors = [{name = "Łukasz Langa"}]
+            """
+        ),
+        encoding="utf-8",
+    )
     assert Configuration.from_file(str(fn))
 
 
-def test_config_regex_init():
+def test_config_regex_init() -> None:
     tag_regex = re.compile(r"v(\d+)")
     conf = Configuration(tag_regex=tag_regex)
     assert conf.tag_regex is tag_regex
+
+
+def test_config_from_file_protects_relative_to(tmp_path: Path) -> None:
+    fn = tmp_path / "pyproject.toml"
+    fn.write_text(
+        textwrap.dedent(
+            """
+            [tool.setuptools_scm]
+            relative_to = "dont_use_me"
+            [project]
+            description = "Factory ⸻ A code generator 🏭"
+            authors = [{name = "Łukasz Langa"}]
+            """
+        ),
+        encoding="utf-8",
+    )
+    with pytest.warns(
+        UserWarning,
+        match=".*pyproject.toml: at \\[tool.setuptools_scm\\]\n"
+        "ignoring value relative_to='dont_use_me'"
+        " as its always relative to the config file",
+    ):
+        assert Configuration.from_file(str(fn))
+
+
+def test_config_overrides(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
+    fn = tmp_path / "pyproject.toml"
+    fn.write_text(
+        textwrap.dedent(
+            """
+            [tool.setuptools_scm]
+            root = "."
+            [project]
+            name = "teSt-.a"
+            """
+        ),
+        encoding="utf-8",
+    )
+    pristine = Configuration.from_file(fn)
+    monkeypatch.setenv(
+        "SETUPTOOLS_SCM_OVERRIDES_FOR_TEST_A", '{root="..", fallback_root=".."}'
+    )
+    overriden = Configuration.from_file(fn)
+
+    assert pristine.root != overriden.root
+    assert pristine.fallback_root != overriden.fallback_root
index 463d3d4fa84b6a2e661d77386fc4edbf54835e05..21b523a84392050bfb18ab927e0349f7b5da4282 100644 (file)
@@ -1,16 +1,21 @@
+from __future__ import annotations
+
 import os
 import sys
+from typing import Iterable
 
 import pytest
 
-from setuptools_scm.integration import find_files
+from .wd_wrapper import WorkDir
+from setuptools_scm._file_finders import find_files
 
 
 @pytest.fixture(params=["git", "hg"])
-def inwd(request, wd, monkeypatch):
-    if request.param == "git":
-        if sys.platform == "win32" and sys.version_info[0] < 3:
-            pytest.skip("Long/short path names supported on Windows Python 2.7")
+def inwd(
+    request: pytest.FixtureRequest, wd: WorkDir, monkeypatch: pytest.MonkeyPatch
+) -> WorkDir:
+    param: str = request.param  # type: ignore
+    if param == "git":
         try:
             wd("git init")
         except OSError:
@@ -19,7 +24,7 @@ def inwd(request, wd, monkeypatch):
         wd('git config user.name "a test"')
         wd.add_command = "git add ."
         wd.commit_command = "git commit -m test-{reason}"
-    elif request.param == "hg":
+    elif param == "hg":
         try:
             wd("hg init")
         except OSError:
@@ -33,28 +38,29 @@ def inwd(request, wd, monkeypatch):
     bdir = wd.cwd / "bdir"
     bdir.mkdir()
     (bdir / "fileb").touch()
-    wd.add_and_commit()
+    if request.node.get_closest_marker("skip_commit") is None:
+        wd.add_and_commit()
     monkeypatch.chdir(wd.cwd)
-    yield wd
+    return wd
 
 
-def _sep(paths):
+def _sep(paths: Iterable[str]) -> set[str]:
     return {path.replace("/", os.path.sep) for path in paths}
 
 
-def test_basic(inwd):
+def test_basic(inwd: WorkDir) -> None:
     assert set(find_files()) == _sep({"file1", "adir/filea", "bdir/fileb"})
     assert set(find_files(".")) == _sep({"./file1", "./adir/filea", "./bdir/fileb"})
     assert set(find_files("adir")) == _sep({"adir/filea"})
 
 
-def test_whitespace(inwd):
+def test_whitespace(inwd: WorkDir) -> None:
     (inwd.cwd / "adir" / "space file").touch()
     inwd.add_and_commit()
     assert set(find_files("adir")) == _sep({"adir/space file", "adir/filea"})
 
 
-def test_case(inwd):
+def test_case(inwd: WorkDir) -> None:
     (inwd.cwd / "CamelFile").touch()
     (inwd.cwd / "file2").touch()
     inwd.add_and_commit()
@@ -64,14 +70,14 @@ def test_case(inwd):
 
 
 @pytest.mark.skipif(sys.platform == "win32", reason="symlinks to dir not supported")
-def test_symlink_dir(inwd):
+def test_symlink_dir(inwd: WorkDir) -> None:
     (inwd.cwd / "adir" / "bdirlink").symlink_to("../bdir")
     inwd.add_and_commit()
     assert set(find_files("adir")) == _sep({"adir/filea", "adir/bdirlink/fileb"})
 
 
 @pytest.mark.skipif(sys.platform == "win32", reason="symlinks to dir not supported")
-def test_symlink_dir_source_not_in_scm(inwd):
+def test_symlink_dir_source_not_in_scm(inwd: WorkDir) -> None:
     (inwd.cwd / "adir" / "bdirlink").symlink_to("../bdir")
     assert set(find_files("adir")) == _sep({"adir/filea"})
 
@@ -79,7 +85,7 @@ def test_symlink_dir_source_not_in_scm(inwd):
 @pytest.mark.skipif(
     sys.platform == "win32", reason="symlinks to files not supported on windows"
 )
-def test_symlink_file(inwd):
+def test_symlink_file(inwd: WorkDir) -> None:
     (inwd.cwd / "adir" / "file1link").symlink_to("../file1")
     inwd.add_and_commit()
     assert set(find_files("adir")) == _sep(
@@ -90,20 +96,20 @@ def test_symlink_file(inwd):
 @pytest.mark.skipif(
     sys.platform == "win32", reason="symlinks to files not supported on windows"
 )
-def test_symlink_file_source_not_in_scm(inwd):
+def test_symlink_file_source_not_in_scm(inwd: WorkDir) -> None:
     (inwd.cwd / "adir" / "file1link").symlink_to("../file1")
     assert set(find_files("adir")) == _sep({"adir/filea"})
 
 
 @pytest.mark.skipif(sys.platform == "win32", reason="symlinks to dir not supported")
-def test_symlink_loop(inwd):
+def test_symlink_loop(inwd: WorkDir) -> None:
     (inwd.cwd / "adir" / "loop").symlink_to("../adir")
     inwd.add_and_commit()
     assert set(find_files("adir")) == _sep({"adir/filea", "adir/loop"})  # -> ../adir
 
 
 @pytest.mark.skipif(sys.platform == "win32", reason="symlinks to dir not supported")
-def test_symlink_loop_outside_path(inwd):
+def test_symlink_loop_outside_path(inwd: WorkDir) -> None:
     (inwd.cwd / "bdir" / "loop").symlink_to("../bdir")
     (inwd.cwd / "adir" / "bdirlink").symlink_to("../bdir")
     inwd.add_and_commit()
@@ -111,7 +117,7 @@ def test_symlink_loop_outside_path(inwd):
 
 
 @pytest.mark.skipif(sys.platform == "win32", reason="symlinks to dir not supported")
-def test_symlink_dir_out_of_git(inwd):
+def test_symlink_dir_out_of_git(inwd: WorkDir) -> None:
     (inwd.cwd / "adir" / "outsidedirlink").symlink_to(os.path.join(__file__, ".."))
     inwd.add_and_commit()
     assert set(find_files("adir")) == _sep({"adir/filea"})
@@ -120,13 +126,21 @@ def test_symlink_dir_out_of_git(inwd):
 @pytest.mark.skipif(
     sys.platform == "win32", reason="symlinks to files not supported on windows"
 )
-def test_symlink_file_out_of_git(inwd):
+def test_symlink_file_out_of_git(inwd: WorkDir) -> None:
     (inwd.cwd / "adir" / "outsidefilelink").symlink_to(__file__)
     inwd.add_and_commit()
     assert set(find_files("adir")) == _sep({"adir/filea"})
 
 
-def test_empty_root(inwd):
+@pytest.mark.parametrize("path_add", ["{cwd}", "{cwd}" + os.pathsep + "broken"])
+def test_ignore_root(
+    inwd: WorkDir, monkeypatch: pytest.MonkeyPatch, path_add: str
+) -> None:
+    monkeypatch.setenv("SETUPTOOLS_SCM_IGNORE_VCS_ROOTS", path_add.format(cwd=inwd.cwd))
+    assert find_files() == []
+
+
+def test_empty_root(inwd: WorkDir) -> None:
     subdir = inwd.cwd / "cdir" / "subdir"
     subdir.mkdir(parents=True)
     (subdir / "filec").touch()
@@ -134,7 +148,7 @@ def test_empty_root(inwd):
     assert set(find_files("cdir")) == _sep({"cdir/subdir/filec"})
 
 
-def test_empty_subdir(inwd):
+def test_empty_subdir(inwd: WorkDir) -> None:
     subdir = inwd.cwd / "adir" / "emptysubdir" / "subdir"
     subdir.mkdir(parents=True)
     (subdir / "xfile").touch()
@@ -145,7 +159,7 @@ def test_empty_subdir(inwd):
 
 
 @pytest.mark.skipif(sys.platform == "win32", reason="symlinks not supported on windows")
-def test_double_include_through_symlink(inwd):
+def test_double_include_through_symlink(inwd: WorkDir) -> None:
     (inwd.cwd / "data").mkdir()
     (inwd.cwd / "data" / "datafile").touch()
     (inwd.cwd / "adir" / "datalink").symlink_to("../data")
@@ -164,7 +178,7 @@ def test_double_include_through_symlink(inwd):
 
 
 @pytest.mark.skipif(sys.platform == "win32", reason="symlinks not supported on windows")
-def test_symlink_not_in_scm_while_target_is(inwd):
+def test_symlink_not_in_scm_while_target_is(inwd: WorkDir) -> None:
     (inwd.cwd / "data").mkdir()
     (inwd.cwd / "data" / "datafile").touch()
     inwd.add_and_commit()
@@ -180,3 +194,39 @@ def test_symlink_not_in_scm_while_target_is(inwd):
             "data/datafile",
         }
     )
+
+
+@pytest.mark.issue(587)
+@pytest.mark.skip_commit()
+def test_not_commited(inwd: WorkDir) -> None:
+    assert find_files() == []
+
+
+def test_unexpanded_git_archival(wd: WorkDir, monkeypatch: pytest.MonkeyPatch) -> None:
+    # When substitutions in `.git_archival.txt` are not expanded, files should
+    # not be automatically listed.
+    monkeypatch.chdir(wd.cwd)
+    (wd.cwd / ".git_archival.txt").write_text("node: $Format:%H$", encoding="utf-8")
+    (wd.cwd / "file1.txt").touch()
+    assert find_files() == []
+
+
+@pytest.mark.parametrize("archive_file", [".git_archival.txt", ".hg_archival.txt"])
+def test_archive(
+    wd: WorkDir, monkeypatch: pytest.MonkeyPatch, archive_file: str
+) -> None:
+    # When substitutions in `.git_archival.txt` are not expanded, files should
+    # not be automatically listed.
+    monkeypatch.chdir(wd.cwd)
+    sha = "a1bda3d984d1a40d7b00ae1d0869354d6d503001"
+    (wd.cwd / archive_file).write_text(f"node: {sha}", encoding="utf-8")
+    (wd.cwd / "data").mkdir()
+    (wd.cwd / "data" / "datafile").touch()
+
+    datalink = wd.cwd / "data" / "datalink"
+    if sys.platform != "win32":
+        datalink.symlink_to("data/datafile")
+    else:
+        os.link("data/datafile", datalink)
+
+    assert set(find_files()) == _sep({archive_file, "data/datafile", "data/datalink"})
index 808a1d12b7babfa35f2f51cdd65731c7a8384488..9a4a09b499631c3ce56eaf4ab6a7b0f81e767f17 100644 (file)
-import pytest
-import sys
-import pkg_resources
-from setuptools_scm import dump_version, get_version, PRETEND_KEY
-from setuptools_scm.version import (
-    guess_next_version,
-    meta,
-    format_version,
-    tag_to_version,
-)
+from __future__ import annotations
 
-from setuptools_scm.config import Configuration
-from setuptools_scm.utils import has_command
+import shutil
+import subprocess
+from pathlib import Path
 
-PY3 = sys.version_info > (2,)
+import pytest
 
+from setuptools_scm import Configuration
+from setuptools_scm import dump_version
+from setuptools_scm import get_version
+from setuptools_scm._overrides import PRETEND_KEY
+from setuptools_scm._run_cmd import has_command
+from setuptools_scm.version import format_version
+from setuptools_scm.version import guess_next_version
+from setuptools_scm.version import meta
+from setuptools_scm.version import tag_to_version
 
-class MockTime(object):
-    def __format__(self, *k):
-        return "time"
+c = Configuration()
 
 
 @pytest.mark.parametrize(
-    "tag, expected",
+    ("tag", "expected"),
     [
         ("1.1", "1.2"),
         ("1.2.dev", "1.2"),
         ("1.1a2", "1.1a3"),
-        ("23.24.post2+deadbeef", "23.24.post3"),
+        pytest.param(
+            "23.24.post2+deadbeef",
+            "23.24.post3",
+            marks=pytest.mark.filterwarnings(
+                "ignore:.*will be stripped of its suffix.*:UserWarning"
+            ),
+        ),
     ],
 )
-def test_next_tag(tag, expected):
-    version = pkg_resources.parse_version(tag)
+def test_next_tag(tag: str, expected: str) -> None:
+    version = meta(tag, config=c)
     assert guess_next_version(version) == expected
 
 
-c = Configuration()
-
 VERSIONS = {
-    "exact": meta("1.1", distance=None, dirty=False, config=c),
-    "zerodistance": meta("1.1", distance=0, dirty=False, config=c),
-    "dirty": meta("1.1", distance=None, dirty=True, config=c),
-    "distance": meta("1.1", distance=3, dirty=False, config=c),
-    "distancedirty": meta("1.1", distance=3, dirty=True, config=c),
+    "exact": meta("1.1", distance=0, dirty=False, config=c),
+    "dirty": meta("1.1", distance=0, dirty=True, config=c),
+    "distance-clean": meta("1.1", distance=3, dirty=False, config=c),
+    "distance-dirty": meta("1.1", distance=3, dirty=True, config=c),
 }
 
 
 @pytest.mark.parametrize(
-    "version,scheme,expected",
+    ("version", "version_scheme", "local_scheme", "expected"),
     [
-        ("exact", "guess-next-dev node-and-date", "1.1"),
-        ("zerodistance", "guess-next-dev node-and-date", "1.2.dev0"),
-        ("zerodistance", "guess-next-dev no-local-version", "1.2.dev0"),
-        ("dirty", "guess-next-dev node-and-date", "1.2.dev0+dtime"),
-        ("dirty", "guess-next-dev no-local-version", "1.2.dev0"),
-        ("distance", "guess-next-dev node-and-date", "1.2.dev3"),
-        ("distancedirty", "guess-next-dev node-and-date", "1.2.dev3+dtime"),
-        ("distancedirty", "guess-next-dev no-local-version", "1.2.dev3"),
-        ("exact", "post-release node-and-date", "1.1"),
-        ("zerodistance", "post-release node-and-date", "1.1.post0"),
-        ("dirty", "post-release node-and-date", "1.1.post0+dtime"),
-        ("distance", "post-release node-and-date", "1.1.post3"),
-        ("distancedirty", "post-release node-and-date", "1.1.post3+dtime"),
+        ("exact", "guess-next-dev", "node-and-date", "1.1"),
+        ("dirty", "guess-next-dev", "node-and-date", "1.2.dev0+d20090213"),
+        ("dirty", "guess-next-dev", "no-local-version", "1.2.dev0"),
+        ("distance-clean", "guess-next-dev", "node-and-date", "1.2.dev3"),
+        ("distance-dirty", "guess-next-dev", "node-and-date", "1.2.dev3+d20090213"),
+        ("exact", "post-release", "node-and-date", "1.1"),
+        ("dirty", "post-release", "node-and-date", "1.1.post0+d20090213"),
+        ("distance-clean", "post-release", "node-and-date", "1.1.post3"),
+        ("distance-dirty", "post-release", "node-and-date", "1.1.post3+d20090213"),
     ],
 )
-def test_format_version(version, monkeypatch, scheme, expected):
-    version = VERSIONS[version]
-    monkeypatch.setattr(version, "time", MockTime())
-    vs, ls = scheme.split()
-    assert format_version(version, version_scheme=vs, local_scheme=ls) == expected
-
-
-def test_dump_version_doesnt_bail_on_value_error(tmpdir):
+def test_format_version(
+    version: str, version_scheme: str, local_scheme: str, expected: str
+) -> None:
+    from dataclasses import replace
+
+    scm_version = VERSIONS[version]
+    configured_version = replace(
+        scm_version,
+        config=replace(
+            scm_version.config, version_scheme=version_scheme, local_scheme=local_scheme
+        ),
+    )
+    assert format_version(configured_version) == expected
+
+
+def test_dump_version_doesnt_bail_on_value_error(tmp_path: Path) -> None:
     write_to = "VERSION"
     version = str(VERSIONS["exact"].tag)
-    with pytest.raises(ValueError) as exc_info:
-        dump_version(tmpdir.strpath, version, write_to)
-    assert str(exc_info.value).startswith("bad file format:")
+    scm_version = meta(VERSIONS["exact"].tag, config=c)
+    with pytest.raises(ValueError, match="^bad file format:"):
+        dump_version(tmp_path, version, write_to, scm_version=scm_version)
 
 
 @pytest.mark.parametrize(
     "version", ["1.0", "1.2.3.dev1+ge871260", "1.2.3.dev15+ge871260.d20180625"]
 )
-def test_dump_version_works_with_pretend(version, tmpdir, monkeypatch):
+def test_dump_version_works_with_pretend(
+    version: str, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
+) -> None:
+    monkeypatch.setenv(PRETEND_KEY, version)
+    name = "VERSION.txt"
+    target = tmp_path.joinpath(name)
+    get_version(root=tmp_path, write_to=name)
+    assert target.read_text() == version
+
+
+def test_dump_version_modern(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
+    version = "1.2.3"
     monkeypatch.setenv(PRETEND_KEY, version)
-    get_version(write_to=str(tmpdir.join("VERSION.txt")))
-    assert tmpdir.join("VERSION.txt").read() == version
+    name = "VERSION.txt"
+
+    project = tmp_path.joinpath("project")
+    target = project.joinpath(name)
+    project.mkdir()
+
+    get_version(root="..", relative_to=target, version_file=name)
+    assert target.read_text() == version
+
+
+def dump_a_version(tmp_path: Path) -> None:
+    from setuptools_scm._integration.dump_version import write_version_to_path
+
+    version = "1.2.3"
+    scm_version = meta(version, config=c)
+    write_version_to_path(
+        tmp_path / "VERSION.py", template=None, version=version, scm_version=scm_version
+    )
+
+
+def test_dump_version_on_old_python(tmp_path: Path) -> None:
+    python37 = shutil.which("python3.7")
+    if python37 is None:
+        pytest.skip("python3.7 not found")
+    dump_a_version(tmp_path)
+    subprocess.run(
+        [python37, "-c", "import VERSION;print(VERSION.version)"],
+        cwd=tmp_path,
+        check=True,
+    )
+
+
+def test_dump_version_mypy(tmp_path: Path) -> None:
+    mypy = shutil.which("mypy")
+    if mypy is None:
+        pytest.skip("mypy not found")
+    dump_a_version(tmp_path)
+    subprocess.run(
+        [mypy, "--python-version=3.8", "--strict", "VERSION.py"],
+        cwd=tmp_path,
+        check=True,
+    )
+
+
+def test_dump_version_flake8(tmp_path: Path) -> None:
+    flake8 = shutil.which("flake8")
+    if flake8 is None:
+        pytest.skip("flake8 not found")
+    dump_a_version(tmp_path)
+    subprocess.run([flake8, "VERSION.py"], cwd=tmp_path, check=True)
+
+
+def test_has_command() -> None:
+    with pytest.warns(RuntimeWarning, match="yadayada"):
+        assert not has_command("yadayada_setuptools_aint_ne")
 
 
-def test_has_command(recwarn):
-    assert not has_command("yadayada_setuptools_aint_ne")
-    msg = recwarn.pop()
-    assert "yadayada" in str(msg.message)
+def test_has_command_logs_stderr(caplog: pytest.LogCaptureFixture) -> None:
+    """
+    If the name provided to has_command() exists as a command, but gives a non-zero
+    return code, there should be a log message generated.
+    """
+    with pytest.warns(RuntimeWarning, match="ls"):
+        has_command("ls", ["--a-flag-that-doesnt-exist-should-give-output-on-stderr"])
+    found_it = False
+    for record in caplog.records:
+        if "returned non-zero. This is stderr" in record.message:
+            found_it = True
+    assert found_it, "Did not find expected log record for "
 
 
 @pytest.mark.parametrize(
-    "tag, expected_version",
+    ("tag", "expected_version"),
     [
         ("1.1", "1.1"),
         ("release-1.1", "1.1"),
         pytest.param("3.3.1-rc26", "3.3.1rc26", marks=pytest.mark.issue(266)),
     ],
 )
-def test_tag_to_version(tag, expected_version):
-    version = str(tag_to_version(tag))
+def test_tag_to_version(tag: str, expected_version: str) -> None:
+    version = str(tag_to_version(tag, c))
     assert version == expected_version
index 542a29f72c2620cbb51a148f0046a7dc2b192542..79fede3ace7d3479ae108b58c52a43ddb7e2290e 100644 (file)
-import sys
+from __future__ import annotations
 
-from setuptools_scm import integration
-from setuptools_scm.utils import do, has_command
-from setuptools_scm import git
-import pytest
+import contextlib
+import os
+import shutil
+import subprocess
+import sys
+from datetime import date
 from datetime import datetime
+from datetime import timezone
 from os.path import join as opj
-from setuptools_scm.file_finder_git import git_find_files
-import warnings
+from pathlib import Path
+from textwrap import dedent
+from typing import Generator
+from unittest.mock import Mock
+from unittest.mock import patch
 
+import pytest
 
-skip_if_win_27 = pytest.mark.skipif(
-    sys.platform == "win32" and sys.version_info[0] < 3,
-    reason="Not supported on Windows + Python 2.7",
+import setuptools_scm._file_finders
+from .conftest import DebugMode
+from .wd_wrapper import WorkDir
+from setuptools_scm import Configuration
+from setuptools_scm import git
+from setuptools_scm import NonNormalizedVersion
+from setuptools_scm._file_finders.git import git_find_files
+from setuptools_scm._run_cmd import CommandNotFoundError
+from setuptools_scm._run_cmd import CompletedProcess
+from setuptools_scm._run_cmd import has_command
+from setuptools_scm._run_cmd import run
+from setuptools_scm.git import archival_to_version
+from setuptools_scm.version import format_version
+
+pytestmark = pytest.mark.skipif(
+    not has_command("git", warn=False), reason="git executable not found"
 )
 
 
-with warnings.catch_warnings():
-    warnings.filterwarnings("ignore")
-    if not has_command("git"):
-        pytestmark = pytest.mark.skip(reason="git executable not found")
-
-
-@pytest.fixture
-def wd(wd, monkeypatch):
+@pytest.fixture(name="wd")
+def wd(wd: WorkDir, monkeypatch: pytest.MonkeyPatch, debug_mode: DebugMode) -> WorkDir:
+    debug_mode.disable()
     monkeypatch.delenv("HOME", raising=False)
     wd("git init")
     wd("git config user.email test@example.com")
     wd('git config user.name "a test"')
     wd.add_command = "git add ."
     wd.commit_command = "git commit -m test-{reason}"
+    debug_mode.enable()
     return wd
 
 
 @pytest.mark.parametrize(
-    "given, tag, number, node, dirty",
+    ("given", "tag", "number", "node", "dirty"),
     [
         ("3.3.1-rc26-0-g9df187b", "3.3.1-rc26", 0, "g9df187b", False),
         ("17.33.0-rc-17-g38c3047c0", "17.33.0-rc", 17, "g38c3047c0", False),
     ],
 )
-def test_parse_describe_output(given, tag, number, node, dirty):
+def test_parse_describe_output(
+    given: str, tag: str, number: int, node: str, dirty: bool
+) -> None:
     parsed = git._git_parse_describe(given)
     assert parsed == (tag, number, node, dirty)
 
 
-def test_root_relative_to(tmpdir, wd, monkeypatch):
+def test_root_relative_to(wd: WorkDir, monkeypatch: pytest.MonkeyPatch) -> None:
     monkeypatch.delenv("SETUPTOOLS_SCM_DEBUG")
     p = wd.cwd.joinpath("sub/package")
     p.mkdir(parents=True)
     p.joinpath("setup.py").write_text(
-        u"""from setuptools import setup
+        """from setuptools import setup
 setup(use_scm_version={"root": "../..",
                        "relative_to": __file__})
 """
     )
-    res = do((sys.executable, "setup.py", "--version"), p)
-    assert res == "0.1.dev0"
+    res = run([sys.executable, "setup.py", "--version"], p)
+    assert res.stdout == "0.1.dev0+d20090213"
+
+
+def test_root_search_parent_directories(
+    wd: WorkDir, monkeypatch: pytest.MonkeyPatch
+) -> None:
+    monkeypatch.delenv("SETUPTOOLS_SCM_DEBUG")
+    p = wd.cwd.joinpath("sub/package")
+    p.mkdir(parents=True)
+    p.joinpath("setup.py").write_text(
+        """from setuptools import setup
+setup(use_scm_version={"search_parent_directories": True})
+"""
+    )
+    res = run([sys.executable, "setup.py", "--version"], p)
+    assert res.stdout == "0.1.dev0+d20090213"
+
+
+def test_git_gone(wd: WorkDir, monkeypatch: pytest.MonkeyPatch) -> None:
+    monkeypatch.setenv("PATH", str(wd.cwd / "not-existing"))
+
+    wd.write("pyproject.toml", "[tool.setuptools_scm]")
+    with pytest.raises(CommandNotFoundError, match=r"git"):
+        git.parse(wd.cwd, Configuration(), git.DEFAULT_DESCRIBE)
+
+    assert wd.get_version(fallback_version="1.0") == "1.0"
 
 
 @pytest.mark.issue("https://github.com/pypa/setuptools_scm/issues/298")
 @pytest.mark.issue(403)
-def test_file_finder_no_history(wd, caplog):
+def test_file_finder_no_history(wd: WorkDir, caplog: pytest.LogCaptureFixture) -> None:
     file_list = git_find_files(str(wd.cwd))
     assert file_list == []
 
@@ -69,131 +112,252 @@ def test_file_finder_no_history(wd, caplog):
 
 
 @pytest.mark.issue("https://github.com/pypa/setuptools_scm/issues/281")
-def test_parse_call_order(wd):
-    git.parse(str(wd.cwd), git.DEFAULT_DESCRIBE)
+def test_parse_call_order(wd: WorkDir) -> None:
+    git.parse(str(wd.cwd), Configuration(), git.DEFAULT_DESCRIBE)
+
+
+def sudo_devnull(
+    args: list[str | os.PathLike[str]], check: bool = False
+) -> subprocess.CompletedProcess[bytes]:
+    """shortcut to run sudo with non-interactive input"""
+    return subprocess.run(
+        ["sudo", *args],
+        stdin=subprocess.DEVNULL,
+        check=check,
+    )
+
+
+@contextlib.contextmanager
+def break_folder_permissions(path: Path) -> Generator[None, None, None]:
+    """break the permissions of a folder for a while"""
+    if not shutil.which("sudo"):
+        pytest.skip("sudo executable not found")
+    original_stat = path.stat()
+
+    proc = sudo_devnull(["chown", "-R", "12345", path])
+    if proc.returncode != 0:
+        pytest.xfail("Failed to change ownership, is passwordless sudo available?")
+
+    try:
+        sudo_devnull(["chmod", "a+r", path], check=True)
+        sudo_devnull(["chgrp", "-R", "12345", path], check=True)
+        yield
+    finally:
+        # Restore the ownership
+        sudo_devnull(["chown", "-R", str(original_stat.st_uid), path], check=True)
+        sudo_devnull(["chgrp", "-R", str(original_stat.st_gid), path], check=True)
 
 
-def test_version_from_git(wd):
-    assert wd.version == "0.1.dev0"
+@pytest.mark.issue("https://github.com/pypa/setuptools_scm/issues/707")
+def test_not_owner(wd: WorkDir) -> None:
+    with break_folder_permissions(wd.cwd):
+        assert git.parse(str(wd.cwd), Configuration())
+
+
+def test_version_from_git(wd: WorkDir) -> None:
+    assert wd.get_version() == "0.1.dev0+d20090213"
+
+    parsed = git.parse(str(wd.cwd), Configuration(), git.DEFAULT_DESCRIBE)
+    assert parsed is not None
+    assert parsed.branch in ("master", "main")
 
     wd.commit_testfile()
-    assert wd.version.startswith("0.1.dev1+g")
-    assert not wd.version.endswith("1-")
+    assert wd.get_version().startswith("0.1.dev1+g")
+    assert not wd.get_version().endswith("1-")
 
     wd("git tag v0.1")
-    assert wd.version == "0.1"
+    assert wd.get_version() == "0.1"
 
     wd.write("test.txt", "test2")
-    assert wd.version.startswith("0.2.dev0+g")
+    assert wd.get_version().startswith("0.2.dev0+g")
 
     wd.commit_testfile()
-    assert wd.version.startswith("0.2.dev1+g")
+    assert wd.get_version().startswith("0.2.dev1+g")
 
     wd("git tag version-0.2")
-    assert wd.version.startswith("0.2")
+    assert wd.get_version().startswith("0.2")
 
     wd.commit_testfile()
     wd("git tag version-0.2.post210+gbe48adfpost3+g0cc25f2")
     with pytest.warns(
         UserWarning, match="tag '.*' will be stripped of its suffix '.*'"
     ):
-        assert wd.version.startswith("0.2")
+        assert wd.get_version().startswith("0.2")
 
     wd.commit_testfile()
     wd("git tag 17.33.0-rc")
-    assert wd.version == "17.33.0rc0"
+    assert wd.get_version() == "17.33.0rc0"
+
+    # custom normalization
+    assert wd.get_version(normalize=False) == "17.33.0-rc"
+    assert wd.get_version(version_cls=NonNormalizedVersion) == "17.33.0-rc"
+    assert (
+        wd.get_version(version_cls="setuptools_scm.NonNormalizedVersion")
+        == "17.33.0-rc"
+    )
+
+
+setup_py_with_normalize: dict[str, str] = {
+    "false": """
+        from setuptools import setup
+        setup(use_scm_version={'normalize': False, 'write_to': 'VERSION.txt'})
+        """,
+    "with_created_class": """
+        from setuptools import setup
+
+        class MyVersion:
+            def __init__(self, tag_str: str):
+                self.version = tag_str
+
+            def __repr__(self):
+                return self.version
+
+        setup(use_scm_version={'version_cls': MyVersion, 'write_to': 'VERSION.txt'})
+        """,
+    "with_named_import": """
+        from setuptools import setup
+        setup(use_scm_version={
+            'version_cls': 'setuptools_scm.NonNormalizedVersion',
+            'write_to': 'VERSION.txt'
+        })
+        """,
+}
+
+
+@pytest.mark.parametrize(
+    "setup_py_txt",
+    [pytest.param(text, id=key) for key, text in setup_py_with_normalize.items()],
+)
+def test_git_version_unnormalized_setuptools(
+    setup_py_txt: str, wd: WorkDir, monkeypatch: pytest.MonkeyPatch
+) -> None:
+    """
+    Test that when integrating with setuptools without normalization,
+    the version is not normalized in write_to files,
+    but still normalized by setuptools for the final dist metadata.
+    """
+    # monkeypatch.delenv("SETUPTOOLS_SCM_DEBUG")
+    monkeypatch.chdir(wd.cwd)
+    wd.write("setup.py", dedent(setup_py_txt))
+
+    # do git operations and tag
+    wd.commit_testfile()
+    wd("git tag 17.33.0-rc1")
+
+    # setuptools still normalizes using packaging.Version (removing the dash)
+    res = wd([sys.executable, "setup.py", "--version"])
+    assert res == "17.33.0rc1"
+
+    # but the version tag in the file is non-normalized (with the dash)
+    assert wd.cwd.joinpath("VERSION.txt").read_text() == "17.33.0-rc1"
 
 
 @pytest.mark.issue(179)
-def test_unicode_version_scheme(wd):
+def test_unicode_version_scheme(wd: WorkDir) -> None:
     scheme = b"guess-next-dev".decode("ascii")
     assert wd.get_version(version_scheme=scheme)
 
 
 @pytest.mark.issue(108)
 @pytest.mark.issue(109)
-def test_git_worktree(wd):
+def test_git_worktree(wd: WorkDir) -> None:
     wd.write("test.txt", "test2")
     # untracked files dont change the state
-    assert wd.version == "0.1.dev0"
+    assert wd.get_version() == "0.1.dev0+d20090213"
+
     wd("git add test.txt")
-    assert wd.version.startswith("0.1.dev0+d")
+    assert wd.get_version().startswith("0.1.dev0+d")
 
 
 @pytest.mark.issue(86)
-def test_git_dirty_notag(wd):
+@pytest.mark.parametrize("today", [False, True])
+def test_git_dirty_notag(
+    today: bool, wd: WorkDir, monkeypatch: pytest.MonkeyPatch
+) -> None:
+    if today:
+        monkeypatch.delenv("SOURCE_DATE_EPOCH", raising=False)
     wd.commit_testfile()
     wd.write("test.txt", "test2")
     wd("git add test.txt")
-    assert wd.version.startswith("0.1.dev1")
-    # the date on the tag is in UTC
-    today = datetime.utcnow().date()
-    # we are dirty, check for the tag
-    assert today.strftime(".d%Y%m%d") in wd.version
+    version = wd.get_version()
+
+    if today:
+        # the date on the tag is in UTC
+        tag = datetime.now(timezone.utc).date().strftime(".d%Y%m%d")
+    else:
+        tag = ".d20090213"
+    assert version.startswith("0.1.dev1+g")
+    assert version.endswith(tag)
 
 
 @pytest.mark.issue(193)
-def test_git_worktree_support(wd, tmpdir):
+@pytest.mark.xfail(reason="sometimes relative path results")
+def test_git_worktree_support(wd: WorkDir, tmp_path: Path) -> None:
     wd.commit_testfile()
-    worktree = tmpdir.join("work_tree")
+    worktree = tmp_path / "work_tree"
     wd("git worktree add -b work-tree %s" % worktree)
 
-    res = do([sys.executable, "-m", "setuptools_scm", "ls"], cwd=worktree)
-    assert str(worktree) in res
+    res = run([sys.executable, "-m", "setuptools_scm", "ls"], cwd=worktree)
+    assert "test.txt" in res.stdout
+    assert str(worktree) in res.stdout
 
 
-@pytest.fixture
-def shallow_wd(wd, tmpdir):
+@pytest.fixture()
+def shallow_wd(wd: WorkDir, tmp_path: Path) -> Path:
     wd.commit_testfile()
     wd.commit_testfile()
     wd.commit_testfile()
-    target = tmpdir.join("wd_shallow")
-    do(["git", "clone", "file://%s" % wd.cwd, str(target), "--depth=1"])
+    target = tmp_path / "wd_shallow"
+    run(["git", "clone", f"file://{wd.cwd}", target, "--depth=1"], tmp_path, check=True)
     return target
 
 
-def test_git_parse_shallow_warns(shallow_wd, recwarn):
-    git.parse(str(shallow_wd))
+def test_git_parse_shallow_warns(
+    shallow_wd: Path, recwarn: pytest.WarningsRecorder
+) -> None:
+    git.parse(shallow_wd, Configuration())
+    print(list(recwarn))
     msg = recwarn.pop()
     assert "is shallow and may cause errors" in str(msg.message)
 
 
-def test_git_parse_shallow_fail(shallow_wd):
-    with pytest.raises(ValueError) as einfo:
-        git.parse(str(shallow_wd), pre_parse=git.fail_on_shallow)
-
-    assert "git fetch" in str(einfo.value)
+def test_git_parse_shallow_fail(shallow_wd: Path) -> None:
+    with pytest.raises(ValueError, match="git fetch"):
+        git.parse(str(shallow_wd), Configuration(), pre_parse=git.fail_on_shallow)
 
 
-def test_git_shallow_autocorrect(shallow_wd, recwarn):
-    git.parse(str(shallow_wd), pre_parse=git.fetch_on_shallow)
+def test_git_shallow_autocorrect(
+    shallow_wd: Path, recwarn: pytest.WarningsRecorder
+) -> None:
+    git.parse(str(shallow_wd), Configuration(), pre_parse=git.fetch_on_shallow)
     msg = recwarn.pop()
     assert "git fetch was used to rectify" in str(msg.message)
-    git.parse(str(shallow_wd), pre_parse=git.fail_on_shallow)
+    git.parse(str(shallow_wd), Configuration(), pre_parse=git.fail_on_shallow)
 
 
-def test_find_files_stop_at_root_git(wd):
+def test_find_files_stop_at_root_git(wd: WorkDir) -> None:
     wd.commit_testfile()
     project = wd.cwd / "project"
     project.mkdir()
     project.joinpath("setup.cfg").touch()
-    assert integration.find_files(str(project)) == []
+    assert setuptools_scm._file_finders.find_files(str(project)) == []
 
 
 @pytest.mark.issue(128)
-def test_parse_no_worktree(tmpdir):
-    ret = git.parse(str(tmpdir))
+def test_parse_no_worktree(tmp_path: Path) -> None:
+    ret = git.parse(str(tmp_path), Configuration(root=str(tmp_path)))
     assert ret is None
 
 
-def test_alphanumeric_tags_match(wd):
+def test_alphanumeric_tags_match(wd: WorkDir) -> None:
     wd.commit_testfile()
     wd("git tag newstyle-development-started")
-    assert wd.version.startswith("0.1.dev1+g")
+    assert wd.get_version().startswith("0.1.dev1+g")
 
 
-@skip_if_win_27
-def test_git_archive_export_ignore(wd, monkeypatch):
+def test_git_archive_export_ignore(
+    wd: WorkDir, monkeypatch: pytest.MonkeyPatch
+) -> None:
     wd.write("test1.txt", "test")
     wd.write("test2.txt", "test")
     wd.write(
@@ -205,32 +369,42 @@ def test_git_archive_export_ignore(wd, monkeypatch):
     wd("git add test1.txt test2.txt")
     wd.commit()
     monkeypatch.chdir(wd.cwd)
-    assert integration.find_files(".") == [opj(".", "test1.txt")]
+    assert setuptools_scm._file_finders.find_files(".") == [opj(".", "test1.txt")]
 
 
-@skip_if_win_27
 @pytest.mark.issue(228)
-def test_git_archive_subdirectory(wd, monkeypatch):
-    wd("mkdir foobar")
+def test_git_archive_subdirectory(wd: WorkDir, monkeypatch: pytest.MonkeyPatch) -> None:
+    os.mkdir(wd.cwd / "foobar")
     wd.write("foobar/test1.txt", "test")
     wd("git add foobar")
     wd.commit()
     monkeypatch.chdir(wd.cwd)
-    assert integration.find_files(".") == [opj(".", "foobar", "test1.txt")]
+    assert setuptools_scm._file_finders.find_files(".") == [
+        opj(".", "foobar", "test1.txt")
+    ]
 
 
-@skip_if_win_27
 @pytest.mark.issue(251)
-def test_git_archive_run_from_subdirectory(wd, monkeypatch):
-    wd("mkdir foobar")
+def test_git_archive_run_from_subdirectory(
+    wd: WorkDir, monkeypatch: pytest.MonkeyPatch
+) -> None:
+    os.mkdir(wd.cwd / "foobar")
     wd.write("foobar/test1.txt", "test")
     wd("git add foobar")
     wd.commit()
     monkeypatch.chdir(wd.cwd / "foobar")
-    assert integration.find_files(".") == [opj(".", "test1.txt")]
+    assert setuptools_scm._file_finders.find_files(".") == [opj(".", "test1.txt")]
 
 
-def test_git_feature_branch_increments_major(wd):
+@pytest.mark.issue("https://github.com/pypa/setuptools_scm/issues/728")
+def test_git_branch_names_correct(wd: WorkDir) -> None:
+    wd.commit_testfile()
+    wd("git checkout -b test/fun")
+    wd_git = git.GitWorkdir(wd.cwd)
+    assert wd_git.get_branch() == "test/fun"
+
+
+def test_git_feature_branch_increments_major(wd: WorkDir) -> None:
     wd.commit_testfile()
     wd("git tag 1.0.0")
     wd.commit_testfile()
@@ -241,7 +415,7 @@ def test_git_feature_branch_increments_major(wd):
 
 
 @pytest.mark.issue("https://github.com/pypa/setuptools_scm/issues/303")
-def test_not_matching_tags(wd):
+def test_not_matching_tags(wd: WorkDir) -> None:
     wd.commit_testfile()
     wd("git tag apache-arrow-0.11.1")
     wd.commit_testfile()
@@ -254,20 +428,147 @@ def test_not_matching_tags(wd):
 
 
 @pytest.mark.issue("https://github.com/pypa/setuptools_scm/issues/411")
-@pytest.mark.xfail(reason="https://github.com/pypa/setuptools_scm/issues/449")
-def test_non_dotted_version(wd):
+def test_non_dotted_version(wd: WorkDir) -> None:
     wd.commit_testfile()
     wd("git tag apache-arrow-1")
     wd.commit_testfile()
     assert wd.get_version().startswith("2")
 
 
+def test_non_dotted_version_with_updated_regex(wd: WorkDir) -> None:
+    wd.commit_testfile()
+    wd("git tag apache-arrow-1")
+    wd.commit_testfile()
+    assert wd.get_version(tag_regex=r"^apache-arrow-([\.0-9]+)$").startswith("2")
+
+
+def test_non_dotted_tag_no_version_match(wd: WorkDir) -> None:
+    wd.commit_testfile()
+    wd("git tag apache-arrow-0.11.1")
+    wd.commit_testfile()
+    wd("git tag apache-arrow")
+    wd.commit_testfile()
+    assert wd.get_version().startswith("0.11.2.dev2")
+
+
 @pytest.mark.issue("https://github.com/pypa/setuptools_scm/issues/381")
-def test_gitdir(monkeypatch, wd):
-    """
-    """
+def test_gitdir(monkeypatch: pytest.MonkeyPatch, wd: WorkDir) -> None:
+    """ """
     wd.commit_testfile()
-    normal = wd.version
+    normal = wd.get_version()
     # git hooks set this and break subsequent setuptools_scm unless we clean
     monkeypatch.setenv("GIT_DIR", __file__)
-    assert wd.version == normal
+    assert wd.get_version() == normal
+
+
+def test_git_getdate(wd: WorkDir) -> None:
+    # TODO: case coverage for git wd parse
+    today = datetime.now(timezone.utc).date()
+
+    def parse_date() -> date:
+        parsed = git.parse(os.fspath(wd.cwd), Configuration())
+        assert parsed is not None
+        assert parsed.node_date is not None
+        return parsed.node_date
+
+    git_wd = git.GitWorkdir(wd.cwd)
+    assert git_wd.get_head_date() is None
+    assert parse_date() == today
+
+    wd.commit_testfile()
+    assert git_wd.get_head_date() == today
+    assert parse_date() == today
+
+
+def test_git_getdate_badgit(
+    wd: WorkDir, caplog: pytest.LogCaptureFixture, monkeypatch: pytest.MonkeyPatch
+) -> None:
+    wd.commit_testfile()
+    git_wd = git.GitWorkdir(wd.cwd)
+    fake_date_result = CompletedProcess(args=[], stdout="%cI", stderr="", returncode=0)
+    with patch.object(
+        git,
+        "run_git",
+        Mock(return_value=fake_date_result),
+    ):
+        assert git_wd.get_head_date() is None
+
+
+@pytest.fixture()
+def signed_commit_wd(monkeypatch: pytest.MonkeyPatch, wd: WorkDir) -> WorkDir:
+    if not has_command("gpg", args=["--version"], warn=False):
+        pytest.skip("gpg executable not found")
+
+    wd.write(
+        ".gpg_batch_params",
+        """\
+%no-protection
+%transient-key
+Key-Type: RSA
+Key-Length: 2048
+Name-Real: a test
+Name-Email: test@example.com
+Expire-Date: 0
+""",
+    )
+    monkeypatch.setenv("GNUPGHOME", str(wd.cwd.resolve(strict=True)))
+    wd("gpg --batch --generate-key .gpg_batch_params")
+
+    wd("git config log.showSignature true")
+    wd.signed_commit_command = "git commit -S -m test-{reason}"
+    return wd
+
+
+@pytest.mark.issue("https://github.com/pypa/setuptools_scm/issues/548")
+def test_git_getdate_signed_commit(signed_commit_wd: WorkDir) -> None:
+    today = datetime.now(timezone.utc).date()
+    signed_commit_wd.commit_testfile(signed=True)
+    git_wd = git.GitWorkdir(signed_commit_wd.cwd)
+    assert git_wd.get_head_date() == today
+
+
+@pytest.mark.parametrize(
+    ("expected", "from_data"),
+    [
+        (
+            "1.0",
+            {"describe-name": "1.0-0-g0000"},
+        ),
+        (
+            "1.1.dev3+g0000",
+            {
+                "describe-name": "1.0-3-g0000",
+                "node": "0" * 20,
+            },
+        ),
+        ("0.0", {"node": "0" * 20}),
+        ("1.2.2", {"describe-name": "release-1.2.2-0-g00000"}),
+        ("1.2.2.dev0", {"ref-names": "tag: release-1.2.2.dev"}),
+        ("1.2.2", {"describe-name": "v1.2.2"}),
+    ],
+)
+@pytest.mark.filterwarnings("ignore:git archive did not support describe output")
+def test_git_archival_to_version(expected: str, from_data: dict[str, str]) -> None:
+    config = Configuration(
+        version_scheme="guess-next-dev", local_scheme="node-and-date"
+    )
+    version = archival_to_version(from_data, config=config)
+    assert version is not None
+    assert format_version(version) == expected
+
+
+@pytest.mark.issue("https://github.com/pypa/setuptools_scm/issues/727")
+def test_git_archival_node_missing_no_version() -> None:
+    config = Configuration()
+    version = archival_to_version({}, config=config)
+    assert version is None
+
+
+def test_git_archival_from_unfiltered() -> None:
+    config = Configuration()
+
+    with pytest.warns(
+        UserWarning, match=r"unprocessed git archival found \(no export subst applied\)"
+    ):
+        version = archival_to_version({"node": "$Format:%H$"}, config=config)
+    assert version is None
diff --git a/testing/test_hg_git.py b/testing/test_hg_git.py
new file mode 100644 (file)
index 0000000..9527cb0
--- /dev/null
@@ -0,0 +1,83 @@
+from __future__ import annotations
+
+import pytest
+
+from setuptools_scm._run_cmd import has_command
+from setuptools_scm._run_cmd import run
+from testing.wd_wrapper import WorkDir
+
+
+@pytest.fixture(scope="module", autouse=True)
+def _check_hg_git() -> None:
+    if not has_command("hg", warn=False):
+        pytest.skip("hg executable not found")
+
+    res = run("hg debuginstall --template {pythonexe}", cwd=".")
+
+    if res.returncode:
+        skip_no_hggit = True
+    else:
+        res = run([res.stdout, "-c", "import hggit"], cwd=".")
+        skip_no_hggit = bool(res.returncode)
+    if skip_no_hggit:
+        pytest.skip("hg-git not installed")
+
+
+def test_base(repositories_hg_git: tuple[WorkDir, WorkDir]) -> None:
+    wd, wd_git = repositories_hg_git
+
+    assert wd_git.get_version() == "0.1.dev0+d20090213"
+    assert wd.get_version() == "0.1.dev0+d20090213"
+
+    wd_git.commit_testfile()
+    version_git = wd_git.get_version()
+
+    wd("hg pull -u")
+
+    version = wd.get_version()
+
+    assert version_git.startswith("0.1.dev1+g")
+    assert version.startswith("0.1.dev1+g")
+
+    assert not version_git.endswith("1-")
+    assert not version.endswith("1-")
+
+    wd_git("git tag v0.1")
+    wd("hg pull -u")
+    assert wd_git.get_version() == "0.1"
+    assert wd.get_version() == "0.1"
+
+    wd_git.write("test.txt", "test2")
+    wd.write("test.txt", "test2")
+    assert wd_git.get_version().startswith("0.2.dev0+g")
+    assert wd.get_version().startswith("0.2.dev0+g")
+
+    wd_git.commit_testfile()
+    wd("hg pull")
+    wd("hg up -C")
+    assert wd_git.get_version().startswith("0.2.dev1+g")
+    assert wd.get_version().startswith("0.2.dev1+g")
+
+    wd_git("git tag version-0.2")
+    wd("hg pull -u")
+    assert wd_git.get_version().startswith("0.2")
+    assert wd.get_version().startswith("0.2")
+
+    wd_git.commit_testfile()
+    wd_git("git tag version-0.2.post210+gbe48adfpost3+g0cc25f2")
+    wd("hg pull -u")
+    with pytest.warns(
+        UserWarning, match="tag '.*' will be stripped of its suffix '.*'"
+    ):
+        assert wd_git.get_version().startswith("0.2")
+
+    with pytest.warns(
+        UserWarning, match="tag '.*' will be stripped of its suffix '.*'"
+    ):
+        assert wd.get_version().startswith("0.2")
+
+    wd_git.commit_testfile()
+    wd_git("git tag 17.33.0-rc")
+    wd("hg pull -u")
+    assert wd_git.get_version() == "17.33.0rc0"
+    assert wd.get_version() == "17.33.0rc0"
index 68c3bfef7ea4ae2f16b4f908e3723c4439ccc376..45ee295dda0ffe457984d8ab2cc76e48e64aed7b 100644 (file)
@@ -1,17 +1,28 @@
+from __future__ import annotations
+
+import importlib.metadata
+import os
+import subprocess
 import sys
+import textwrap
+from pathlib import Path
 
 import pytest
 
-from setuptools_scm.utils import do
+import setuptools_scm._integration.setuptools
+from .wd_wrapper import WorkDir
+from setuptools_scm import Configuration
+from setuptools_scm._integration.setuptools import _warn_on_old_setuptools
+from setuptools_scm._overrides import PRETEND_KEY
+from setuptools_scm._overrides import PRETEND_KEY_NAMED
+from setuptools_scm._run_cmd import run
 
+c = Configuration()
 
-@pytest.fixture
-def wd(wd):
-    try:
-        wd("git init")
-    except OSError:
-        pytest.skip("git executable not found")
 
+@pytest.fixture()
+def wd(wd: WorkDir) -> WorkDir:
+    wd("git init")
     wd("git config user.email test@example.com")
     wd('git config user.name "a test"')
     wd.add_command = "git add ."
@@ -19,24 +30,223 @@ def wd(wd):
     return wd
 
 
-def test_pyproject_support(tmpdir, monkeypatch):
-    pytest.importorskip("toml")
+def test_pyproject_support(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
+    if sys.version_info <= (3, 10):
+        pytest.importorskip("tomli")
     monkeypatch.delenv("SETUPTOOLS_SCM_DEBUG")
-    pkg = tmpdir.ensure("package", dir=42)
-    pkg.join("pyproject.toml").write(
-        """[tool.setuptools_scm]
-fallback_version = "12.34"
-"""
+    pkg = tmp_path / "package"
+    pkg.mkdir()
+    pkg.joinpath("pyproject.toml").write_text(
+        textwrap.dedent(
+            """
+            [tool.setuptools_scm]
+            fallback_version = "12.34"
+            [project]
+            name = "foo"
+            description = "Factory ⸻ A code generator 🏭"
+            authors = [{name = "Łukasz Langa"}]
+            dynamic = ["version"]
+            """
+        ),
+        encoding="utf-8",
     )
-    pkg.join("setup.py").write("__import__('setuptools').setup()")
-    res = do((sys.executable, "setup.py", "--version"), pkg)
-    assert res == "12.34"
+    pkg.joinpath("setup.py").write_text("__import__('setuptools').setup()")
+    res = run([sys.executable, "setup.py", "--version"], pkg)
+    assert res.stdout == "12.34"
 
 
-def test_pyproject_support_with_git(tmpdir, monkeypatch, wd):
-    monkeypatch.delenv("SETUPTOOLS_SCM_DEBUG")
-    pkg = tmpdir.join("wd")
-    pkg.join("pyproject.toml").write("""[tool.setuptools_scm]""")
-    pkg.join("setup.py").write("__import__('setuptools').setup()")
-    res = do((sys.executable, "setup.py", "--version"), pkg)
-    assert res == "0.1.dev0"
+PYPROJECT_FILES = {
+    "setup.py": "[tool.setuptools_scm]",
+    "setup.cfg": "[tool.setuptools_scm]",
+    "pyproject tool.setuptools_scm": (
+        "[tool.setuptools_scm]\ndist_name='setuptools_scm_example'"
+    ),
+    "pyproject.project": (
+        "[project]\nname='setuptools_scm_example'\n"
+        "dynamic=['version']\n[tool.setuptools_scm]"
+    ),
+}
+
+SETUP_PY_PLAIN = "__import__('setuptools').setup()"
+SETUP_PY_WITH_NAME = "__import__('setuptools').setup(name='setuptools_scm_example')"
+
+SETUP_PY_FILES = {
+    "setup.py": SETUP_PY_WITH_NAME,
+    "setup.cfg": SETUP_PY_PLAIN,
+    "pyproject tool.setuptools_scm": SETUP_PY_PLAIN,
+    "pyproject.project": SETUP_PY_PLAIN,
+}
+
+SETUP_CFG_FILES = {
+    "setup.py": "",
+    "setup.cfg": "[metadata]\nname=setuptools_scm_example",
+    "pyproject tool.setuptools_scm": "",
+    "pyproject.project": "",
+}
+
+with_metadata_in = pytest.mark.parametrize(
+    "metadata_in",
+    ["setup.py", "setup.cfg", "pyproject tool.setuptools_scm", "pyproject.project"],
+)
+
+
+@with_metadata_in
+def test_pyproject_support_with_git(wd: WorkDir, metadata_in: str) -> None:
+    if sys.version_info <= (3, 10):
+        pytest.importorskip("tomli")
+    wd.write("pyproject.toml", PYPROJECT_FILES[metadata_in])
+    wd.write("setup.py", SETUP_PY_FILES[metadata_in])
+    wd.write("setup.cfg", SETUP_CFG_FILES[metadata_in])
+    res = wd([sys.executable, "setup.py", "--version"])
+    assert res.endswith("0.1.dev0+d20090213")
+
+
+@pytest.mark.parametrize("use_scm_version", ["True", "{}", "lambda: {}"])
+def test_pyproject_missing_setup_hook_works(wd: WorkDir, use_scm_version: str) -> None:
+    wd.write(
+        "setup.py",
+        f"""__import__('setuptools').setup(
+    name="example-scm-unique",
+    use_scm_version={use_scm_version},
+    )""",
+    )
+    wd.write(
+        "pyproject.toml",
+        textwrap.dedent(
+            """
+            [build-system]
+            requires=["setuptools", "setuptools_scm"]
+            build-backend = "setuptools.build_meta"
+            [tool]
+            """
+        ),
+    )
+
+    res = subprocess.run(
+        [sys.executable, "setup.py", "--version"],
+        cwd=wd.cwd,
+        check=True,
+        stdout=subprocess.PIPE,
+        encoding="utf-8",
+    )
+    stripped = res.stdout.strip()
+    assert stripped.endswith("0.1.dev0+d20090213")
+
+    res_build = subprocess.run(
+        [sys.executable, "-m", "build", "-nxw"],
+        env={k: v for k, v in os.environ.items() if k != "SETUPTOOLS_SCM_DEBUG"},
+        cwd=wd.cwd,
+    )
+    import pprint
+
+    pprint.pprint(res_build)
+    wheel: Path = next(wd.cwd.joinpath("dist").iterdir())
+    assert "0.1.dev0+d20090213" in str(wheel)
+
+
+def test_pretend_version(monkeypatch: pytest.MonkeyPatch, wd: WorkDir) -> None:
+    monkeypatch.setenv(PRETEND_KEY, "1.0.0")
+
+    assert wd.get_version() == "1.0.0"
+    assert wd.get_version(dist_name="ignored") == "1.0.0"
+
+
+@with_metadata_in
+def test_pretend_version_named_pyproject_integration(
+    monkeypatch: pytest.MonkeyPatch, wd: WorkDir, metadata_in: str
+) -> None:
+    test_pyproject_support_with_git(wd, metadata_in)
+    monkeypatch.setenv(
+        PRETEND_KEY_NAMED.format(name="setuptools_scm_example".upper()), "3.2.1"
+    )
+    res = wd([sys.executable, "setup.py", "--version"])
+    assert res.endswith("3.2.1")
+
+
+def test_pretend_version_named(monkeypatch: pytest.MonkeyPatch, wd: WorkDir) -> None:
+    monkeypatch.setenv(PRETEND_KEY_NAMED.format(name="test".upper()), "1.0.0")
+    monkeypatch.setenv(PRETEND_KEY_NAMED.format(name="test2".upper()), "2.0.0")
+    assert wd.get_version(dist_name="test") == "1.0.0"
+    assert wd.get_version(dist_name="test2") == "2.0.0"
+
+
+def test_pretend_version_name_takes_precedence(
+    monkeypatch: pytest.MonkeyPatch, wd: WorkDir
+) -> None:
+    monkeypatch.setenv(PRETEND_KEY_NAMED.format(name="test".upper()), "1.0.0")
+    monkeypatch.setenv(PRETEND_KEY, "2.0.0")
+    assert wd.get_version(dist_name="test") == "1.0.0"
+
+
+def test_pretend_version_accepts_bad_string(
+    monkeypatch: pytest.MonkeyPatch, wd: WorkDir
+) -> None:
+    monkeypatch.setenv(PRETEND_KEY, "dummy")
+    wd.write("setup.py", SETUP_PY_PLAIN)
+    assert wd.get_version(write_to="test.py") == "dummy"
+    pyver = wd([sys.executable, "setup.py", "--version"])
+    assert pyver == "0.0.0"
+
+
+def testwarn_on_broken_setuptools() -> None:
+    _warn_on_old_setuptools("61")
+    with pytest.warns(RuntimeWarning, match="ERROR: setuptools==60"):
+        _warn_on_old_setuptools("60")
+
+
+@pytest.mark.issue(611)
+def test_distribution_provides_extras() -> None:
+    from importlib.metadata import distribution
+
+    dist = distribution("setuptools_scm")
+    pe: list[str] = dist.metadata.get_all("Provides-Extra", [])
+    assert sorted(pe) == ["docs", "rich", "test", "toml"]
+
+
+@pytest.mark.issue(760)
+def test_unicode_in_setup_cfg(tmp_path: Path) -> None:
+    cfg = tmp_path / "setup.cfg"
+    cfg.write_text(
+        textwrap.dedent(
+            """
+            [metadata]
+            name = configparser
+            author = Łukasz Langa
+            """
+        ),
+        encoding="utf-8",
+    )
+    name = setuptools_scm._integration.setuptools.read_dist_name_from_setup_cfg(cfg)
+    assert name == "configparser"
+
+
+def test_setuptools_version_keyword_ensures_regex(
+    wd: WorkDir,
+    monkeypatch: pytest.MonkeyPatch,
+) -> None:
+    wd.commit_testfile("test")
+    wd("git tag 1.0")
+    monkeypatch.chdir(wd.cwd)
+    from setuptools_scm._integration.setuptools import version_keyword
+    import setuptools
+
+    dist = setuptools.Distribution({"name": "test"})
+    version_keyword(dist, "use_scm_version", {"tag_regex": "(1.0)"})
+
+
+@pytest.mark.parametrize(
+    "ep_name", ["setuptools_scm.parse_scm", "setuptools_scm.parse_scm_fallback"]
+)
+def test_git_archival_plugin_ignored(tmp_path: Path, ep_name: str) -> None:
+    tmp_path.joinpath(".git_archival.txt").write_text("broken")
+    try:
+        dist = importlib.metadata.distribution("setuptools_scm_git_archive")
+    except importlib.metadata.PackageNotFoundError:
+        pytest.skip("setuptools_scm_git_archive not installed")
+    else:
+        print(dist.metadata["Name"], dist.version)
+    from setuptools_scm.discover import iter_matching_entrypoints
+
+    found = list(iter_matching_entrypoints(tmp_path, config=c, entrypoint=ep_name))
+    imports = [item.value for item in found]
+    assert "setuptools_scm_git_archive:parse" not in imports
diff --git a/testing/test_internal_log_level.py b/testing/test_internal_log_level.py
new file mode 100644 (file)
index 0000000..68ce8e0
--- /dev/null
@@ -0,0 +1,15 @@
+from __future__ import annotations
+
+import logging
+
+from setuptools_scm import _log
+
+
+def test_log_levels_when_set() -> None:
+    assert _log._default_log_level({"SETUPTOOLS_SCM_DEBUG": ""}) == logging.DEBUG
+    assert _log._default_log_level({"SETUPTOOLS_SCM_DEBUG": "INFO"}) == logging.DEBUG
+    assert _log._default_log_level({"SETUPTOOLS_SCM_DEBUG": "3"}) == logging.DEBUG
+
+
+def test_log_levels_when_unset() -> None:
+    assert _log._default_log_level({}) == logging.WARNING
index 97ea05e008e85be37c06a44c2ef3474f247c43fe..cf760a6e29b3ab2800fe12a5716c518d9deaac72 100644 (file)
@@ -1,10 +1,67 @@
+from __future__ import annotations
+
 import os.path
+import sys
+import textwrap
+
+import pytest
 
+from .wd_wrapper import WorkDir
 
-def test_main():
+
+def test_main() -> None:
     mainfile = os.path.join(
         os.path.dirname(__file__), "..", "src", "setuptools_scm", "__main__.py"
     )
+    ns = {"__package__": "setuptools_scm"}
     with open(mainfile) as f:
         code = compile(f.read(), "__main__.py", "exec")
-        exec(code)
+        exec(code, ns)
+
+
+@pytest.fixture()
+def repo(wd: WorkDir) -> WorkDir:
+    wd("git init")
+    wd("git config user.email user@host")
+    wd("git config user.name user")
+    wd.add_command = "git add ."
+    wd.commit_command = "git commit -m test-{reason}"
+
+    wd.write("README.rst", "My example")
+    wd.add_and_commit()
+    wd("git tag v0.1.0")
+
+    wd.write("file.txt", "file.txt")
+    wd.add_and_commit()
+
+    return wd
+
+
+def test_repo_with_config(repo: WorkDir) -> None:
+    pyproject = """\
+    [tool.setuptools_scm]
+    version_scheme = "no-guess-dev"
+
+    [project]
+    name = "example"
+    """
+    repo.write("pyproject.toml", textwrap.dedent(pyproject))
+    repo.add_and_commit()
+    res = repo([sys.executable, "-m", "setuptools_scm"])
+    assert res.startswith("0.1.0.post1.dev2")
+
+
+def test_repo_without_config(repo: WorkDir) -> None:
+    res = repo([sys.executable, "-m", "setuptools_scm"])
+    assert res.startswith("0.1.1.dev1")
+
+
+def test_repo_with_pyproject_missing_setuptools_scm(repo: WorkDir) -> None:
+    pyproject = """\
+    [project]
+    name = "example"
+    """
+    repo.write("pyproject.toml", textwrap.dedent(pyproject))
+    repo.add_and_commit()
+    res = repo([sys.executable, "-m", "setuptools_scm"])
+    assert res.startswith("0.1.1.dev2")
index 29370c119d01dceea89ea1d023748585819a8f60..3aa00973ec01a6dfd26f65b9fd2d833fe61d36c2 100644 (file)
@@ -1,20 +1,27 @@
-from setuptools_scm import format_version
-from setuptools_scm.hg import archival_to_version, parse
-from setuptools_scm import integration
-from setuptools_scm.config import Configuration
-from setuptools_scm.utils import has_command
+from __future__ import annotations
+
+import os
+from pathlib import Path
+
 import pytest
-import warnings
 
+import setuptools_scm._file_finders
+from setuptools_scm import Configuration
+from setuptools_scm._run_cmd import CommandNotFoundError
+from setuptools_scm._run_cmd import has_command
+from setuptools_scm.hg import archival_to_version
+from setuptools_scm.hg import parse
+from setuptools_scm.version import format_version
+from testing.wd_wrapper import WorkDir
 
-with warnings.catch_warnings():
-    warnings.filterwarnings("ignore")
-    if not has_command("hg"):
-        pytestmark = pytest.mark.skip(reason="hg executable not found")
 
+pytestmark = pytest.mark.skipif(
+    not has_command("hg", warn=False), reason="hg executable not found"
+)
 
-@pytest.fixture
-def wd(wd):
+
+@pytest.fixture()
+def wd(wd: WorkDir) -> WorkDir:
     wd("hg init")
     wd.add_command = "hg add ."
     wd.commit_command = 'hg commit -m test-{reason} -u test -d "0 0"'
@@ -34,101 +41,112 @@ archival_mapping = {
 }
 
 
-@pytest.mark.parametrize("expected,data", sorted(archival_mapping.items()))
-def test_archival_to_version(expected, data):
-    config = Configuration()
-    version = archival_to_version(data, config=config)
-    assert (
-        format_version(
-            version, version_scheme="guess-next-dev", local_scheme="node-and-date"
-        )
-        == expected
+@pytest.mark.parametrize(("expected", "data"), sorted(archival_mapping.items()))
+def test_archival_to_version(expected: str, data: dict[str, str]) -> None:
+    config = Configuration(
+        version_scheme="guess-next-dev", local_scheme="node-and-date"
     )
+    version = archival_to_version(data, config=config)
+    assert format_version(version) == expected
 
 
-def test_find_files_stop_at_root_hg(wd, monkeypatch):
+def test_hg_gone(wd: WorkDir, monkeypatch: pytest.MonkeyPatch) -> None:
+    monkeypatch.setenv("PATH", str(wd.cwd / "not-existing"))
+    config = Configuration()
+    wd.write("pyproject.toml", "[tool.setuptools_scm]")
+    with pytest.raises(CommandNotFoundError, match=r"hg"):
+        parse(wd.cwd, config=config)
+
+    assert wd.get_version(fallback_version="1.0") == "1.0"
+
+
+def test_find_files_stop_at_root_hg(
+    wd: WorkDir, monkeypatch: pytest.MonkeyPatch
+) -> None:
     wd.commit_testfile()
     project = wd.cwd / "project"
     project.mkdir()
     project.joinpath("setup.cfg").touch()
     # setup.cfg has not been committed
-    assert integration.find_files(str(project)) == []
+    assert setuptools_scm._file_finders.find_files(str(project)) == []
     # issue 251
     wd.add_and_commit()
     monkeypatch.chdir(project)
-    assert integration.find_files() == ["setup.cfg"]
+    assert setuptools_scm._file_finders.find_files() == ["setup.cfg"]
 
 
 # XXX: better tests for tag prefixes
-def test_version_from_hg_id(wd):
-    assert wd.version == "0.0"
+def test_version_from_hg_id(wd: WorkDir) -> None:
+    assert wd.get_version() == "0.0"
 
     wd.commit_testfile()
-    assert wd.version.startswith("0.1.dev2+")
+    assert wd.get_version().startswith("0.1.dev1+")
 
     # tagging commit is considered the tag
     wd('hg tag v0.1 -u test -d "0 0"')
-    assert wd.version == "0.1"
+    assert wd.get_version() == "0.1"
 
     wd.commit_testfile()
-    assert wd.version.startswith("0.2.dev2")
+    assert wd.get_version().startswith("0.2.dev2")
 
     wd("hg up v0.1")
-    assert wd.version == "0.1"
+    assert wd.get_version() == "0.1"
 
-    # commit originating from the taged revision
-    # that is not a actual tag
+    # commit originating from the tagged revision
+    # that is not an actual tag
     wd.commit_testfile()
-    assert wd.version.startswith("0.2.dev1+")
+    assert wd.get_version().startswith("0.2.dev1+")
 
     # several tags
     wd("hg up")
     wd('hg tag v0.2 -u test -d "0 0"')
     wd('hg tag v0.3 -u test -d "0 0" -r v0.2')
-    assert wd.version == "0.3"
+    assert wd.get_version() == "0.3"
 
 
-def test_version_from_archival(wd):
+def test_version_from_archival(wd: WorkDir) -> None:
     # entrypoints are unordered,
-    # cleaning the wd ensure this test wont break randomly
+    # cleaning the wd ensure this test won't break randomly
     wd.cwd.joinpath(".hg").rename(wd.cwd / ".nothg")
     wd.write(".hg_archival.txt", "node: 000000000000\n" "tag: 0.1\n")
-    assert wd.version == "0.1"
+    assert wd.get_version() == "0.1"
 
     wd.write(
         ".hg_archival.txt",
         "node: 000000000000\n" "latesttag: 0.1\n" "latesttagdistance: 3\n",
     )
 
-    assert wd.version == "0.2.dev3+h000000000000"
+    assert wd.get_version() == "0.2.dev3+h000000000000"
 
 
 @pytest.mark.issue("#72")
-def test_version_in_merge(wd):
+def test_version_in_merge(wd: WorkDir) -> None:
     wd.commit_testfile()
     wd.commit_testfile()
     wd("hg up 0")
     wd.commit_testfile()
     wd("hg merge --tool :merge")
-    assert wd.version is not None
+    assert wd.get_version() is not None
 
 
 @pytest.mark.issue(128)
-def test_parse_no_worktree(tmpdir):
-    ret = parse(str(tmpdir))
+def test_parse_no_worktree(tmp_path: Path) -> None:
+    config = Configuration()
+    ret = parse(os.fspath(tmp_path), config)
     assert ret is None
 
 
-@pytest.fixture
-def version_1_0(wd):
+@pytest.fixture()
+def version_1_0(wd: WorkDir) -> WorkDir:
     wd("hg branch default")
     wd.commit_testfile()
     wd('hg tag 1.0.0 -u test -d "0 0"')
     return wd
 
 
-@pytest.fixture
-def pre_merge_commit_after_tag(wd, version_1_0):
+@pytest.fixture()
+def pre_merge_commit_after_tag(version_1_0: WorkDir) -> WorkDir:
+    wd = version_1_0
     wd("hg branch testbranch")
     wd.write("branchfile", "branchtext")
     wd(wd.add_command)
@@ -139,43 +157,41 @@ def pre_merge_commit_after_tag(wd, version_1_0):
 
 
 @pytest.mark.usefixtures("pre_merge_commit_after_tag")
-def test_version_bump_before_merge_commit(wd):
-    assert wd.version.startswith("1.0.1.dev1+")
+def test_version_bump_before_merge_commit(wd: WorkDir) -> None:
+    assert wd.get_version().startswith("1.0.1.dev1+")
 
 
 @pytest.mark.issue(219)
 @pytest.mark.usefixtures("pre_merge_commit_after_tag")
-def test_version_bump_from_merge_commit(wd):
+def test_version_bump_from_merge_commit(wd: WorkDir) -> None:
     wd.commit()
-    assert wd.version.startswith("1.0.1.dev3+")  # issue 219
+    assert wd.get_version().startswith("1.0.1.dev3+")  # issue 219
 
 
 @pytest.mark.usefixtures("version_1_0")
-def test_version_bump_from_commit_including_hgtag_mods(wd):
-    """ Test the case where a commit includes changes to .hgtags and other files
-    """
+def test_version_bump_from_commit_including_hgtag_mods(wd: WorkDir) -> None:
+    """Test the case where a commit includes changes to .hgtags and other files"""
     with wd.cwd.joinpath(".hgtags").open("ab") as tagfile:
         tagfile.write(b"0  0\n")
     wd.write("branchfile", "branchtext")
     wd(wd.add_command)
-    assert wd.version.startswith("1.0.1.dev1+")  # bump from dirty version
+    assert wd.get_version().startswith("1.0.1.dev1+")  # bump from dirty version
     wd.commit()  # commits both the testfile _and_ .hgtags
-    assert wd.version.startswith("1.0.1.dev2+")
+    assert wd.get_version().startswith("1.0.1.dev2+")
 
 
 @pytest.mark.issue(229)
 @pytest.mark.usefixtures("version_1_0")
-def test_latest_tag_detection(wd):
-    """ Tests that tags not containing a "." are ignored, the same as for git.
-    Note that will be superceded by the fix for pypa/setuptools_scm/issues/235
+def test_latest_tag_detection(wd: WorkDir) -> None:
+    """Tests that tags not containing a "." are ignored, the same as for git.
+    Note that will be superseded by the fix for pypa/setuptools_scm/issues/235
     """
     wd('hg tag some-random-tag -u test -d "0 0"')
-    assert wd.version == "1.0.0"
+    assert wd.get_version() == "1.0.0"
 
 
 @pytest.mark.usefixtures("version_1_0")
-def test_feature_branch_increments_major(wd):
-
+def test_feature_branch_increments_major(wd: WorkDir) -> None:
     wd.commit_testfile()
     assert wd.get_version(version_scheme="python-simplified-semver").startswith("1.0.1")
     wd("hg branch feature/fun")
index 8bde3730730327599da5989ee1db0bcdfe79798a..e2594e893e43efd445ef36bcb72e1fa401c93371 100644 (file)
@@ -1,70 +1,74 @@
-import sys
+from __future__ import annotations
+
+import pprint
 import subprocess
+import sys
+from dataclasses import replace
+from importlib.metadata import distribution
+from importlib.metadata import EntryPoint
+from pathlib import Path
+
+import pytest
 
-from setuptools_scm import get_version
+from setuptools_scm import Configuration
+from setuptools_scm._run_cmd import run
 from setuptools_scm.git import parse
-from setuptools_scm.utils import do_ex, do
+from setuptools_scm.integration import data_from_mime
+from setuptools_scm.version import meta
 
-import pytest
+
+def test_data_from_mime_ignores_body() -> None:
+    assert data_from_mime(
+        "test",
+        "version: 1.0\r\n\r\nversion: bad",
+    ) == {"version": "1.0"}
 
 
-def test_pkginfo_noscmroot(tmpdir, monkeypatch):
+def test_pkginfo_noscmroot(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
     """if we are indeed a sdist, the root does not apply"""
     monkeypatch.delenv("SETUPTOOLS_SCM_DEBUG")
 
     # we should get the version from pkg-info if git is broken
-    p = tmpdir.ensure("sub/package", dir=1)
-    tmpdir.mkdir(".git")
-    p.join("setup.py").write(
+    p = tmp_path.joinpath("sub/package")
+    p.mkdir(parents=True)
+
+    tmp_path.joinpath(".git").mkdir()
+    p.joinpath("setup.py").write_text(
         "from setuptools import setup;" 'setup(use_scm_version={"root": ".."})'
     )
 
-    _, stderr, ret = do_ex((sys.executable, "setup.py", "--version"), p)
-    assert "setuptools-scm was unable to detect version for" in stderr
-    assert ret == 1
+    res = run([sys.executable, "setup.py", "--version"], p)
+    assert "setuptools-scm was unable to detect version for" in res.stderr
+    assert res.returncode == 1
 
-    p.join("PKG-INFO").write("Version: 1.0")
-    res = do((sys.executable, "setup.py", "--version"), p)
-    assert res == "1.0"
+    p.joinpath("PKG-INFO").write_text("Version: 1.0")
+    res = run([sys.executable, "setup.py", "--version"], p)
+    assert res.stdout == "1.0"
 
     try:
-        do("git init", p.dirpath())
+        run("git init", p.parent)
     except OSError:
         pass
     else:
-        res = do((sys.executable, "setup.py", "--version"), p)
-        assert res == "0.1.dev0"
-
-
-def test_pip_egg_info(tmpdir, monkeypatch):
-    """if we are indeed a sdist, the root does not apply"""
-
-    # we should get the version from pkg-info if git is broken
-    p = tmpdir.ensure("sub/package", dir=1)
-    tmpdir.mkdir(".git")
-    p.join("setup.py").write(
-        "from setuptools import setup;" 'setup(use_scm_version={"root": ".."})'
-    )
-
-    with pytest.raises(LookupError):
-        get_version(root=p.strpath, fallback_root=p.strpath)
-
-    p.ensure("pip-egg-info/random.egg-info/PKG-INFO").write("Version: 1.0")
-    assert get_version(root=p.strpath, fallback_root=p.strpath) == "1.0"
+        res = run([sys.executable, "setup.py", "--version"], p)
+        assert res.stdout == "0.1.dev0+d20090213"
 
 
 @pytest.mark.issue(164)
-def test_pip_download(tmpdir, monkeypatch):
-    monkeypatch.chdir(tmpdir)
+def test_pip_download(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
+    monkeypatch.chdir(tmp_path)
     subprocess.check_call([sys.executable, "-m", "pip", "download", "lz4==0.9.0"])
 
 
-def test_use_scm_version_callable(tmpdir, monkeypatch):
+def test_use_scm_version_callable(
+    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
+) -> None:
     """use of callable as use_scm_version argument"""
     monkeypatch.delenv("SETUPTOOLS_SCM_DEBUG")
 
-    p = tmpdir.ensure("sub/package", dir=1)
-    p.join("setup.py").write(
+    p = tmp_path / "sub" / "package"
+    p.mkdir(parents=True)
+    p.joinpath("setup.py").write_text(
         """from setuptools import setup
 def vcfg():
     from setuptools_scm.version import guess_next_dev_version
@@ -74,17 +78,49 @@ def vcfg():
 setup(use_scm_version=vcfg)
 """
     )
-    p.join("PKG-INFO").write("Version: 1.0")
+    p.joinpath("PKG-INFO").write_text("Version: 1.0")
 
-    res = do((sys.executable, "setup.py", "--version"), p)
-    assert res == "1.0"
+    res = run([sys.executable, "setup.py", "--version"], p)
+    assert res.stdout == "1.0"
 
 
 @pytest.mark.skipif(sys.platform != "win32", reason="this bug is only valid on windows")
-def test_case_mismatch_on_windows_git(tmpdir):
+def test_case_mismatch_on_windows_git(tmp_path: Path) -> None:
     """Case insensitive path checks on Windows"""
-    p = tmpdir.ensure("CapitalizedDir", dir=1)
-
-    do("git init", p)
-    res = parse(str(p).lower())
+    camel_case_path = tmp_path / "CapitalizedDir"
+    camel_case_path.mkdir()
+    run("git init", camel_case_path)
+    res = parse(str(camel_case_path).lower(), Configuration())
     assert res is not None
+
+
+def test_entrypoints_load() -> None:
+    d = distribution("setuptools-scm")
+
+    eps = d.entry_points
+    failed: list[tuple[EntryPoint, Exception]] = []
+    for ep in eps:
+        try:
+            ep.load()
+        except Exception as e:
+            failed.append((ep, e))
+    if failed:
+        pytest.fail(pprint.pformat(failed))
+
+
+def test_write_to_absolute_path_passes_when_subdir_of_root(tmp_path: Path) -> None:
+    c = Configuration(root=tmp_path, write_to=tmp_path / "VERSION.py")
+    v = meta("1.0", config=c)
+    from setuptools_scm._get_version_impl import write_version_files
+
+    with pytest.warns(DeprecationWarning, match=".*write_to=.* is a absolute.*"):
+        write_version_files(c, "1.0", v)
+    write_version_files(replace(c, write_to="VERSION.py"), "1.0", v)
+    subdir = tmp_path / "subdir"
+    subdir.mkdir()
+    with pytest.raises(
+        # todo: python version specific error list
+        ValueError,
+        match=".*VERSION.py' .* .*subdir.*",
+    ):
+        write_version_files(replace(c, root=subdir), "1.0", v)
diff --git a/testing/test_setuptools_support.py b/testing/test_setuptools_support.py
deleted file mode 100644 (file)
index ce3444e..0000000
+++ /dev/null
@@ -1,83 +0,0 @@
-"""
-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")
index 0b487b30221590015a8fff1e6abd22aeb6596bc9..ea4c7d991d3f3b7497ce08249b063ba35b2b4cb2 100644 (file)
@@ -1,18 +1,31 @@
+from __future__ import annotations
+
+from dataclasses import replace
+from datetime import date
+from datetime import timedelta
+from typing import Any
+
 import pytest
-from setuptools_scm.config import Configuration
-from setuptools_scm.version import (
-    meta,
-    simplified_semver_version,
-    release_branch_semver_version,
-    tags_to_versions,
-)
+
+from setuptools_scm import Configuration
+from setuptools_scm import NonNormalizedVersion
+from setuptools_scm.version import calver_by_date
+from setuptools_scm.version import format_version
+from setuptools_scm.version import guess_next_date_ver
+from setuptools_scm.version import guess_next_version
+from setuptools_scm.version import meta
+from setuptools_scm.version import no_guess_dev_version
+from setuptools_scm.version import release_branch_semver_version
+from setuptools_scm.version import ScmVersion
+from setuptools_scm.version import simplified_semver_version
 
 
 c = Configuration()
+c_non_normalize = Configuration(version_cls=NonNormalizedVersion)
 
 
 @pytest.mark.parametrize(
-    "version, expected_next",
+    ("version", "expected_next"),
     [
         pytest.param(meta("1.0.0", config=c), "1.0.0", id="exact"),
         pytest.param(meta("1.0", config=c), "1.0.0", id="short_tag"),
@@ -43,13 +56,21 @@ c = Configuration()
         ),
     ],
 )
-def test_next_semver(version, expected_next):
+def test_next_semver(version: ScmVersion, expected_next: str) -> None:
     computed = simplified_semver_version(version)
     assert computed == expected_next
 
 
+def test_next_semver_bad_tag() -> None:
+    version = meta("1.0.0-foo", preformatted=True, config=c)
+    with pytest.raises(
+        ValueError, match=r"1\.0\.0-foo.* can't be parsed as numeric version"
+    ):
+        simplified_semver_version(version)
+
+
 @pytest.mark.parametrize(
-    "version, expected_next",
+    ("version", "expected_next"),
     [
         pytest.param(meta("1.0.0", config=c), "1.0.0", id="exact"),
         pytest.param(
@@ -67,6 +88,11 @@ def test_next_semver(version, expected_next):
             "1.0.1.dev2",
             id="release_branch_legacy_version",
         ),
+        pytest.param(
+            meta("1.0.0", distance=2, branch="v1.0.x", config=c),
+            "1.0.1.dev2",
+            id="release_branch_with_v_prefix",
+        ),
         pytest.param(
             meta("1.0.0", distance=2, branch="release-1.0", config=c),
             "1.0.1.dev2",
@@ -79,33 +105,308 @@ def test_next_semver(version, expected_next):
         ),
     ],
 )
-def test_next_release_branch_semver(version, expected_next):
+def test_next_release_branch_semver(version: ScmVersion, expected_next: str) -> None:
     computed = release_branch_semver_version(version)
     assert computed == expected_next
 
 
+def m(tag: str, **kw: Any) -> ScmVersion:
+    return meta(tag, **kw, config=c)
+
+
+@pytest.mark.parametrize(
+    ("version", "expected_next"),
+    [
+        pytest.param(
+            m("1.0.0", distance=2),
+            "1.0.0.post1.dev2",
+            id="dev_distance",
+        ),
+        pytest.param(
+            m("1.0.dev0", distance=2), "1.0.dev2", id="dev_distance_after_dev_tag"
+        ),
+        pytest.param(
+            m("1.0", distance=2),
+            "1.0.post1.dev2",
+            id="dev_distance_short_tag",
+        ),
+        pytest.param(
+            m("1.0.0"),
+            "1.0.0",
+            id="no_dev_distance",
+        ),
+    ],
+)
+def test_no_guess_version(version: ScmVersion, expected_next: str) -> None:
+    computed = no_guess_dev_version(version)
+    assert computed == expected_next
+
+
 @pytest.mark.parametrize(
-    "tag, expected",
+    ("version", "match"),
     [
-        pytest.param("v1.0.0", "1.0.0"),
-        pytest.param("v1.0.0-rc.1", "1.0.0rc1"),
-        pytest.param("v1.0.0-rc.1+-25259o4382757gjurh54", "1.0.0rc1"),
+        ("1.0.dev1", "choosing custom numbers for the `.devX` distance"),
+        ("1.0.post1", "already is a post release"),
     ],
 )
-def test_tag_regex1(tag, expected):
-    config = Configuration()
+def test_no_guess_version_bad(version: str, match: str) -> None:
+    with pytest.raises(ValueError, match=match):
+        no_guess_dev_version(m(version, distance=1))
+
+
+def test_bump_dev_version_zero() -> None:
+    assert guess_next_version(m("1.0.dev0")) == "1.0"
+
+
+def test_bump_dev_version_nonzero_raises() -> None:
+    match = (
+        "choosing custom numbers for the `.devX` distance "
+        "is not supported.\n "
+        "The 1.0.dev1 can't be bumped\n"
+        "Please drop the tag or create a new supported one ending in .dev0"
+    )
+
+    with pytest.raises(ValueError, match=match):
+        guess_next_version(m("1.0.dev1"))
+
+
+@pytest.mark.parametrize(
+    ("tag", "expected"),
+    [
+        ("v1.0.0", "1.0.0"),
+        ("v1.0.0-rc.1", "1.0.0rc1"),
+        ("v1.0.0-rc.1+-25259o4382757gjurh54", "1.0.0rc1"),
+    ],
+)
+def test_tag_regex1(tag: str, expected: str) -> None:
     if "+" in tag:
         # pytest bug wrt cardinality
         with pytest.warns(UserWarning):
-            result = meta(tag, config=config)
+            result = meta(tag, config=c)
     else:
-        result = meta(tag, config=config)
-
+        result = meta(tag, config=c)
+    assert not isinstance(result.tag, str)
     assert result.tag.public == expected
 
 
-@pytest.mark.issue("https://github.com/pypa/setuptools_scm/issues/286")
-def test_tags_to_versions():
-    config = Configuration()
-    versions = tags_to_versions(["1.0", "2.0", "3.0"], config=config)
-    assert isinstance(versions, list)  # enable subscription
+@pytest.mark.issue("https://github.com/pypa/setuptools_scm/issues/471")
+def test_version_bump_bad() -> None:
+    class YikesVersion:
+        val: str
+
+        def __init__(self, val: str) -> None:
+            self.val = val
+
+        def __str__(self) -> str:
+            return self.val
+
+    config = Configuration(version_cls=YikesVersion)  # type: ignore[arg-type]
+    with pytest.raises(
+        ValueError,
+        match=".*does not end with a number to bump, "
+        "please correct or use a custom version scheme",
+    ):
+        guess_next_version(tag_version=meta("2.0.0-alpha.5-PMC", config=config))
+
+
+def test_format_version_schemes() -> None:
+    version = meta(
+        "1.0",
+        config=replace(
+            c,
+            local_scheme="no-local-version",
+            version_scheme=[  # type: ignore[arg-type]
+                lambda v: None,
+                "guess-next-dev",
+            ],
+        ),
+    )
+    assert format_version(version) == "1.0"
+
+
+def test_custom_version_schemes() -> None:
+    version = meta(
+        "1.0",
+        config=replace(
+            c,
+            local_scheme="no-local-version",
+            version_scheme="setuptools_scm.version:no_guess_dev_version",
+        ),
+    )
+    custom_computed = format_version(version)
+    assert custom_computed == no_guess_dev_version(version)
+
+
+def date_offset(base_date: date | None = None, days_offset: int = 0) -> date:
+    if base_date is None:
+        from setuptools_scm.version import _source_epoch_or_utc_now
+
+        base_date = _source_epoch_or_utc_now().date()
+    return base_date - timedelta(days=days_offset)
+
+
+def date_to_str(
+    base_date: date | None = None,
+    days_offset: int = 0,
+    fmt: str = "%y.%m.%d",
+) -> str:
+    return format(date_offset(base_date, days_offset), fmt)
+
+
+@pytest.mark.parametrize(
+    ("version", "expected_next"),
+    [
+        pytest.param(
+            meta(date_to_str(days_offset=3), config=c_non_normalize),
+            date_to_str(days_offset=3),
+            id="exact",
+        ),
+        pytest.param(
+            meta(date_to_str() + ".1", config=c_non_normalize),
+            date_to_str() + ".1",
+            id="exact patch",
+        ),
+        pytest.param(
+            meta("20.01.02", config=c),
+            "20.1.2",
+            id="leading 0s",
+        ),
+        pytest.param(
+            meta(date_to_str(days_offset=3), config=c_non_normalize, dirty=True),
+            date_to_str() + ".0.dev0",
+            id="dirty other day",
+        ),
+        pytest.param(
+            meta(date_to_str(), config=c_non_normalize, distance=2, branch="default"),
+            date_to_str() + ".1.dev2",
+            id="normal branch",
+        ),
+        pytest.param(
+            meta(date_to_str(fmt="%Y.%m.%d"), config=c_non_normalize),
+            date_to_str(fmt="%Y.%m.%d"),
+            id="4 digits year",
+        ),
+        pytest.param(
+            meta(
+                date_to_str(),
+                config=c_non_normalize,
+                distance=2,
+                branch="release-2021.05.06",
+            ),
+            "2021.05.06",
+            id="release branch",
+        ),
+        pytest.param(
+            meta(
+                date_to_str() + ".2",
+                config=c_non_normalize,
+                distance=2,
+                branch="release-21.5.1",
+            ),
+            "21.5.1",
+            id="release branch short",
+        ),
+        pytest.param(
+            meta(
+                date_to_str(days_offset=3) + ".2",
+                config=c_non_normalize,
+                node_date=date_offset(days_offset=2),
+            ),
+            date_to_str(days_offset=3) + ".2",
+            id="node date clean",
+        ),
+        pytest.param(
+            meta(
+                date_to_str(days_offset=2) + ".2",
+                config=c_non_normalize,
+                distance=2,
+                node_date=date_offset(days_offset=2),
+            ),
+            date_to_str(days_offset=2) + ".3.dev2",
+            id="node date distance",
+        ),
+        pytest.param(
+            meta(
+                "1.2.0",
+                config=c_non_normalize,
+                distance=2,
+                node_date=date_offset(days_offset=2),
+            ),
+            date_to_str(days_offset=2) + ".0.dev2",
+            marks=pytest.mark.filterwarnings(
+                "ignore:.*not correspond to a valid versioning date.*:UserWarning"
+            ),
+            id="using on old version tag",
+        ),
+    ],
+)
+def test_calver_by_date(version: ScmVersion, expected_next: str) -> None:
+    computed = calver_by_date(version)
+    assert computed == expected_next
+
+
+@pytest.mark.parametrize(
+    ("version", "expected_next"),
+    [
+        pytest.param(meta("1.0.0", config=c), "1.0.0", id="SemVer exact stays"),
+        pytest.param(
+            meta("1.0.0", config=c_non_normalize, dirty=True),
+            "09.02.13.1.dev0",
+            id="SemVer dirty is replaced by date",
+            marks=pytest.mark.filterwarnings("ignore:.*legacy version.*:UserWarning"),
+        ),
+    ],
+)
+def test_calver_by_date_semver(version: ScmVersion, expected_next: str) -> None:
+    computed = calver_by_date(version)
+    assert computed == expected_next
+
+
+def test_calver_by_date_future_warning() -> None:
+    with pytest.warns(UserWarning, match="your previous tag*"):
+        calver_by_date(
+            meta(date_to_str(days_offset=-2), config=c_non_normalize, distance=2)
+        )
+
+
+@pytest.mark.parametrize(
+    ("tag", "node_date", "expected"),
+    [
+        pytest.param("20.03.03", date(2020, 3, 4), "20.03.04.0", id="next day"),
+        pytest.param("20.03.03", date(2020, 3, 3), "20.03.03.1", id="same day"),
+        pytest.param(
+            "20.03.03.2", date(2020, 3, 3), "20.03.03.3", id="same day with patch"
+        ),
+        pytest.param(
+            "v20.03.03", date(2020, 3, 4), "v20.03.04.0", id="next day with v prefix"
+        ),
+    ],
+)
+def test_calver_guess_next_data(tag: str, node_date: date, expected: str) -> None:
+    version = meta(tag, config=c_non_normalize, node_date=node_date)
+    next = guess_next_date_ver(
+        version,
+        node_date=node_date,
+        version_cls=c_non_normalize.version_cls,
+    )
+    assert next == expected
+
+
+def test_custom_version_cls() -> None:
+    """Test that we can pass our own version class instead of pkg_resources"""
+
+    class MyVersion:
+        def __init__(self, tag_str: str) -> None:
+            self.tag = tag_str
+
+        def __str__(self) -> str:
+            return "Custom %s" % self.tag
+
+        def __repr__(self) -> str:
+            return "MyVersion<Custom%s>" % self.tag
+
+    config = Configuration(version_cls=MyVersion)  # type: ignore[arg-type]
+    scm_version = meta("1.0.0-foo", config=config)
+
+    assert isinstance(scm_version.tag, MyVersion)
+    assert str(scm_version.tag) == "Custom 1.0.0-foo"
diff --git a/testing/wd_wrapper.py b/testing/wd_wrapper.py
new file mode 100644 (file)
index 0000000..dd504ee
--- /dev/null
@@ -0,0 +1,69 @@
+from __future__ import annotations
+
+import itertools
+from pathlib import Path
+from typing import Any
+
+
+class WorkDir:
+    """a simple model for a"""
+
+    commit_command: str
+    signed_commit_command: str
+    add_command: str
+
+    def __repr__(self) -> str:
+        return f"<WD {self.cwd}>"
+
+    def __init__(self, cwd: Path) -> None:
+        self.cwd = cwd
+        self.__counter = itertools.count()
+
+    def __call__(self, cmd: list[str] | str, **kw: object) -> str:
+        if kw:
+            assert isinstance(cmd, str), "formatting the command requires text input"
+            cmd = cmd.format(**kw)
+        from setuptools_scm._run_cmd import run
+
+        return run(cmd, cwd=self.cwd).stdout
+
+    def write(self, name: str, content: str | bytes) -> Path:
+        path = self.cwd / name
+        if isinstance(content, bytes):
+            path.write_bytes(content)
+        else:
+            path.write_text(content)
+        return path
+
+    def _reason(self, given_reason: str | None) -> str:
+        if given_reason is None:
+            return f"number-{next(self.__counter)}"
+        else:
+            return given_reason
+
+    def add_and_commit(
+        self, reason: str | None = None, signed: bool = False, **kwargs: object
+    ) -> None:
+        self(self.add_command)
+        self.commit(reason=reason, signed=signed, **kwargs)
+
+    def commit(self, reason: str | None = None, signed: bool = False) -> None:
+        reason = self._reason(reason)
+        self(
+            self.commit_command if not signed else self.signed_commit_command,
+            reason=reason,
+        )
+
+    def commit_testfile(self, reason: str | None = None, signed: bool = False) -> None:
+        reason = self._reason(reason)
+        self.write("test.txt", f"test {reason}")
+        self(self.add_command)
+        self.commit(reason=reason, signed=signed)
+
+    def get_version(self, **kw: Any) -> str:
+        __tracebackhide__ = True
+        from setuptools_scm import get_version
+
+        version = get_version(root=self.cwd, fallback_root=self.cwd, **kw)
+        print(self.cwd.name, version, sep=": ")
+        return version
diff --git a/tox.ini b/tox.ini
index 447c67e1594a18e791460ff6d865794f26d8370a..e2da4b97d99a879979d7a705747f0bca817f0a96 100644 (file)
--- a/tox.ini
+++ b/tox.ini
@@ -1,83 +1,46 @@
 [tox]
-envlist=py{27,34,35,36,37,38}-test,flake8,check_readme,py{27,37}-selfcheck
-
-[pytest]
-testpaths=testing
-filterwarnings=error
-markers=
-    issue(id): reference to github issue
+envlist=py{38,39,310,311},check_readme,check-dist
+requires= tox>4
 
 [flake8]
 max-complexity = 10
 max-line-length = 88
 ignore=E203,W503
-exclude=
-       .git,
-       .tox,
-       .env,
-       .venv,
-       .pytest_cache,
-       __pycache__,
-       ./setuptools_scm/win_py31_compat.py
 
 [testenv]
 usedevelop=True
-skip_install=
-    selfcheck: True
-    test: False
 deps=
     pytest
-    setuptools >= 42
+    setuptools >= 45
+    rich
+    build
 commands=
-    test: pytest []
-    selfcheck: python setup.py --version
-extras =
-    toml
+    pytest {posargs}
+
 
-[testenv:flake8]
-skip_install=True
-deps=
-    flake8
-    mccabe
-commands =
-    flake8 src/setuptools_scm/ testing/ setup.py --exclude=setuptools_scm/win_py31_compat.py
 
 [testenv:check_readme]
 skip_install=True
-setenv = SETUPTOOLS_SCM_PRETEND_VERSION=2.0
 deps=
-    readme
     check-manifest
+    docutils
+    pygments
+    typing_extensions
+    hatchling
+    rich
 commands=
-    python setup.py check -r
-    rst2html.py README.rst {envlogdir}/README.html --strict []
-    check-manifest
+    check-manifest --no-build-isolation
 
-[testenv:upload]
+[testenv:check_dist]
+skip_install = true
 deps=
-    wheel
+    build
     twine
 commands=
-    python setup.py clean --all rotate -k - -m .whl,.tar.gz,.zip
-    python setup.py -q egg_info
-    python setup.py -q sdist --formats zip bdist_wheel register
-
+    python -m build
+    twine check dist/*
 
 
-[testenv:dist]
-deps= wheel
-whitelist_externals = rm
-commands=
-    python setup.py -q clean --all
-    python setup.py -q rotate -k 0 -m .egg,.zip,.whl,.tar.gz
-    python setup.py -q egg_info
-    python setup.py -q sdist --formats zip,bztar bdist_wheel upload
 
-[testenv:devpi]
-deps=
-    devpi-client
-commands =
-    python setup.py -q egg_info
-    devpi upload --from-dir dist
 
 #XXX: envs for hg versions