diff --git a/.git_archival.txt b/.git_archival.txt
index 8fb235d7..7c510094 100644
--- a/.git_archival.txt
+++ b/.git_archival.txt
@@ -1,4 +1,3 @@
node: $Format:%H$
node-date: $Format:%cI$
describe-name: $Format:%(describe:tags=true,match=*[0-9]*)$
-ref-names: $Format:%D$
diff --git a/.github/release.yml b/.github/release.yml
new file mode 100644
index 00000000..9d1e0987
--- /dev/null
+++ b/.github/release.yml
@@ -0,0 +1,5 @@
+changelog:
+ exclude:
+ authors:
+ - dependabot
+ - pre-commit-ci
diff --git a/.github/workflows/python-tests.yml b/.github/workflows/python-tests.yml
index 240e4d25..c8818651 100644
--- a/.github/workflows/python-tests.yml
+++ b/.github/workflows/python-tests.yml
@@ -43,6 +43,8 @@ jobs:
name: ${{ matrix.os }} - Python ${{ matrix.python_version }}
steps:
- uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
- name: Setup python
uses: actions/setup-python@v5
if: matrix.python_version != 'msys2'
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 7bd9dada..d28a324c 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,45 +1,25 @@
-default_language_version:
- python: python3.9
repos:
-- repo: https://github.com/psf/black
- rev: 23.12.1
- hooks:
- - id: black
- args: [--safe, --quiet]
- exclude: docs/examples/
-- repo: https://github.com/asottile/reorder-python-imports
- rev: v3.12.0
- hooks:
- - id: reorder-python-imports
- args: [ "--application-directories=.:src" , --py38-plus, --add-import, 'from __future__ import annotations']
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.5.0
hooks:
- id: trailing-whitespace
- id: check-yaml
- id: debug-statements
-- repo: https://github.com/PyCQA/flake8
- rev: 7.0.0
- hooks:
- - id: flake8
-- repo: https://github.com/asottile/pyupgrade
- rev: v3.15.0
- hooks:
- - id: pyupgrade
- args: [--py38-plus]
- repo: https://github.com/astral-sh/ruff-pre-commit
- rev: v0.1.11
+ rev: v0.3.4
hooks:
- id: ruff
- args: [--fix, --exit-non-zero-on-fix]
+ args: [--fix, --exit-non-zero-on-fix, --show-fixes]
+ - id: ruff-format
+
- repo: https://github.com/tox-dev/pyproject-fmt
- rev: "1.5.3"
+ rev: 1.7.0
hooks:
- id: pyproject-fmt
exclude: docs/examples/
- repo: https://github.com/pre-commit/mirrors-mypy
- rev: 'v1.8.0'
+ rev: v1.9.0
hooks:
- id: mypy
args: [--strict]
@@ -51,3 +31,8 @@ repos:
- importlib_metadata
- typing-extensions>=4.5
- rich
+
+- repo: https://github.com/scientific-python/cookie
+ rev: 2024.04.23
+ hooks:
+ - id: sp-repo-review
diff --git a/CHANGELOG.md b/CHANGELOG.md
index fcf3123d..01d6cb4a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,8 @@
+# Unreleased
+
+## Changed
+
+- inclusion of `__all__` in autogenerated `version.py` files to aid IDE autoimports
# v8.0.4
@@ -14,7 +19,7 @@
- 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
+- fix #930: temporary restore `DEFAULT_VERSION_SCHEME` and `DEFAULT_LOCAL_SCHEME` on the `setuptools-scm` package
@@ -60,6 +65,7 @@
- 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`
+- renameed the project from `setuptools_scm` to `setuptools-scm`
## features
diff --git a/README.md b/README.md
index 1a5e815f..e1f06f82 100644
--- a/README.md
+++ b/README.md
@@ -1,19 +1,21 @@
-# setuptools_scm
-[](https://github.com/pypa/setuptools_scm/actions)
+# setuptools-scm
+[](https://github.com/pypa/setuptools-scm/actions/workflows/python-tests.yml)
[](https://setuptools-scm.readthedocs.io/en/latest/?badge=latest)
[ ](https://tidelift.com/subscription/pkg/pypi-setuptools-scm?utm_source=pypi-setuptools-scm&utm_medium=readme)
## about
-[setuptools-scm] extracts Python package versions from `git` or
-`hg` metadata instead of declaring them as the version argument
-or in an SCM managed file.
-
-Additionally, [setuptools-scm] provides setuptools
-with a list of files that are managed by the SCM
-(i.e. it automatically adds **all of** the SCM-managed files to the sdist).
-Unwanted files must be excluded via `MANIFEST.in`.
+[setuptools-scm] extracts Python package versions from `git` or `hg` metadata
+instead of declaring them as the version argument
+or in a Source Code Managed (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`
+or [configuring Git archive][git-archive-docs].
## `pyproject.toml` usage
@@ -26,7 +28,7 @@ build step by specifying it as one of the build requirements.
```toml title="pyproject.toml"
[build-system]
-requires = ["setuptools>=64", "setuptools_scm>=8"]
+requires = ["setuptools>=64", "setuptools-scm>=8"]
build-backend = "setuptools.build_meta"
```
@@ -69,8 +71,9 @@ $ python -m setuptools_scm --help
For further configuration see the [documentation].
-[setuptools-scm]: https://github.com/pypa/setuptools_scm
+[setuptools-scm]: https://github.com/pypa/setuptools-scm
[documentation]: https://setuptools-scm.readthedocs.io/
+[git-archive-docs]: https://setuptools-scm.readthedocs.io/en/stable/usage/#builtin-mechanisms-for-obtaining-version-numbers
## Interaction with Enterprise Distributions
@@ -78,7 +81,7 @@ For further configuration see the [documentation].
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`.
+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.
diff --git a/_own_version_helper.py b/_own_version_helper.py
index 9ef23f5c..da7484fe 100644
--- a/_own_version_helper.py
+++ b/_own_version_helper.py
@@ -5,22 +5,24 @@
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 import build_meta as build_meta
-from setuptools_scm import _types as _t
from setuptools_scm import Configuration
+from setuptools_scm import _types as _t
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 ScmVersion
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
diff --git a/changelog.d/20240105_133254_subprocess_timeout_var.md b/changelog.d/20240105_133254_subprocess_timeout_var.md
new file mode 100644
index 00000000..78ecab27
--- /dev/null
+++ b/changelog.d/20240105_133254_subprocess_timeout_var.md
@@ -0,0 +1,4 @@
+
+### Changed
+
+- fix #957 - add subprocess timeout control env var
diff --git a/changelog.d/20240108_134756_cli_version_file_force.md b/changelog.d/20240108_134756_cli_version_file_force.md
new file mode 100644
index 00000000..c313c178
--- /dev/null
+++ b/changelog.d/20240108_134756_cli_version_file_force.md
@@ -0,0 +1,30 @@
+
+### Added
+
+- fix #960: add a ``--force-write-version-files`` flag for the cli
+
+-->
+
+
+
+
diff --git a/changelog.d/20240305_102047_allow_non_normalized_semver.md b/changelog.d/20240305_102047_allow_non_normalized_semver.md
new file mode 100644
index 00000000..f5d85673
--- /dev/null
+++ b/changelog.d/20240305_102047_allow_non_normalized_semver.md
@@ -0,0 +1,4 @@
+
+### Fixed
+
+- fix #1018: allow non-normalized versions for semver
diff --git a/docs/config.md b/docs/config.md
index 429d6a91..b30fce86 100644
--- a/docs/config.md
+++ b/docs/config.md
@@ -11,17 +11,19 @@ Callables or other Python objects have to be passed in `setup.py` (via the `use_
: 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.
+: Configures how the version number is constructed; either an entrypoint name or a callable.
+ See [Version number construction](extending.md#setuptools_scmversion_scheme) for predefined implementations.
`local_scheme : str | Callable[[ScmVersion], str]`
-: Configures how the local component of the version is constructed
+: Configures how the local component of the version (the optional part after the `+`) is constructed;
either an entrypoint name or a callable.
+ See [Version number construction](extending.md#setuptools_scmlocal_scheme) for predefined implementations.
`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`
+ package, typically used to avoid using `importlib.metadata`
(which adds some overhead).
!!! warning ""
@@ -30,8 +32,11 @@ Callables or other Python objects have to be passed in `setup.py` (via the `use_
for other file types it is necessary to provide `version_file_template`.
`version_file_template: str | None = None`
-: A new-style format string that is given the current version as
- the `version` keyword argument for formatting.
+: A new-style format string taking `version`, `scm_version` and `version_tuple` as parameters.
+ `version` is the generated next_version as string,
+ `version_tuple` is a tuple of split numbers/strings and
+ `scm_version` is the `ScmVersion` instance the current `version` was rendered from
+
`write_to: Pathlike[str] | Path | None = None`
: (deprecated) legacy option to create a version file relative to the scm root
@@ -66,14 +71,14 @@ Callables or other Python objects have to be passed in `setup.py` (via the `use_
`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
+ 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.
+ familiar with the `setuptools-scm` internals to use it.
`git_describe_command`
: This command will be used instead the default `git describe --long` command.
@@ -93,7 +98,7 @@ Callables or other Python objects have to be passed in `setup.py` (via the `use_
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`
+ `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).
@@ -118,10 +123,10 @@ Callables or other Python objects have to be passed in `setup.py` (via the `use_
in which case it will be an unparsed string.
Specifying distribution-specific pretend versions will
avoid possible collisions with third party distributions
- also using ``setuptools_scm``
+ also using ``setuptools-scm``
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
+ more of ".-\_" being replaced by a single "\_", and the name being upper-cased
this will take precedence over ``SETUPTOOLS_SCM_PRETEND_VERSION``
diff --git a/docs/customizing.md b/docs/customizing.md
index b4bc7a15..616e12e9 100644
--- a/docs/customizing.md
+++ b/docs/customizing.md
@@ -3,10 +3,10 @@
## 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.
+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.
+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).
@@ -33,7 +33,7 @@ setup(use_scm_version={"version_scheme": myversion_func})
``` { .toml title="pyproject.toml" file="docs/examples/version_scheme_code/pyproject.toml" }
[build-system]
-requires = ["setuptools>=64", "setuptools_scm>=8"]
+requires = ["setuptools>=64", "setuptools-scm>=8"]
build-backend = "setuptools.build_meta"
[project]
@@ -52,7 +52,7 @@ dynamic = [
## Importing in setup.py
-With the pep 517/518 build backend, setuptools_scm is importable from `setup.py`
+With the pep 517/518 build backend, setuptools-scm is importable from `setup.py`
``` { .python title="setup.py" }
import setuptools
diff --git a/docs/examples/version_scheme_code/pyproject.toml b/docs/examples/version_scheme_code/pyproject.toml
index 10ef31d4..389aad09 100644
--- a/docs/examples/version_scheme_code/pyproject.toml
+++ b/docs/examples/version_scheme_code/pyproject.toml
@@ -1,6 +1,6 @@
# ~/~ begin <>[init]
[build-system]
-requires = ["setuptools>=64", "setuptools_scm>=8"]
+requires = ["setuptools>=64", "setuptools-scm>=8"]
build-backend = "setuptools.build_meta"
[project]
@@ -10,4 +10,4 @@ dynamic = [
]
[tool.setuptools_scm]
-# ~/~ end
\ No newline at end of file
+# ~/~ end
diff --git a/docs/extending.md b/docs/extending.md
index 957c762e..66f1ffd4 100644
--- a/docs/extending.md
+++ b/docs/extending.md
@@ -1,12 +1,12 @@
-# Extending setuptools_scm
+# Extending setuptools-scm
-`setuptools_scm` uses [entry-point][entry-point] based hooks to extend its default capabilities.
+`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` provides two entrypoints for adding new SCMs:
`setuptools_scm.parse_scm`
: A function used to parse the metadata of the current workdir
@@ -84,6 +84,11 @@ representing the version.
`no-guess-dev`
: Does no next version guessing, just adds `.post1.devN`
+`only-version`
+: Only use the version from the tag, as given.
+
+ !!! warning "This means version is no longer pseudo unique per commit"
+
### `setuptools_scm.local_scheme`
Configures how the local part of a version is rendered given a
diff --git a/docs/index.md b/docs/index.md
index eff1bc94..b40dbf42 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -1,25 +1,28 @@
# About
+`setuptools-scm` extracts Python package versions from `git` or `hg` metadata
+instead of declaring them as the version argument
+or in a Source Code Managed (SCM) managed file.
-`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`
+or [configuring Git archive][git-archive-docs].
-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`.
+[git-archive-docs]: usage.md#builtin-mechanisms-for-obtaining-version-numbers
-## basic usage
+## Basic usage
-### with setuptools
+### With setuptools
-Note: `setuptools_scm>=8` intentionally doesn't depend on setuptools to ease non-setuptools usage.
+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 title="pyproject.toml"
[build-system]
-requires = ["setuptools>=64", "setuptools_scm>=8"]
+requires = ["setuptools>=64", "setuptools-scm>=8"]
build-backend = "setuptools.build_meta"
[project]
@@ -34,8 +37,8 @@ dynamic = ["version"]
```
-### with hatch
+### With hatch
-[Hatch-vcs](https://github.com/ofek/hatch-vcs) integrates with setuptools_scm
+[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
index 5114a843..5a6093bb 100644
--- a/docs/overrides.md
+++ b/docs/overrides.md
@@ -2,7 +2,7 @@
## pretend versions
-setuptools_scm provides a mechanism to override the version number build time.
+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.
@@ -12,5 +12,12 @@ 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}`
+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`.
+
+## subprocess timeouts
+
+The environment variable `SETUPTOOLS_SCM_SUBPROCESS_TIMEOUT` allows to override the subprocess timeout.
+The default is 40 seconds and should work for most needs. However, users with git lfs + windows reported
+situations where this was not enough.
+
diff --git a/docs/usage.md b/docs/usage.md
index a96ff8cf..006b8b47 100644
--- a/docs/usage.md
+++ b/docs/usage.md
@@ -1,15 +1,15 @@
# Usage
-## at build time
+## At build time
-The preferred way to configure `setuptools_scm` is to author
+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 title="pyproject.toml"
[build-system]
-requires = ["setuptools>=64", "setuptools_scm>=8"]
+requires = ["setuptools>=64", "setuptools-scm>=8"]
build-backend = "setuptools.build_meta"
[project]
@@ -17,15 +17,17 @@ build-backend = "setuptools.build_meta"
dynamic = ["version"]
[tool.setuptools_scm]
-# can be empty if no extra settings are needed, presence enables 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 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
+### Version files
+
+Version files can be created with the ``version_file`` directive.
```toml title="pyproject.toml"
...
@@ -34,19 +36,17 @@ version_file = "pkg/_version.py"
```
Where ``pkg`` is the name of your package.
+Unless the small overhead of introspecting the version at runtime via
+`importlib.metadata` is a concern or you need a version file in an
+alternative format such as plain-text (see ``version_file_template``)
+you most likely do _not_ need to write a separate version file; see
+the runtime discussion below for more details.
-```commandline
-$ python -m setuptools_scm
-
-# To explore other options, try:
-$ python -m setuptools_scm --help
-```
-
-## as cli tool
+## 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)
+[setuptools-scm](https://github.com/pypa/setuptools-scm)
directly in your working environment and run:
```commandline
@@ -65,46 +65,31 @@ $ python -m setuptools_scm ls # output trimmed for brevity
...
```
-!!! note "committed files only"
+!!! 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
+ 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.
+To explore other options, try
+```commandline
+$ python -m setuptools_scm --help
+## At runtime
-### version at runtime
+### Python Metadata
-If you have opted not to hardcode the version number inside the package,
-you can retrieve it at runtime from [PEP-0566](https://www.python.org/dev/peps/pep-0566/) metadata using
+The standard method to retrieve the version number at runtime is via
+[PEP-0566](https://www.python.org/dev/peps/pep-0566/) metadata using
``importlib.metadata`` from the standard library (added in Python 3.8)
-or the [`importlib_metadata`](https://pypi.org/project/importlib-metadata/) backport:
+or the
+[`importlib_metadata`](https://pypi.org/project/importlib-metadata/)
+backport for earlier versions:
```python title="package_name/__init__.py"
from importlib.metadata import version, PackageNotFoundError
@@ -116,6 +101,40 @@ except PackageNotFoundError:
pass
```
+### Via your version file
+
+If you have opted to create a Python version file via the standard
+template, you can import that file, where you will have a ``version``
+string and a ``version_tuple`` tuple with elements corresponding to
+the version tags.
+
+```python title="Using package_name/_version.py"
+import package_name._version as v
+
+print(v.version)
+print(v.version_tuple)
+```
+
+### Via setuptools_scm (strongly discouraged)
+
+While the most simple **looking** way to use `setuptools_scm` at
+runtime is:
+
+```python
+from setuptools_scm import get_version
+version = get_version()
+```
+
+it is strongly discouraged to call directly into `setuptools_scm` over
+the standard Python `importlib.metadata`.
+
+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__)
+```
### Usage from Sphinx
@@ -132,7 +151,7 @@ the working directory for good reasons and using the installed metadata
prevents using needless volatile data there.
-## with Docker/Podman
+### With Docker/Podman
In some situations, Docker may not copy the `.git` into the container when
@@ -172,7 +191,7 @@ is preferred over `SETUPTOOLS_SCM_PRETEND_VERSION`.
## Default versioning scheme
-In the standard configuration `setuptools_scm` takes a look at three things:
+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)
@@ -205,7 +224,7 @@ so you will see an additional `g` prepended to the `{revision hash}`.
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].
+ versions. Local version identifiers must comply with [PEP 440](https://peps.python.org/pep-0440/#local-version-identifiers>).
## Semantic Versioning (SemVer)
@@ -235,9 +254,19 @@ Ensure the content of the following files:
node: $Format:%H$
node-date: $Format:%cI$
describe-name: $Format:%(describe:tags=true,match=*[0-9]*)$
-ref-names: $Format:%D$
```
+Feel free to alter the `match` field in `describe-name` to match your project's
+tagging style.
+
+!!! note
+
+ If your git host provider does not properly expand `describe-name`, you may
+ need to include `ref-names: $Format:%D$`. But **beware**, this can often
+ lead to the git archive's checksum changing after a commit is added
+ post-release. See [this issue][git-archive-issue] for more details.
+
+
``` {.text file=".gitattributes"}
.git_archival.txt export-subst
```
@@ -251,14 +280,14 @@ $ 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.
-
+[git-archive-issue]: https://github.com/pypa/setuptools-scm/issues/806
### File finders hook makes most of `MANIFEST.in` unnecessary
-`setuptools_scm` implements a [file_finders] entry point
+`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:
+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`.
diff --git a/pyproject.toml b/pyproject.toml
index a93544d2..ba539651 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,7 +4,7 @@
build-backend = "_own_version_helper:build_meta"
requires = [
"setuptools>=61",
- 'tomli; python_version < "3.11"',
+ 'tomli<=2.0.2; python_version < "3.11"',
]
backend-path = [
".",
@@ -41,9 +41,9 @@ dynamic = [
]
dependencies = [
"packaging>=20",
- "setuptools",
+ "setuptools>=61",
'tomli>=1; python_version < "3.11"',
- "typing-extensions",
+ 'typing-extensions; python_version < "3.10"',
]
[project.optional-dependencies]
docs = [
@@ -61,15 +61,19 @@ test = [
"build",
"pytest",
"rich",
+ 'typing-extensions; python_version < "3.11"',
"wheel",
]
toml = [
]
[project.urls]
documentation = "https://setuptools-scm.readthedocs.io/"
-repository = "https://github.com/pypa/setuptools_scm/"
+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."pipx.run"]
+setuptools-scm = "setuptools_scm._cli:main"
+setuptools_scm = "setuptools_scm._cli:main"
[project.entry-points."setuptools.file_finders"]
setuptools_scm = "setuptools_scm._file_finders:find_files"
[project.entry-points."setuptools.finalize_distribution_options"]
@@ -98,6 +102,7 @@ PKG-INFO = "setuptools_scm.fallbacks:parse_pkginfo"
"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"
+"only-version" = "setuptools_scm.version:only_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"
@@ -112,10 +117,23 @@ version = { attr = "_own_version_helper.version"}
[tool.setuptools_scm]
[tool.ruff]
-select = ["E", "F", "B", "U", "YTT", "C", "DTZ", "PYI", "PT"]
-ignore = ["B028"]
+src = ["src"]
+fix = true
+lint.select = ["E", "F", "B", "UP", "YTT", "C", "DTZ", "PYI", "PT", "I", "FURB", "RUF"]
+lint.ignore = ["B028"]
+lint.preview = true
+
+[tool.ruff.lint.isort]
+force-single-line = true
+from-first = false
+lines-between-types = 1
+order-by-type = true
+
+[tool.repo-review]
+ignore = ["PP305", "GH103", "GH212", "MY100", "PC111", "PC160", "PC170", "PC180", "PC901"]
[tool.pytest.ini_options]
+minversion = "7"
testpaths = ["testing"]
filterwarnings = [
"error",
@@ -125,7 +143,7 @@ filterwarnings = [
log_level = "debug"
log_cli_level = "info"
# disable unraisable until investigated
-addopts = ["-p", "no:unraisableexception"]
+addopts = ["-ra", "--strict-config", "--strict-markers", "-p", "no:unraisableexception"]
markers = [
"issue(id): reference to github issue",
"skip_commit: allows to skip committing in the helpers",
diff --git a/src/setuptools_scm/.git_archival.txt b/src/setuptools_scm/.git_archival.txt
index 8fb235d7..7c510094 100644
--- a/src/setuptools_scm/.git_archival.txt
+++ b/src/setuptools_scm/.git_archival.txt
@@ -1,4 +1,3 @@
node: $Format:%H$
node-date: $Format:%cI$
describe-name: $Format:%(describe:tags=true,match=*[0-9]*)$
-ref-names: $Format:%D$
diff --git a/src/setuptools_scm/__init__.py b/src/setuptools_scm/__init__.py
index aa40ab31..e265e859 100644
--- a/src/setuptools_scm/__init__.py
+++ b/src/setuptools_scm/__init__.py
@@ -2,29 +2,29 @@
:copyright: 2010-2023 by Ronny Pfannschmidt
:license: MIT
"""
+
from __future__ import annotations
+from ._config import DEFAULT_LOCAL_SCHEME
+from ._config import DEFAULT_VERSION_SCHEME
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 ._get_version_impl import _get_version
+from ._get_version_impl import get_version
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",
+ "DEFAULT_VERSION_SCHEME",
"Configuration",
- "Version",
- "ScmVersion",
"NonNormalizedVersion",
+ "ScmVersion",
+ "Version",
+ "_get_version",
+ "dump_version",
+ # soft deprecated imports, left for backward compatibility
+ "get_version",
]
diff --git a/src/setuptools_scm/__main__.py b/src/setuptools_scm/__main__.py
index dab6068a..3f56d42a 100644
--- a/src/setuptools_scm/__main__.py
+++ b/src/setuptools_scm/__main__.py
@@ -3,4 +3,4 @@
from ._cli import main
if __name__ == "__main__":
- main()
+ raise SystemExit(main())
diff --git a/src/setuptools_scm/_cli.py b/src/setuptools_scm/_cli.py
index 66099b12..b54903a4 100644
--- a/src/setuptools_scm/_cli.py
+++ b/src/setuptools_scm/_cli.py
@@ -1,16 +1,19 @@
from __future__ import annotations
import argparse
+import json
import os
import sys
+from typing import Any
+
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:
+def main(args: list[str] | None = None) -> int:
opts = _get_cli_opts(args)
inferred_root: str = opts.root or "."
@@ -29,18 +32,17 @@ def main(args: list[str] | None = None) -> None:
f" Reason: {ex}.",
file=sys.stderr,
)
- config = Configuration(inferred_root)
+ config = Configuration(root=inferred_root)
- version = _get_version(config, force_write_version_files=False)
+ version = _get_version(
+ config, force_write_version_files=opts.force_write_version_files
+ )
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)
+ return command(opts, version, config)
def _get_cli_opts(args: list[str] | None) -> argparse.Namespace:
@@ -59,7 +61,7 @@ def _get_cli_opts(args: list[str] | None) -> argparse.Namespace:
"--config",
default=None,
metavar="PATH",
- help="path to 'pyproject.toml' with setuptools_scm config, "
+ help="path to 'pyproject.toml' with setuptools-scm config, "
"default: looked up in the current or parent directories",
)
parser.add_argument(
@@ -67,13 +69,112 @@ def _get_cli_opts(args: list[str] | None) -> argparse.Namespace:
action="store_true",
help="remove the dev/local parts of the version before printing the version",
)
+ parser.add_argument(
+ "-N",
+ "--no-version",
+ action="store_true",
+ help="do not include package version in the output",
+ )
+ output_formats = ["json", "plain", "key-value"]
+ parser.add_argument(
+ "-f",
+ "--format",
+ type=str.casefold,
+ default="plain",
+ help="specify output format",
+ choices=output_formats,
+ )
+ parser.add_argument(
+ "-q",
+ "--query",
+ type=str.casefold,
+ nargs="*",
+ help="display setuptools-scm settings according to query, "
+ "e.g. dist_name, do not supply an argument in order to "
+ "print a list of valid queries.",
+ )
+ parser.add_argument(
+ "--force-write-version-files",
+ action="store_true",
+ help="trigger to write the content of the version files\n"
+ "its recommended to use normal/editable installation instead)",
+ )
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"
+ desc = "List information about the package, e.g. included files"
sub.add_parser("ls", help=desc[0].lower() + desc[1:], description=desc)
return parser.parse_args(args)
+# flake8: noqa: C901
+def command(opts: argparse.Namespace, version: str, config: Configuration) -> int:
+ data: dict[str, Any] = {}
+
+ if opts.command == "ls":
+ opts.query = ["files"]
+
+ if opts.query == []:
+ opts.no_version = True
+ sys.stderr.write("Available queries:\n\n")
+ opts.query = ["queries"]
+ data["queries"] = ["files", *config.__dataclass_fields__]
+
+ if opts.query is None:
+ opts.query = []
+
+ if not opts.no_version:
+ data["version"] = version
+
+ if "files" in opts.query:
+ data["files"] = find_files(config.root)
+
+ for q in opts.query:
+ if q in ["files", "queries", "version"]:
+ continue
+
+ try:
+ if q.startswith("_"):
+ raise AttributeError()
+ data[q] = getattr(config, q)
+ except AttributeError:
+ sys.stderr.write(f"Error: unknown query: '{q}'\n")
+ return 1
+
+ if opts.format == "json":
+ print(json.dumps(data, indent=2))
+
+ if opts.format == "plain":
+ _print_plain(data)
+
+ if opts.format == "key-value":
+ _print_key_value(data)
+
+ return 0
+
+
+def _print_plain(data: dict[str, Any]) -> None:
+ version = data.pop("version", None)
+ if version:
+ print(version)
+ files = data.pop("files", [])
+ for file_ in files:
+ print(file_)
+ queries = data.pop("queries", [])
+ for query in queries:
+ print(query)
+ if data:
+ print("\n".join(data.values()))
+
+
+def _print_key_value(data: dict[str, Any]) -> None:
+ for key, value in data.items():
+ if isinstance(value, str):
+ print(f"{key} = {value}")
+ else:
+ str_value = "\n ".join(value)
+ print(f"{key} = {str_value}")
+
+
def _find_pyproject(parent: str) -> str:
for directory in walk_potential_roots(os.path.abspath(parent)):
pyproject = os.path.join(directory, "pyproject.toml")
diff --git a/src/setuptools_scm/_config.py b/src/setuptools_scm/_config.py
index 5e5feb17..8d5eac2b 100644
--- a/src/setuptools_scm/_config.py
+++ b/src/setuptools_scm/_config.py
@@ -1,10 +1,12 @@
-""" configuration """
+"""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
@@ -17,9 +19,9 @@
)
from ._integration.pyproject_reading import read_pyproject as _read_pyproject
from ._overrides import read_toml_overrides
+from ._version_cls import Version as _Version
from ._version_cls import _validate_version_cls
from ._version_cls import _VersionT
-from ._version_cls import Version as _Version
log = _log.log.getChild("config")
@@ -41,9 +43,9 @@ def _check_tag_regex(value: str | Pattern[str] | None) -> Pattern[str]:
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."
+ raise ValueError(
+ f"Expected tag_regex '{regex.pattern}' to contain a single match group or"
+ " a group named 'version' to identify the version part of any tag."
)
return regex
@@ -52,8 +54,7 @@ def _check_tag_regex(value: str | Pattern[str] | None) -> Pattern[str]:
class ParseFunction(Protocol):
def __call__(
self, root: _t.PathT, *, config: Configuration
- ) -> _t.SCMVERSION | None:
- ...
+ ) -> _t.SCMVERSION | None: ...
def _check_absolute_root(root: _t.PathT, relative_to: _t.PathT | None) -> str:
@@ -105,6 +106,9 @@ class Configuration:
parent: _t.PathT | None = None
+ def __post_init__(self) -> None:
+ self.tag_regex = _check_tag_regex(self.tag_regex)
+
@property
def absolute_root(self) -> str:
return _check_absolute_root(self.root, self.relative_to)
@@ -139,13 +143,11 @@ def from_data(
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,
+ relative_to=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
index 50c91829..510a96b8 100644
--- a/src/setuptools_scm/_entrypoints.py
+++ b/src/setuptools_scm/_entrypoints.py
@@ -1,24 +1,24 @@
from __future__ import annotations
import sys
+
+from typing import TYPE_CHECKING
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 typing import cast
from . import _log
from . import version
if TYPE_CHECKING:
from . import _types as _t
- from ._config import Configuration, ParseFunction
+ from ._config import Configuration
+ from ._config import 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
@@ -38,7 +38,8 @@ def entry_points(group: str) -> EntryPoints:
return EntryPoints(legacy_entry_points()[group])
else:
- from importlib.metadata import entry_points, EntryPoints
+ from importlib.metadata import EntryPoints
+ from importlib.metadata import entry_points
log = _log.log.getChild("entrypoints")
@@ -106,34 +107,26 @@ def _iter_version_schemes(
yield scheme_value
-@overload
def _call_version_scheme(
version: version.ScmVersion,
entrypoint: str,
given_value: _t.VERSION_SCHEMES,
- default: str,
+ default: str | None = None,
) -> 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:
+ found_any_implementation = False
for scheme in _iter_version_schemes(entrypoint, given_value):
+ found_any_implementation = True
result = scheme(version)
if result is not None:
return result
- return default
+ if not found_any_implementation:
+ raise ValueError(
+ f'Couldn\'t find any implementations for entrypoint "{entrypoint}"'
+ f' with value "{given_value}".'
+ )
+ if default is not None:
+ return default
+ raise ValueError(
+ f'None of the "{entrypoint}" entrypoints matching "{given_value}"'
+ " returned a value."
+ )
diff --git a/src/setuptools_scm/_file_finders/__init__.py b/src/setuptools_scm/_file_finders/__init__.py
index 403ca4f8..8201bae1 100644
--- a/src/setuptools_scm/_file_finders/__init__.py
+++ b/src/setuptools_scm/_file_finders/__init__.py
@@ -2,15 +2,22 @@
import itertools
import os
-from typing import Callable
+
from typing import TYPE_CHECKING
+from typing import Callable
from .. import _log
from .. import _types as _t
from .._entrypoints import iter_entry_points
+from .pathtools import norm_real
if TYPE_CHECKING:
- from typing_extensions import TypeGuard
+ import sys
+
+ if sys.version_info >= (3, 10):
+ from typing import TypeGuard
+ else:
+ from typing_extensions import TypeGuard
log = _log.log.getChild("file_finder")
@@ -37,12 +44,12 @@ def scm_find_files(
Spec here: https://setuptools.pypa.io/en/latest/userguide/extension.html#\
adding-support-for-revision-control-systems
"""
- realpath = os.path.normcase(os.path.realpath(path))
+ realpath = norm_real(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))
+ realdirpath = norm_real(dirpath)
def _link_not_in_scm(n: str, realdirpath: str = realdirpath) -> bool:
fn = os.path.join(realdirpath, os.path.normcase(n))
@@ -72,7 +79,7 @@ def _link_not_in_scm(n: str, realdirpath: str = realdirpath) -> bool:
continue
# dirpath + filename with symlinks preserved
fullfilename = os.path.join(dirpath, filename)
- is_tracked = os.path.normcase(os.path.realpath(fullfilename)) in scm_files
+ is_tracked = norm_real(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)
diff --git a/src/setuptools_scm/_file_finders/git.py b/src/setuptools_scm/_file_finders/git.py
index 873b4ba3..7b23f886 100644
--- a/src/setuptools_scm/_file_finders/git.py
+++ b/src/setuptools_scm/_file_finders/git.py
@@ -4,13 +4,15 @@
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
+from . import is_toplevel_acceptable
+from . import scm_find_files
+from .pathtools import norm_real
log = logging.getLogger(__name__)
@@ -43,7 +45,7 @@ def _git_toplevel(path: str) -> str | None:
# ``\\`` 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()))
+ return norm_real(out)
except subprocess.CalledProcessError:
# git returned error, we are not in a git repo
return None
@@ -91,7 +93,7 @@ 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))
+ fullpath = norm_real(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)
diff --git a/src/setuptools_scm/_file_finders/hg.py b/src/setuptools_scm/_file_finders/hg.py
index f87ba066..9115a5fa 100644
--- a/src/setuptools_scm/_file_finders/hg.py
+++ b/src/setuptools_scm/_file_finders/hg.py
@@ -9,18 +9,18 @@
from .._file_finders import scm_find_files
from .._run_cmd import run as _run
from ..integration import data_from_mime
+from .pathtools import norm_real
log = logging.getLogger(__name__)
def _hg_toplevel(path: str) -> str | None:
try:
- res = _run(
+ return _run(
["hg", "root"],
cwd=(path or "."),
- )
- res.check_returncode()
- return os.path.normcase(os.path.realpath(res.stdout))
+ check=True,
+ ).parse_success(norm_real)
except subprocess.CalledProcessError:
# hg returned error, we are not in a mercurial repo
return None
diff --git a/src/setuptools_scm/_file_finders/pathtools.py b/src/setuptools_scm/_file_finders/pathtools.py
new file mode 100644
index 00000000..6de85089
--- /dev/null
+++ b/src/setuptools_scm/_file_finders/pathtools.py
@@ -0,0 +1,9 @@
+from __future__ import annotations
+
+import os
+
+from setuptools_scm import _types as _t
+
+
+def norm_real(path: _t.PathT) -> str:
+ return os.path.normcase(os.path.realpath(path))
diff --git a/src/setuptools_scm/_get_version_impl.py b/src/setuptools_scm/_get_version_impl.py
index 2d9d9478..cced45e2 100644
--- a/src/setuptools_scm/_get_version_impl.py
+++ b/src/setuptools_scm/_get_version_impl.py
@@ -3,6 +3,7 @@
import logging
import re
import warnings
+
from pathlib import Path
from typing import Any
from typing import NoReturn
@@ -15,8 +16,12 @@
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
+from .version import format_version as _format_version
+
+EMPTY_TAG_REGEX_DEPRECATION = DeprecationWarning(
+ "empty regex for tag regex is invalid, using default"
+)
_log = logging.getLogger(__name__)
@@ -117,7 +122,10 @@ def _version_missing(config: Configuration) -> NoReturn:
"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"
+ "use git+https://github.com/user/proj.git#egg=proj\n\n"
+ "Alternatively, set the version with the environment variable "
+ "SETUPTOOLS_SCM_PRETEND_VERSION_FOR_${NORMALIZED_DIST_NAME} as described "
+ "in https://setuptools-scm.readthedocs.io/en/latest/config."
)
@@ -144,7 +152,7 @@ def get_version(
"""
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
+ in the root of the repository to direct setuptools-scm to the
root of the repository by supplying ``__file__``.
"""
@@ -162,11 +170,7 @@ def get_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"
- )
- )
+ warnings.warn(EMPTY_TAG_REGEX_DEPRECATION)
return _config.DEFAULT_TAG_REGEX
else:
return re.compile(tag_regex)
diff --git a/src/setuptools_scm/_integration/dump_version.py b/src/setuptools_scm/_integration/dump_version.py
index d8902432..a7bfcae7 100644
--- a/src/setuptools_scm/_integration/dump_version.py
+++ b/src/setuptools_scm/_integration/dump_version.py
@@ -1,6 +1,7 @@
from __future__ import annotations
import warnings
+
from pathlib import Path
from .. import _types as _t
@@ -8,16 +9,20 @@
from .._version_cls import _version_as_tuple
from ..version import ScmVersion
-
log = parent_log.getChild("dump_version")
TEMPLATES = {
".py": """\
-# file generated by setuptools_scm
+# file generated by setuptools-scm
# don't change, don't track in version control
+
+__all__ = ["__version__", "__version_tuple__", "version", "version_tuple"]
+
TYPE_CHECKING = False
if TYPE_CHECKING:
- from typing import Tuple, Union
+ from typing import Tuple
+ from typing import Union
+
VERSION_TUPLE = Tuple[Union[int, str], ...]
else:
VERSION_TUPLE = object
diff --git a/src/setuptools_scm/_integration/pyproject_reading.py b/src/setuptools_scm/_integration/pyproject_reading.py
index c9818a29..0e4f9aa1 100644
--- a/src/setuptools_scm/_integration/pyproject_reading.py
+++ b/src/setuptools_scm/_integration/pyproject_reading.py
@@ -1,14 +1,14 @@
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
-
+from .toml import read_toml_content
log = _log.log.getChild("pyproject_reading")
@@ -39,7 +39,7 @@ def read_pyproject(
if require_section:
raise LookupError(error) from e
else:
- log.warning("toml section missing %r", error)
+ log.warning("toml section missing %r", error, exc_info=True)
section = {}
project = defn.get("project", {})
diff --git a/src/setuptools_scm/_integration/setuptools.py b/src/setuptools_scm/_integration/setuptools.py
index f574d23d..3ed48470 100644
--- a/src/setuptools_scm/_integration/setuptools.py
+++ b/src/setuptools_scm/_integration/setuptools.py
@@ -3,6 +3,7 @@
import logging
import os
import warnings
+
from typing import Any
from typing import Callable
@@ -30,10 +31,10 @@ def _warn_on_old_setuptools(_version: str = setuptools.__version__) -> None:
warnings.warn(
RuntimeWarning(
f"""
-ERROR: setuptools=={_version} is used in combination with setuptools_scm>=8.x
+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
+setuptools-scm requires setuptools>=61
Suggested workaround if applicable:
- migrating from the deprecated setup_requires mechanism to pep517/518
@@ -47,7 +48,8 @@ def _warn_on_old_setuptools(_version: str = setuptools.__version__) -> None:
def _assign_version(
dist: setuptools.Distribution, config: _config.Configuration
) -> None:
- from .._get_version_impl import _get_version, _version_missing
+ from .._get_version_impl import _get_version
+ from .._get_version_impl import _version_missing
# todo: build time plugin
maybe_version = _get_version(config, force_write_version_files=True)
@@ -111,11 +113,11 @@ def infer_version(dist: setuptools.Distribution) -> None:
dist_name = read_dist_name_from_setup_cfg()
if not os.path.isfile("pyproject.toml"):
return
- if dist_name == "setuptools_scm":
+ if dist_name == "setuptools-scm":
return
try:
config = _config.Configuration.from_file(dist_name=dist_name)
except LookupError as e:
- log.warning(e)
+ log.info(e, exc_info=True)
else:
_assign_version(dist, config)
diff --git a/src/setuptools_scm/_integration/toml.py b/src/setuptools_scm/_integration/toml.py
index a08b7b88..8ca38d97 100644
--- a/src/setuptools_scm/_integration/toml.py
+++ b/src/setuptools_scm/_integration/toml.py
@@ -1,13 +1,14 @@
from __future__ import annotations
import sys
+
from pathlib import Path
+from typing import TYPE_CHECKING
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
+from typing import cast
if sys.version_info >= (3, 11):
from tomllib import loads as load_toml
@@ -15,7 +16,10 @@
from tomli import loads as load_toml
if TYPE_CHECKING:
- from typing_extensions import TypeAlias
+ if sys.version_info >= (3, 10):
+ from typing import TypeAlias
+ else:
+ from typing_extensions import TypeAlias
from .. import _log
diff --git a/src/setuptools_scm/_log.py b/src/setuptools_scm/_log.py
index 1247d46c..7e4b7db7 100644
--- a/src/setuptools_scm/_log.py
+++ b/src/setuptools_scm/_log.py
@@ -1,12 +1,14 @@
"""
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
diff --git a/src/setuptools_scm/_modify_version.py b/src/setuptools_scm/_modify_version.py
index 63c0dfda..aae41a63 100644
--- a/src/setuptools_scm/_modify_version.py
+++ b/src/setuptools_scm/_modify_version.py
@@ -6,7 +6,7 @@
def strip_local(version_string: str) -> str:
- public, sep, local = version_string.partition("+")
+ public = version_string.partition("+")[0]
return public
diff --git a/src/setuptools_scm/_overrides.py b/src/setuptools_scm/_overrides.py
index 792bfd27..ee9269a7 100644
--- a/src/setuptools_scm/_overrides.py
+++ b/src/setuptools_scm/_overrides.py
@@ -2,6 +2,7 @@
import os
import re
+
from typing import Any
from . import _config
diff --git a/src/setuptools_scm/_run_cmd.py b/src/setuptools_scm/_run_cmd.py
index 5861411c..42904cfb 100644
--- a/src/setuptools_scm/_run_cmd.py
+++ b/src/setuptools_scm/_run_cmd.py
@@ -5,13 +5,14 @@
import subprocess
import textwrap
import warnings
+
+from typing import TYPE_CHECKING
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 typing import overload
from . import _log
from . import _types as _t
@@ -25,7 +26,12 @@
# unfortunately github CI for windows sometimes needs
# up to 30 seconds to start a command
-BROKEN_TIMEOUT: Final[int] = 40
+
+def _get_timeout(env: Mapping[str, str]) -> int:
+ return int(env.get("SETUPTOOLS_SCM_SUBPROCESS_TIMEOUT") or 40)
+
+
+BROKEN_TIMEOUT: Final[int] = _get_timeout(os.environ)
log = _log.log.getChild("run_cmd")
@@ -51,8 +57,7 @@ def parse_success(
parse: Callable[[str], PARSE_RESULT],
default: None = None,
error_msg: str | None = None,
- ) -> PARSE_RESULT | None:
- ...
+ ) -> PARSE_RESULT | None: ...
@overload
def parse_success(
@@ -60,8 +65,7 @@ def parse_success(
parse: Callable[[str], PARSE_RESULT],
default: T,
error_msg: str | None = None,
- ) -> PARSE_RESULT | T:
- ...
+ ) -> PARSE_RESULT | T: ...
def parse_success(
self,
@@ -113,7 +117,7 @@ def avoid_pip_isolation(env: Mapping[str, str]) -> dict[str, str]:
[
path
for path in new_env["PYTHONPATH"].split(os.pathsep)
- if "pip-build-env-" not in path
+ if "-build-env-" not in path
]
)
return new_env
@@ -132,7 +136,7 @@ def run(
*,
strip: bool = True,
trace: bool = True,
- timeout: int = BROKEN_TIMEOUT,
+ timeout: int | None = None,
check: bool = False,
) -> CompletedProcess:
if isinstance(cmd, str):
@@ -141,6 +145,8 @@ def run(
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)
+ if timeout is None:
+ timeout = BROKEN_TIMEOUT
res = subprocess.run(
cmd,
capture_output=True,
@@ -181,7 +187,7 @@ def has_command(
name: str, args: Sequence[str] = ["version"], warn: bool = True
) -> bool:
try:
- p = run([name, *args], cwd=".", timeout=BROKEN_TIMEOUT)
+ p = run([name, *args], cwd=".")
if p.returncode != 0:
log.error(f"Command '{name}' returned non-zero. This is stderr:")
log.error(p.stderr)
@@ -195,7 +201,7 @@ def has_command(
else:
res = not p.returncode
if not res and warn:
- warnings.warn("%r was not found" % name, category=RuntimeWarning)
+ warnings.warn(f"{name!r} was not found", category=RuntimeWarning)
return res
diff --git a/src/setuptools_scm/_types.py b/src/setuptools_scm/_types.py
index df8fa945..b655c76f 100644
--- a/src/setuptools_scm/_types.py
+++ b/src/setuptools_scm/_types.py
@@ -1,16 +1,22 @@
from __future__ import annotations
import os
+
+from typing import TYPE_CHECKING
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
+ import sys
+
+ if sys.version_info >= (3, 10):
+ from typing import TypeAlias
+ else:
+ from typing_extensions import TypeAlias
+
from . import version
PathT: TypeAlias = Union["os.PathLike[str]", str]
diff --git a/src/setuptools_scm/_version_cls.py b/src/setuptools_scm/_version_cls.py
index 3fd4a32e..bb89bbb1 100644
--- a/src/setuptools_scm/_version_cls.py
+++ b/src/setuptools_scm/_version_cls.py
@@ -1,8 +1,8 @@
from __future__ import annotations
-from typing import cast
from typing import Type
from typing import Union
+from typing import cast
try:
from packaging.version import InvalidVersion
diff --git a/src/setuptools_scm/discover.py b/src/setuptools_scm/discover.py
index b12b2f12..7c1be381 100644
--- a/src/setuptools_scm/discover.py
+++ b/src/setuptools_scm/discover.py
@@ -1,6 +1,7 @@
from __future__ import annotations
import os
+
from pathlib import Path
from typing import Iterable
from typing import Iterator
diff --git a/src/setuptools_scm/fallbacks.py b/src/setuptools_scm/fallbacks.py
index e1ea60c9..45a75351 100644
--- a/src/setuptools_scm/fallbacks.py
+++ b/src/setuptools_scm/fallbacks.py
@@ -2,6 +2,7 @@
import logging
import os
+
from pathlib import Path
from typing import TYPE_CHECKING
@@ -9,8 +10,8 @@
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 meta
from .version import tag_to_version
log = logging.getLogger(__name__)
diff --git a/src/setuptools_scm/git.py b/src/setuptools_scm/git.py
index d511961c..7eccf198 100644
--- a/src/setuptools_scm/git.py
+++ b/src/setuptools_scm/git.py
@@ -2,29 +2,32 @@
import dataclasses
import logging
+import operator
import os
import re
import shlex
+import sys
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 TYPE_CHECKING
from typing import Callable
from typing import Sequence
-from typing import TYPE_CHECKING
-from . import _types as _t
from . import Configuration
+from . import _types as _t
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 meta
from .version import tag_to_version
if TYPE_CHECKING:
@@ -53,7 +56,7 @@ def run_git(
repo: Path,
*,
check: bool = False,
- timeout: int = 20,
+ timeout: int | None = None,
) -> _CompletedProcess:
return _run(
["git", "--git-dir", repo / ".git", *args],
@@ -118,6 +121,8 @@ 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
+ if sys.version_info < (3, 11) and timestamp_text.endswith("Z"):
+ timestamp_text = timestamp_text[:-1] + "+00:00"
return datetime.fromisoformat(timestamp_text).date()
res = run_git(
@@ -140,8 +145,7 @@ def fetch_shallow(self) -> None:
run_git(["fetch", "--unshallow"], self.path, check=True, timeout=240)
def node(self) -> str | None:
- def _unsafe_short_node(node: str) -> str:
- return node[:7]
+ _unsafe_short_node = operator.itemgetter(slice(7))
return run_git(
["rev-parse", "--verify", "--quiet", "HEAD"], self.path
diff --git a/src/setuptools_scm/hg.py b/src/setuptools_scm/hg.py
index 522dfb66..43fb295b 100644
--- a/src/setuptools_scm/hg.py
+++ b/src/setuptools_scm/hg.py
@@ -3,6 +3,7 @@
import datetime
import logging
import os
+
from pathlib import Path
from typing import TYPE_CHECKING
@@ -10,14 +11,15 @@
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 meta
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
+from ._run_cmd import require_command as _require_command
+from ._run_cmd import run as _run
log = logging.getLogger(__name__)
@@ -33,10 +35,9 @@ def from_potential_worktree(cls, wd: _t.PathT) -> HgWorkdir | None:
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}"
+ node, tags_str, node_date_str = self.hg_log(
+ ".", "{node}\n{tag}\n{date|shortdate}"
).split("\n")
# TODO: support bookmarks and topics (but nowadays bookmarks are
@@ -100,8 +101,8 @@ def get_meta(self, config: Configuration) -> ScmVersion | None:
else:
return meta(tag, config=config, node_date=node_date)
- except ValueError as e:
- log.exception("error %s", e)
+ except ValueError:
+ log.exception("error")
pass # unpacking failed, old hg
return None
@@ -178,6 +179,7 @@ def archival_to_version(data: dict[str, str], config: Configuration) -> ScmVersi
data["latesttag"],
distance=int(data["latesttagdistance"]),
node=node,
+ branch=data.get("branch"),
config=config,
)
else:
diff --git a/src/setuptools_scm/hg_git.py b/src/setuptools_scm/hg_git.py
index b6c30360..9cab6f45 100644
--- a/src/setuptools_scm/hg_git.py
+++ b/src/setuptools_scm/hg_git.py
@@ -2,6 +2,7 @@
import logging
import os
+
from contextlib import suppress
from datetime import date
from pathlib import Path
diff --git a/src/setuptools_scm/integration.py b/src/setuptools_scm/integration.py
index 390b0a70..48874e38 100644
--- a/src/setuptools_scm/integration.py
+++ b/src/setuptools_scm/integration.py
@@ -2,6 +2,7 @@
import logging
import textwrap
+
from pathlib import Path
from . import _types as _t
diff --git a/src/setuptools_scm/version.py b/src/setuptools_scm/version.py
index f43e14b6..835e9e09 100644
--- a/src/setuptools_scm/version.py
+++ b/src/setuptools_scm/version.py
@@ -5,29 +5,36 @@
import os
import re
import warnings
+
from datetime import date
from datetime import datetime
from datetime import timezone
+from typing import TYPE_CHECKING
from typing import Any
from typing import Callable
from typing import Match
-from typing import TYPE_CHECKING
from . import _entrypoints
from . import _modify_version
if TYPE_CHECKING:
- from typing_extensions import Concatenate
- from typing_extensions import ParamSpec
+ import sys
+
+ if sys.version_info >= (3, 10):
+ from typing import Concatenate
+ from typing import ParamSpec
+ else:
+ from typing_extensions import Concatenate
+ from typing_extensions import ParamSpec
_P = ParamSpec("_P")
from typing import TypedDict
-
-from ._version_cls import Version as PkgVersion, _VersionT
-from . import _version_cls as _v
from . import _config
+from . import _version_cls as _v
+from ._version_cls import Version as PkgVersion
+from ._version_cls import _VersionT
log = logging.getLogger(__name__)
@@ -55,15 +62,21 @@ def _parse_version_tag(
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
+ if version := match.group(key):
+ result = _TagDict(
+ version=version,
+ prefix=full[: match.start(key)],
+ suffix=full[match.end(key) :],
+ )
+
+ log.debug("tag %r parsed to %r", tag, result)
+ return result
+
+ raise ValueError(
+ f'The tag_regex "{config.tag_regex.pattern}" matched tag "{tag}", '
+ "however the matched group has no value."
+ )
else:
log.debug("tag %r did not parse", tag)
@@ -209,7 +222,7 @@ def meta(
) -> ScmVersion:
parsed_version = _parse_tag(tag, preformatted, config)
log.info("version %s -> %s", tag, parsed_version)
- assert parsed_version is not None, "Can't parse version %s" % tag
+ assert parsed_version is not None, f"Can't parse version {tag}"
return ScmVersion(
parsed_version,
distance=distance,
@@ -237,10 +250,13 @@ def guess_next_dev_version(version: ScmVersion) -> str:
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
+ if isinstance(version.tag, _v.Version):
+ parts = list(version.tag.release[:retain])
+ else:
+ 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:
@@ -299,6 +315,10 @@ def release_branch_semver(version: ScmVersion) -> str:
return release_branch_semver_version(version)
+def only_version(version: ScmVersion) -> str:
+ return version.format_with("{tag}")
+
+
def no_guess_dev_version(version: ScmVersion) -> str:
if version.exact:
return version.format_with("{tag}")
@@ -428,8 +448,9 @@ def format_version(version: ScmVersion) -> str:
if version.preformatted:
assert isinstance(version.tag, str)
return version.tag
+
main_version = _entrypoints._call_version_scheme(
- version, "setuptools_scm.version_scheme", version.config.version_scheme, None
+ version, "setuptools_scm.version_scheme", version.config.version_scheme
)
log.debug("version %s", main_version)
assert main_version is not None
diff --git a/testing/conftest.py b/testing/conftest.py
index 05ab5344..d1c96ed3 100644
--- a/testing/conftest.py
+++ b/testing/conftest.py
@@ -2,17 +2,24 @@
import contextlib
import os
+import sys
+
from pathlib import Path
from types import TracebackType
from typing import Any
from typing import Iterator
import pytest
-from typing_extensions import Self
-from .wd_wrapper import WorkDir
from setuptools_scm._run_cmd import run
+if sys.version_info >= (3, 11):
+ from typing import Self
+else:
+ from typing_extensions import Self
+
+from .wd_wrapper import WorkDir
+
def pytest_configure() -> None:
# 2009-02-13T23:31:30+00:00
diff --git a/testing/test_basic_api.py b/testing/test_basic_api.py
index d42d40f2..76239841 100644
--- a/testing/test_basic_api.py
+++ b/testing/test_basic_api.py
@@ -2,21 +2,22 @@
import os
import sys
+
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._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 setuptools_scm.version import meta
from testing.wd_wrapper import WorkDir
-
c = Configuration()
template = """\
diff --git a/testing/test_cli.py b/testing/test_cli.py
index cc5a0ef0..7bb87f4a 100644
--- a/testing/test_cli.py
+++ b/testing/test_cli.py
@@ -1,15 +1,16 @@
from __future__ import annotations
import io
+
from contextlib import redirect_stdout
import pytest
+from setuptools_scm._cli import main
+
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]"
@@ -23,10 +24,7 @@ def get_output(args: list[str]) -> str:
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 '.*'"
+ UserWarning, match="root .. is overridden by the cli arg .*"
)
exits_with_not_found = pytest.raises(SystemExit, match="no version found for")
@@ -35,11 +33,9 @@ def get_output(args: list[str]) -> str:
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+")
@@ -50,9 +46,37 @@ def test_cli_find_pyproject(
with exits_with_not_found:
print(get_output(["-c", PYPROJECT_TOML]))
- with exits_with_not_found, warns_absolute_root_override:
+ with warns_cli_root_override, exits_with_not_found:
get_output(["-c", PYPROJECT_TOML, "--root=.."])
with warns_cli_root_override:
out = get_output(["-c", PYPROJECT_TOML, "--root=."])
assert out.startswith("0.1.dev1+")
+
+
+def test_cli_force_version_files(
+ wd: WorkDir, monkeypatch: pytest.MonkeyPatch, debug_mode: DebugMode
+) -> None:
+ debug_mode.disable()
+ wd.commit_testfile()
+ wd.write(
+ PYPROJECT_TOML,
+ """
+[project]
+name = "test"
+[tool.setuptools_scm]
+version_file = "ver.py"
+""",
+ )
+ monkeypatch.chdir(wd.cwd)
+
+ version_file = wd.cwd.joinpath("ver.py")
+ assert not version_file.exists()
+
+ get_output([])
+ assert not version_file.exists()
+
+ output = get_output(["--force-write-version-files"])
+ assert version_file.exists()
+
+ assert output[:5] in version_file.read_text("utf-8")
diff --git a/testing/test_config.py b/testing/test_config.py
index 6f19b23b..d0f06bd6 100644
--- a/testing/test_config.py
+++ b/testing/test_config.py
@@ -2,6 +2,7 @@
import re
import textwrap
+
from pathlib import Path
import pytest
@@ -97,3 +98,23 @@ def test_config_overrides(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> No
assert pristine.root != overridden.root
assert pristine.fallback_root != overridden.fallback_root
+
+
+@pytest.mark.parametrize(
+ "tag_regex",
+ [
+ r".*",
+ r"(.+)(.+)",
+ r"((.*))",
+ ],
+)
+def test_config_bad_regex(tag_regex: str) -> None:
+ with pytest.raises(
+ ValueError,
+ match=(
+ f"Expected tag_regex '{re.escape(tag_regex)}' to contain a single match"
+ " group or a group named 'version' to identify the version part of any"
+ " tag."
+ ),
+ ):
+ Configuration(tag_regex=re.compile(tag_regex))
diff --git a/testing/test_file_finder.py b/testing/test_file_finder.py
index 21b523a8..5af94fcf 100644
--- a/testing/test_file_finder.py
+++ b/testing/test_file_finder.py
@@ -2,13 +2,15 @@
import os
import sys
+
from typing import Iterable
import pytest
-from .wd_wrapper import WorkDir
from setuptools_scm._file_finders import find_files
+from .wd_wrapper import WorkDir
+
@pytest.fixture(params=["git", "hg"])
def inwd(
@@ -69,6 +71,19 @@ def test_case(inwd: WorkDir) -> None:
)
+@pytest.mark.skipif(
+ os.path.normcase("B") != os.path.normcase("b"), reason="case sensitive filesystem"
+)
+def test_case_cwd_evil(inwd: WorkDir, monkeypatch: pytest.MonkeyPatch) -> None:
+ (inwd.cwd / "CamelFile").touch()
+ (inwd.cwd / "file2").touch()
+ inwd.add_and_commit()
+ monkeypatch.chdir(inwd.cwd.parent.joinpath(inwd.cwd.name.capitalize()))
+ assert set(find_files()) == _sep(
+ {"CamelFile", "file2", "file1", "adir/filea", "bdir/fileb"}
+ )
+
+
@pytest.mark.skipif(sys.platform == "win32", reason="symlinks to dir not supported")
def test_symlink_dir(inwd: WorkDir) -> None:
(inwd.cwd / "adir" / "bdirlink").symlink_to("../bdir")
diff --git a/testing/test_functions.py b/testing/test_functions.py
index 71a1dd77..5f394b0b 100644
--- a/testing/test_functions.py
+++ b/testing/test_functions.py
@@ -2,6 +2,7 @@
import shutil
import subprocess
+
from pathlib import Path
import pytest
@@ -152,6 +153,14 @@ def test_dump_version_flake8(tmp_path: Path) -> None:
subprocess.run([flake8, "VERSION.py"], cwd=tmp_path, check=True)
+def test_dump_version_ruff(tmp_path: Path) -> None:
+ ruff = shutil.which("ruff")
+ if ruff is None:
+ pytest.skip("ruff not found")
+ dump_a_version(tmp_path)
+ subprocess.run([ruff, "check", "--no-fix", "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")
diff --git a/testing/test_git.py b/testing/test_git.py
index 7af70fa6..661dcb76 100644
--- a/testing/test_git.py
+++ b/testing/test_git.py
@@ -5,6 +5,7 @@
import shutil
import subprocess
import sys
+
from datetime import date
from datetime import datetime
from datetime import timezone
@@ -18,11 +19,10 @@
import pytest
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 import git
from setuptools_scm._file_finders.git import git_find_files
from setuptools_scm._run_cmd import CommandNotFoundError
from setuptools_scm._run_cmd import CompletedProcess
@@ -31,6 +31,9 @@
from setuptools_scm.git import archival_to_version
from setuptools_scm.version import format_version
+from .conftest import DebugMode
+from .wd_wrapper import WorkDir
+
pytestmark = pytest.mark.skipif(
not has_command("git", warn=False), reason="git executable not found"
)
@@ -104,7 +107,7 @@ def test_git_gone(wd: WorkDir, monkeypatch: pytest.MonkeyPatch) -> None:
assert wd.get_version(fallback_version="1.0") == "1.0"
-@pytest.mark.issue("https://github.com/pypa/setuptools_scm/issues/298")
+@pytest.mark.issue("https://github.com/pypa/setuptools-scm/issues/298")
@pytest.mark.issue(403)
def test_file_finder_no_history(wd: WorkDir, caplog: pytest.LogCaptureFixture) -> None:
file_list = git_find_files(str(wd.cwd))
@@ -113,7 +116,7 @@ def test_file_finder_no_history(wd: WorkDir, caplog: pytest.LogCaptureFixture) -
assert "listing git files failed - pretending there aren't any" in caplog.text
-@pytest.mark.issue("https://github.com/pypa/setuptools_scm/issues/281")
+@pytest.mark.issue("https://github.com/pypa/setuptools-scm/issues/281")
def test_parse_call_order(wd: WorkDir) -> None:
git.parse(str(wd.cwd), Configuration(), git.DEFAULT_DESCRIBE)
@@ -150,7 +153,7 @@ def break_folder_permissions(path: Path) -> Generator[None, None, None]:
sudo_devnull(["chgrp", "-R", str(original_stat.st_gid), path], check=True)
-@pytest.mark.issue("https://github.com/pypa/setuptools_scm/issues/707")
+@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())
@@ -297,7 +300,7 @@ def test_git_dirty_notag(
def test_git_worktree_support(wd: WorkDir, tmp_path: Path) -> None:
wd.commit_testfile()
worktree = tmp_path / "work_tree"
- wd("git worktree add -b work-tree %s" % worktree)
+ wd(f"git worktree add -b work-tree {worktree}")
res = run([sys.executable, "-m", "setuptools_scm", "ls"], cwd=worktree)
assert "test.txt" in res.stdout
@@ -398,7 +401,7 @@ def test_git_archive_run_from_subdirectory(
assert setuptools_scm._file_finders.find_files(".") == [opj(".", "test1.txt")]
-@pytest.mark.issue("https://github.com/pypa/setuptools_scm/issues/728")
+@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")
@@ -416,7 +419,7 @@ def test_git_feature_branch_increments_major(wd: WorkDir) -> None:
assert wd.get_version(version_scheme="python-simplified-semver").startswith("1.1.0")
-@pytest.mark.issue("https://github.com/pypa/setuptools_scm/issues/303")
+@pytest.mark.issue("https://github.com/pypa/setuptools-scm/issues/303")
def test_not_matching_tags(wd: WorkDir) -> None:
wd.commit_testfile()
wd("git tag apache-arrow-0.11.1")
@@ -429,7 +432,7 @@ def test_not_matching_tags(wd: WorkDir) -> None:
).startswith("0.11.2")
-@pytest.mark.issue("https://github.com/pypa/setuptools_scm/issues/411")
+@pytest.mark.issue("https://github.com/pypa/setuptools-scm/issues/411")
def test_non_dotted_version(wd: WorkDir) -> None:
wd.commit_testfile()
wd("git tag apache-arrow-1")
@@ -453,12 +456,12 @@ def test_non_dotted_tag_no_version_match(wd: WorkDir) -> None:
assert wd.get_version().startswith("0.11.2.dev2")
-@pytest.mark.issue("https://github.com/pypa/setuptools_scm/issues/381")
+@pytest.mark.issue("https://github.com/pypa/setuptools-scm/issues/381")
def test_gitdir(monkeypatch: pytest.MonkeyPatch, wd: WorkDir) -> None:
""" """
wd.commit_testfile()
normal = wd.get_version()
- # git hooks set this and break subsequent setuptools_scm unless we clean
+ # git hooks set this and break subsequent setuptools-scm unless we clean
monkeypatch.setenv("GIT_DIR", __file__)
assert wd.get_version() == normal
@@ -496,6 +499,22 @@ def test_git_getdate_badgit(
assert git_wd.get_head_date() is None
+def test_git_getdate_git_2_45_0_plus(
+ wd: WorkDir, caplog: pytest.LogCaptureFixture, monkeypatch: pytest.MonkeyPatch
+) -> None:
+ wd.commit_testfile()
+ git_wd = git.GitWorkdir(wd.cwd)
+ fake_date_result = CompletedProcess(
+ args=[], stdout="2024-04-30T22:33:10Z", stderr="", returncode=0
+ )
+ with patch.object(
+ git,
+ "run_git",
+ Mock(return_value=fake_date_result),
+ ):
+ assert git_wd.get_head_date() == date(2024, 4, 30)
+
+
@pytest.fixture()
def signed_commit_wd(monkeypatch: pytest.MonkeyPatch, wd: WorkDir) -> WorkDir:
if not has_command("gpg", args=["--version"], warn=False):
@@ -521,7 +540,7 @@ def signed_commit_wd(monkeypatch: pytest.MonkeyPatch, wd: WorkDir) -> WorkDir:
return wd
-@pytest.mark.issue("https://github.com/pypa/setuptools_scm/issues/548")
+@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)
@@ -559,7 +578,7 @@ def test_git_archival_to_version(expected: str, from_data: dict[str, str]) -> No
assert format_version(version) == expected
-@pytest.mark.issue("https://github.com/pypa/setuptools_scm/issues/727")
+@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)
diff --git a/testing/test_integration.py b/testing/test_integration.py
index f043da6d..86c8cbee 100644
--- a/testing/test_integration.py
+++ b/testing/test_integration.py
@@ -5,18 +5,21 @@
import subprocess
import sys
import textwrap
+
from pathlib import Path
import pytest
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
+from .wd_wrapper import WorkDir
+
c = Configuration()
@@ -229,9 +232,10 @@ def test_setuptools_version_keyword_ensures_regex(
wd.commit_testfile("test")
wd("git tag 1.0")
monkeypatch.chdir(wd.cwd)
- from setuptools_scm._integration.setuptools import version_keyword
import setuptools
+ from setuptools_scm._integration.setuptools import version_keyword
+
dist = setuptools.Distribution({"name": "test"})
version_keyword(dist, "use_scm_version", {"tag_regex": "(1.0)"})
diff --git a/testing/test_main.py b/testing/test_main.py
index 148bd45a..ad9a2903 100644
--- a/testing/test_main.py
+++ b/testing/test_main.py
@@ -1,22 +1,23 @@
from __future__ import annotations
-import os.path
import sys
import textwrap
+from pathlib import Path
+
import pytest
from .wd_wrapper import WorkDir
def test_main() -> None:
- mainfile = os.path.join(
- os.path.dirname(__file__), "..", "src", "setuptools_scm", "__main__.py"
+ mainfile = Path(__file__).parent.parent.joinpath(
+ "src", "setuptools_scm", "__main__.py"
)
ns = {"__package__": "setuptools_scm"}
- with open(mainfile, encoding="utf-8") as f:
- code = compile(f.read(), "__main__.py", "exec")
- exec(code, ns)
+
+ code = compile(mainfile.read_text(encoding="utf-8"), "__main__.py", "exec")
+ exec(code, ns)
@pytest.fixture()
diff --git a/testing/test_mercurial.py b/testing/test_mercurial.py
index 3aa00973..b51c3fd9 100644
--- a/testing/test_mercurial.py
+++ b/testing/test_mercurial.py
@@ -1,11 +1,13 @@
from __future__ import annotations
import os
+
from pathlib import Path
import pytest
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
@@ -14,7 +16,6 @@
from setuptools_scm.version import format_version
from testing.wd_wrapper import WorkDir
-
pytestmark = pytest.mark.skipif(
not has_command("hg", warn=False), reason="hg executable not found"
)
@@ -30,11 +31,17 @@ def wd(wd: WorkDir) -> WorkDir:
archival_mapping = {
"1.0": {"tag": "1.0"},
- "1.1.dev3+h000000000000": {
+ "1.1.0.dev3+h000000000000": {
"latesttag": "1.0",
"latesttagdistance": "3",
"node": "0" * 20,
},
+ "1.0.1.dev3+h000000000000": {
+ "latesttag": "1.0.0",
+ "latesttagdistance": "3",
+ "branch": "1.0",
+ "node": "0" * 20,
+ },
"0.0": {"node": "0" * 20},
"1.2.2": {"tag": "release-1.2.2"},
"1.2.2.dev0": {"tag": "release-1.2.2.dev"},
@@ -44,7 +51,7 @@ def wd(wd: WorkDir) -> WorkDir:
@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_scheme="release-branch-semver", local_scheme="node-and-date"
)
version = archival_to_version(data, config=config)
assert format_version(version) == expected
@@ -108,12 +115,22 @@ def test_version_from_archival(wd: WorkDir) -> None:
# entrypoints are unordered,
# cleaning the wd ensure this test won't break randomly
wd.cwd.joinpath(".hg").rename(wd.cwd / ".nothg")
- wd.write(".hg_archival.txt", "node: 000000000000\n" "tag: 0.1\n")
+ wd.write(
+ ".hg_archival.txt",
+ """\
+node: 000000000000
+tag: 0.1
+""",
+ )
assert wd.get_version() == "0.1"
wd.write(
".hg_archival.txt",
- "node: 000000000000\n" "latesttag: 0.1\n" "latesttagdistance: 3\n",
+ """\
+node: 000000000000
+latesttag: 0.1
+latesttagdistance: 3
+""",
)
assert wd.get_version() == "0.2.dev3+h000000000000"
@@ -184,7 +201,7 @@ def test_version_bump_from_commit_including_hgtag_mods(wd: WorkDir) -> None:
@pytest.mark.usefixtures("version_1_0")
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
+ 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.get_version() == "1.0.0"
diff --git a/testing/test_regressions.py b/testing/test_regressions.py
index 34f32ed8..d523f60c 100644
--- a/testing/test_regressions.py
+++ b/testing/test_regressions.py
@@ -3,9 +3,10 @@
import pprint
import subprocess
import sys
+
from dataclasses import replace
-from importlib.metadata import distribution
from importlib.metadata import EntryPoint
+from importlib.metadata import distribution
from pathlib import Path
import pytest
@@ -34,7 +35,10 @@ def test_pkginfo_noscmroot(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> N
tmp_path.joinpath(".git").mkdir()
p.joinpath("setup.py").write_text(
- "from setuptools import setup;" 'setup(use_scm_version={"root": ".."})',
+ """\
+from setuptools import setup
+setup(use_scm_version={"root": ".."})
+""",
encoding="utf-8",
)
diff --git a/testing/test_version.py b/testing/test_version.py
index ea4c7d99..c0c5853f 100644
--- a/testing/test_version.py
+++ b/testing/test_version.py
@@ -1,5 +1,7 @@
from __future__ import annotations
+import re
+
from dataclasses import replace
from datetime import date
from datetime import timedelta
@@ -9,17 +11,17 @@
from setuptools_scm import Configuration
from setuptools_scm import NonNormalizedVersion
+from setuptools_scm.version import ScmVersion
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 only_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)
@@ -54,6 +56,11 @@
"1.1.0.dev2",
id="feature_in_branch",
),
+ pytest.param(
+ meta(NonNormalizedVersion("v1.0"), distance=2, branch="default", config=c),
+ "1.0.1.dev2",
+ id="non-normalized-allowed",
+ ),
],
)
def test_next_semver(version: ScmVersion, expected_next: str) -> None:
@@ -170,6 +177,33 @@ def test_bump_dev_version_nonzero_raises() -> None:
guess_next_version(m("1.0.dev1"))
+@pytest.mark.parametrize(
+ "version",
+ [
+ "1.dev0",
+ "1.0.dev456",
+ "1.0a1",
+ "1.0a2.dev456",
+ "1.0a12.dev456",
+ "1.0a12",
+ "1.0b1.dev456",
+ "1.0b2",
+ "1.0b2.post345.dev456",
+ "1.0b2.post345",
+ "1.0rc1.dev456",
+ "1.0rc1",
+ "1.0",
+ "1.0.post456.dev34",
+ "1.0.post456",
+ "1.0.15",
+ "1.1.dev1",
+ ],
+)
+def test_only_version(version: str) -> None:
+ assert version == only_version(meta(version, config=c))
+ assert version == only_version(meta(version, distance=2, config=c))
+
+
@pytest.mark.parametrize(
("tag", "expected"),
[
@@ -189,7 +223,18 @@ def test_tag_regex1(tag: str, expected: str) -> None:
assert result.tag.public == expected
-@pytest.mark.issue("https://github.com/pypa/setuptools_scm/issues/471")
+def test_regex_match_but_no_version() -> None:
+ with pytest.raises(
+ ValueError,
+ match=(
+ r'The tag_regex "\(\?P\)\.\*" matched tag "v1",'
+ " however the matched group has no value"
+ ),
+ ):
+ meta("v1", config=replace(c, tag_regex=re.compile("(?P).*")))
+
+
+@pytest.mark.issue("https://github.com/pypa/setuptools-scm/issues/471")
def test_version_bump_bad() -> None:
class YikesVersion:
val: str
@@ -400,13 +445,47 @@ def __init__(self, tag_str: str) -> None:
self.tag = tag_str
def __str__(self) -> str:
- return "Custom %s" % self.tag
+ return f"Custom {self.tag}"
def __repr__(self) -> str:
- return "MyVersion" % self.tag
+ return f"MyVersion"
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"
+
+
+@pytest.mark.parametrize("config_key", ["version_scheme", "local_scheme"])
+def test_no_matching_entrypoints(config_key: str) -> None:
+ version = meta(
+ "1.0",
+ config=replace(c, **{config_key: "nonexistant"}), # type: ignore
+ )
+ with pytest.raises(
+ ValueError,
+ match=(
+ r'Couldn\'t find any implementations for entrypoint "setuptools_scm\..*?"'
+ ' with value "nonexistant"'
+ ),
+ ):
+ format_version(version)
+
+
+def test_all_entrypoints_return_none() -> None:
+ version = meta(
+ "1.0",
+ config=replace(
+ c,
+ version_scheme=lambda v: None, # type: ignore
+ ),
+ )
+ with pytest.raises(
+ ValueError,
+ match=(
+ 'None of the "setuptools_scm.version_scheme" entrypoints matching'
+ r" .*? returned a value."
+ ),
+ ):
+ format_version(version)
diff --git a/testing/wd_wrapper.py b/testing/wd_wrapper.py
index 960bf44c..e1aa6c4f 100644
--- a/testing/wd_wrapper.py
+++ b/testing/wd_wrapper.py
@@ -1,6 +1,7 @@
from __future__ import annotations
import itertools
+
from pathlib import Path
from typing import Any
diff --git a/tox.ini b/tox.ini
index 87affcf0..049f2a77 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,5 +1,5 @@
[tox]
-envlist=py{38,39,310,311},check_readme,check-dist
+envlist=py{38,39,310,311,312},check_readme,check-dist
requires= tox>4
[flake8]
@@ -9,11 +9,7 @@ ignore=E203,W503
[testenv]
usedevelop=True
-deps=
- pytest
- setuptools >= 45
- rich
- build
+extras=test
commands=
python -X warn_default_encoding -m pytest {posargs}