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} pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy