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 -[![github ci](https://github.com/pypa/setuptools_scm/workflows/python%20tests+artifacts+release/badge.svg)](https://github.com/pypa/setuptools_scm/actions) +# setuptools-scm +[![github ci](https://github.com/pypa/setuptools-scm/actions/workflows/python-tests.yml/badge.svg)](https://github.com/pypa/setuptools-scm/actions/workflows/python-tests.yml) [![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
-(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}