diff --git a/.github/workflows/python-tests.yml b/.github/workflows/python-tests.yml index 27533332..af4126bd 100644 --- a/.github/workflows/python-tests.yml +++ b/.github/workflows/python-tests.yml @@ -85,30 +85,6 @@ jobs: architecture: x64 - run: pip install -e .[toml,test] pytest virtualenv - run: pytest --test-legacy testing/test_setuptools_support.py || true # ignore fail flaky on ci - check_selfinstall: - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - python_version: [ '3.7', '3.9', 'pypy-3.8' ] - installer: ["pip install"] - name: check self install - Python ${{ matrix.python_version }} via ${{ matrix.installer }} - steps: - - uses: actions/checkout@v3 - - name: Setup python - uses: actions/setup-python@v4 - with: - python-version: ${{ matrix.python_version }} - architecture: x64 - # self install testing needs some clarity - # so its being executed without any other tools running - # setuptools smaller 52 is needed to do easy_install - - run: pip install -U "setuptools<52" tomli packaging typing_extensions importlib_metadata - - run: python setup.py egg_info - - run: python setup.py sdist - - run: ${{ matrix.installer }} dist/* - - run: python testing/check_self_install.py - dist: runs-on: ubuntu-latest diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f6294005..ced36e7d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,7 +2,7 @@ default_language_version: python: python3.9 repos: - repo: https://github.com/psf/black - rev: 22.12.0 + rev: 23.1.0 hooks: - id: black args: [--safe, --quiet] @@ -27,13 +27,13 @@ repos: hooks: - id: pyupgrade args: [--py37-plus] -- repo: https://github.com/asottile/setup-cfg-fmt - rev: v2.2.0 - hooks: - - id: setup-cfg-fmt - args: [ --include-version-classifiers ] +- repo: https://github.com/tox-dev/pyproject-fmt + rev: "0.8.0" + hooks: + - id: pyproject-fmt + - repo: https://github.com/pre-commit/mirrors-mypy - rev: 'v0.991' + rev: 'v1.0.0' hooks: - id: mypy args: [--strict] diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 3c731cc0..456c281b 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,3 +1,29 @@ +v8.0.0 +====== + + +breaking +-------- +* remove legacy version parser api - config arg always required +* turn Configuration into a dataclass +* require confiuration to always pass into helpers + +features +-------- + +* git: expect main as possible default branch +* drop version_from_scm helper +* trim down exposed public api +* no longer self-call twice in setuptools +* chores + + * migrate own metadata to pyproject.toml + * consolidate version schemes + * stricter tag typing + * pre-compiled regex + * move helpers to private modules + + v7.1.0 ====== @@ -45,7 +71,7 @@ v7.0.0 * drop python 3.6 support * include git archival support -* fix #707: support git version detection even when git protects against mistmatched owners +* fix #707: support git version detection even when git protects against mismatched owners (common with misconfigured containers, thanks @chrisburr ) v6.4.3 @@ -410,7 +436,7 @@ v1.16.0 * avoid shlex.split on windows * fix #218 - better handling of mercurial edge-cases with tag commits being considered as the tagged commit -* fix #223 - remove the dependency on the interal SetupttoolsVersion +* fix #223 - remove the dependency on the internal ``SetuptoolsVersion`` as it was removed after long-standing deprecation v1.15.7 @@ -612,20 +638,20 @@ v1.5.0 * moved setuptools integration related code to own file * support storing version strings into a module/text file - using the :code:`write_to` coniguration parameter + using the :code:`write_to` configuration parameter v1.4.0 ====== * proper handling for sdist * fix file-finder failure from windows -* resuffle docs +* reshuffle docs v1.3.0 ====== * support setuptools easy_install egg creation details - by hardwireing the version in the sdist + by hardwire-ing the version in the sdist v1.2.0 ====== diff --git a/MANIFEST.in b/MANIFEST.in index c758a17a..54e9473e 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -8,5 +8,6 @@ include *.rst include LICENSE include *.toml include mypy.ini -include testing/Dockerfile.busted-buster +include testing/Dockerfile.* +include src/setuptools_scm/.git_archival.txt recursive-include testing *.bash diff --git a/mypy.ini b/mypy.ini index fb092111..9b387952 100644 --- a/mypy.ini +++ b/mypy.ini @@ -3,8 +3,4 @@ python_version = 3.7 warn_return_any = True warn_unused_configs = True mypy_path = $MYPY_CONFIG_FILE_DIR/src - -[mypy-setuptools_scm.*] -# disabled as it will take a bit -# disallow_untyped_defs = True strict = true diff --git a/nextgen/vcs-versioning/LICENSE.txt b/nextgen/vcs-versioning/LICENSE.txt new file mode 100644 index 00000000..5b48e3b8 --- /dev/null +++ b/nextgen/vcs-versioning/LICENSE.txt @@ -0,0 +1,9 @@ +MIT License + +Copyright (c) 2023-present Ronny Pfannschmidt + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/nextgen/vcs-versioning/README.md b/nextgen/vcs-versioning/README.md new file mode 100644 index 00000000..78fe5d7d --- /dev/null +++ b/nextgen/vcs-versioning/README.md @@ -0,0 +1,21 @@ +# vcs-versioning + +[![PyPI - Version](https://img.shields.io/pypi/v/vcs-versioning.svg)](https://pypi.org/project/vcs-versioning) +[![PyPI - Python Version](https://img.shields.io/pypi/pyversions/vcs-versioning.svg)](https://pypi.org/project/vcs-versioning) + +----- + +**Table of Contents** + +- [Installation](#installation) +- [License](#license) + +## Installation + +```console +pip install vcs-versioning +``` + +## License + +`vcs-versioning` is distributed under the terms of the [MIT](https://spdx.org/licenses/MIT.html) license. diff --git a/nextgen/vcs-versioning/pyproject.toml b/nextgen/vcs-versioning/pyproject.toml new file mode 100644 index 00000000..c7e50972 --- /dev/null +++ b/nextgen/vcs-versioning/pyproject.toml @@ -0,0 +1,64 @@ +[build-system] +build-backend = "hatchling.build" +requires = [ + "hatchling", +] + +[project] +name = "vcs-versioning" +description = "the blessed package to manage your versions by vcs metadata" +readme = "README.md" +keywords = [ +] +license = "MIT" +authors = [ + { name = "Ronny Pfannschmidt", email = "opensource@ronnypfannschmidt.de" }, +] +requires-python = ">=3.8" +classifiers = [ + "Development Status :: 1 - Planning", + "Programming Language :: Python", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", +] +dynamic = [ + "version", +] +dependencies = [ +] +[project.urls] +Documentation = "https://github.com/unknown/vcs-versioning#readme" +Issues = "https://github.com/unknown/vcs-versioning/issues" +Source = "https://github.com/unknown/vcs-versioning" + + +[tool.hatch.version] +path = "vcs_versioning/__about__.py" + +[tool.hatch.envs.default] +dependencies = [ + "pytest", + "pytest-cov", +] +[tool.hatch.envs.default.scripts] +cov = "pytest --cov-report=term-missing --cov-config=pyproject.toml --cov=vcs_versioning --cov=tests {args}" +no-cov = "cov --no-cov {args}" + +[[tool.hatch.envs.test.matrix]] +python = ["38", "39", "310", "311"] + +[tool.coverage.run] +branch = true +parallel = true +omit = [ + "vcs_versioning/__about__.py", +] + +[tool.coverage.report] +exclude_lines = [ + "no cov", + "if __name__ == .__main__.:", + "if TYPE_CHECKING:", +] diff --git a/nextgen/vcs-versioning/tests/__init__.py b/nextgen/vcs-versioning/tests/__init__.py new file mode 100644 index 00000000..9d48db4f --- /dev/null +++ b/nextgen/vcs-versioning/tests/__init__.py @@ -0,0 +1 @@ +from __future__ import annotations diff --git a/nextgen/vcs-versioning/vcs_versioning/__about__.py b/nextgen/vcs-versioning/vcs_versioning/__about__.py new file mode 100644 index 00000000..eba4921f --- /dev/null +++ b/nextgen/vcs-versioning/vcs_versioning/__about__.py @@ -0,0 +1,3 @@ +from __future__ import annotations + +__version__ = "0.0.1" diff --git a/nextgen/vcs-versioning/vcs_versioning/__init__.py b/nextgen/vcs-versioning/vcs_versioning/__init__.py new file mode 100644 index 00000000..9d48db4f --- /dev/null +++ b/nextgen/vcs-versioning/vcs_versioning/__init__.py @@ -0,0 +1 @@ +from __future__ import annotations diff --git a/pyproject.toml b/pyproject.toml index 1d11e4ca..93a1e44d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,94 @@ [build-system] +build-backend = "setuptools.build_meta" requires = [ - "setuptools>=45", - "packaging>=20.0", - "typing_extensions", + "packaging>=20", + "setuptools>=55", + "typing_extensions", ] -build-backend = "setuptools.build_meta" + +[project] +name = "setuptools-scm" +description = "the blessed package to manage your versions by scm tags" +readme = "README.rst" +license.file = "LICENSE" +authors = [ + {name="Ronny Pfannschmidt", email="opensource@ronnypfannschmidt.de"} +] +requires-python = ">=3.7" +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Topic :: Software Development :: Libraries", + "Topic :: Software Development :: Version Control", + "Topic :: System :: Software Distribution", + "Topic :: Utilities", +] +dynamic = [ + "version", +] +dependencies = [ + 'importlib-metadata; python_version < "3.8"', + "packaging>=20", + "setuptools", + 'tomli>=1; python_version < "3.11"', + "typing-extensions", +] +[project.urls] +repository = "https://github.com/pypa/setuptools_scm/" + +[project.entry-points."distutils.setup_keywords"] +use_scm_version = "setuptools_scm.integration:version_keyword" + +[project.entry-points."setuptools.file_finders"] +setuptools_scm = "setuptools_scm.integration:find_files" + +[project.entry-points."setuptools.finalize_distribution_options"] +setuptools_scm = "setuptools_scm.integration:infer_version" + +[project.entry-points."setuptools_scm.files_command"] +".git" = "setuptools_scm.file_finder_git:git_find_files" +".hg" = "setuptools_scm.file_finder_hg:hg_find_files" + +[project.entry-points."setuptools_scm.files_command_fallback"] +".git_archival.txt" = "setuptools_scm.file_finder_git:git_archive_find_files" +".hg_archival.txt" = "setuptools_scm.file_finder_hg:hg_archive_find_files" + +[project.entry-points."setuptools_scm.local_scheme"] +dirty-tag = "setuptools_scm.version:get_local_dirty_tag" +no-local-version = "setuptools_scm.version:get_no_local_node" +node-and-date = "setuptools_scm.version:get_local_node_and_date" +node-and-timestamp = "setuptools_scm.version:get_local_node_and_timestamp" + +[project.entry-points."setuptools_scm.parse_scm"] +".git" = "setuptools_scm.git:parse" +".hg" = "setuptools_scm.hg:parse" + +[project.entry-points."setuptools_scm.parse_scm_fallback"] +".git_archival.txt" = "setuptools_scm.git:parse_archival" +".hg_archival.txt" = "setuptools_scm.hg:parse_archival" +PKG-INFO = "setuptools_scm.hacks:parse_pkginfo" +pip-egg-info = "setuptools_scm.hacks:parse_pip_egg_info" +"pyproject.toml" = "setuptools_scm.hacks:fallback_version" +"setup.py" = "setuptools_scm.hacks:fallback_version" + +[project.entry-points."setuptools_scm.version_scheme"] +"calver-by-date" = "setuptools_scm.version:calver_by_date" +"guess-next-dev" = "setuptools_scm.version:guess_next_dev_version" +"no-guess-dev" = "setuptools_scm.version:no_guess_dev_version" +"post-release" = "setuptools_scm.version:postrelease_version" +"python-simplified-semver" = "setuptools_scm.version:simplified_semver_version" +"release-branch-semver" = "setuptools_scm.version:release_branch_semver_version" + + + +[tool.setuptools.packages.find] +where = ["src"] diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 795bb218..00000000 --- a/setup.cfg +++ /dev/null @@ -1,78 +0,0 @@ -[metadata] -name = setuptools_scm -description = the blessed package to manage your versions by scm tags -long_description = file: README.rst -long_description_content_type = text/x-rst -url = https://github.com/pypa/setuptools_scm/ -author = Ronny Pfannschmidt -author_email = opensource@ronnypfannschmidt.de -license = MIT -license_file = LICENSE -classifiers = - Development Status :: 5 - Production/Stable - Intended Audience :: Developers - License :: OSI Approved :: MIT License - Programming Language :: Python - Programming Language :: Python :: 3 - Programming Language :: Python :: 3 :: Only - Programming Language :: Python :: 3.7 - Programming Language :: Python :: 3.8 - Programming Language :: Python :: 3.9 - Programming Language :: Python :: 3.10 - Programming Language :: Python :: 3.11 - Topic :: Software Development :: Libraries - Topic :: Software Development :: Version Control - Topic :: System :: Software Distribution - Topic :: Utilities - -[options] -packages = find: -install_requires = - packaging>=20.0 - setuptools - typing-extensions - importlib-metadata;python_version < '3.8' - tomli>=1.0.0;python_version < '3.11' # keep in sync -python_requires = >=3.7 -package_dir = - =src -zip_safe = true - -[options.packages.find] -where = src - -[options.entry_points] -distutils.setup_keywords = - use_scm_version = setuptools_scm.integration:version_keyword -setuptools.file_finders = - setuptools_scm = setuptools_scm.integration:find_files -setuptools.finalize_distribution_options = - setuptools_scm = setuptools_scm.integration:infer_version -setuptools_scm.files_command = - .hg = setuptools_scm.file_finder_hg:hg_find_files - .git = setuptools_scm.file_finder_git:git_find_files -setuptools_scm.files_command_fallback = - .hg_archival.txt = setuptools_scm.file_finder_hg:hg_archive_find_files - .git_archival.txt = setuptools_scm.file_finder_git:git_archive_find_files -setuptools_scm.local_scheme = - node-and-date = setuptools_scm.version:get_local_node_and_date - node-and-timestamp = setuptools_scm.version:get_local_node_and_timestamp - dirty-tag = setuptools_scm.version:get_local_dirty_tag - no-local-version = setuptools_scm.version:get_no_local_node -setuptools_scm.parse_scm = - .hg = setuptools_scm.hg:parse - .git = setuptools_scm.git:parse -setuptools_scm.parse_scm_fallback = - .hg_archival.txt = setuptools_scm.hg:parse_archival - .git_archival.txt = setuptools_scm.git:parse_archival - PKG-INFO = setuptools_scm.hacks:parse_pkginfo - pip-egg-info = setuptools_scm.hacks:parse_pip_egg_info - setup.py = setuptools_scm.hacks:fallback_version - pyproject.toml = setuptools_scm.hacks:fallback_version -setuptools_scm.version_scheme = - guess-next-dev = setuptools_scm.version:guess_next_dev_version - post-release = setuptools_scm.version:postrelease_version - python-simplified-semver = setuptools_scm.version:simplified_semver_version - release-branch-semver = setuptools_scm.version:release_branch_semver_version - no-guess-dev = setuptools_scm.version:no_guess_dev_version - calver-by-date = setuptools_scm.version:calver_by_date diff --git a/setup.py b/setup.py index 66670033..124b9447 100644 --- a/setup.py +++ b/setup.py @@ -28,7 +28,7 @@ def scm_version() -> str: from setuptools_scm import git from setuptools_scm import hg from setuptools_scm.version import guess_next_dev_version, get_local_node_and_date - from setuptools_scm.config import Configuration + from setuptools_scm import Configuration from setuptools_scm.version import ScmVersion diff --git a/src/setuptools_scm/__init__.py b/src/setuptools_scm/__init__.py index aeb4ab6a..d6def65e 100644 --- a/src/setuptools_scm/__init__.py +++ b/src/setuptools_scm/__init__.py @@ -5,33 +5,27 @@ from __future__ import annotations import os -import warnings +import re from typing import Any -from typing import Callable +from typing import Pattern from typing import TYPE_CHECKING -from ._entrypoints import _call_entrypoint_fn +from ._config import Configuration +from ._config import DEFAULT_LOCAL_SCHEME +from ._config import DEFAULT_TAG_REGEX +from ._config import DEFAULT_VERSION_SCHEME from ._entrypoints import _version_from_entrypoints from ._overrides import _read_pretended_version_for from ._overrides import PRETEND_KEY from ._overrides import PRETEND_KEY_NAMED +from ._version_cls import _validate_version_cls from ._version_cls import _version_as_tuple from ._version_cls import NonNormalizedVersion from ._version_cls import Version -from .config import Configuration -from .config import DEFAULT_LOCAL_SCHEME -from .config import DEFAULT_TAG_REGEX -from .config import DEFAULT_VERSION_SCHEME -from .discover import iter_matching_entrypoints -from .utils import function_has_arg -from .utils import trace -from .version import format_version -from .version import meta -from .version import ScmVersion +from .version import format_version as _format_version if TYPE_CHECKING: from typing import NoReturn - from . import _types as _t TEMPLATES = { @@ -45,16 +39,6 @@ } -def version_from_scm(root: _t.PathT) -> ScmVersion | None: - warnings.warn( - "version_from_scm is deprecated please use get_version", - category=DeprecationWarning, - stacklevel=2, - ) - config = Configuration(root=root) - return _version_from_entrypoints(config) - - def dump_version( root: _t.PathT, version: str, @@ -65,7 +49,9 @@ def dump_version( target = os.path.normpath(os.path.join(root, write_to)) ext = os.path.splitext(target)[1] template = template or TEMPLATES.get(ext) + from .utils import trace + trace("dump", write_to, version) if template is None: raise ValueError( "bad file format: '{}' (of {}) \nonly *.txt and *.py are supported".format( @@ -78,30 +64,32 @@ def dump_version( fp.write(template.format(version=version, version_tuple=version_tuple)) -def _do_parse(config: Configuration) -> ScmVersion | None: +def _do_parse(config: Configuration) -> _t.SCMVERSION | None: + from .version import ScmVersion + pretended = _read_pretended_version_for(config) if pretended is not None: return pretended - + parsed_version: ScmVersion | None if config.parse: - parse_result = _call_entrypoint_fn(config.absolute_root, config, config.parse) + parse_result = config.parse(config.absolute_root, config=config) if isinstance(parse_result, str): raise TypeError( f"version parse result was {str!r}\nplease return a parsed version" ) - version: ScmVersion | None + if parse_result: assert isinstance(parse_result, ScmVersion) - version = parse_result + parsed_version = parse_result else: - version = _version_from_entrypoints(config, fallback=True) + parsed_version = _version_from_entrypoints(config, fallback=True) else: # include fallbacks after dropping them from the main entrypoint - version = _version_from_entrypoints(config) or _version_from_entrypoints( + parsed_version = _version_from_entrypoints(config) or _version_from_entrypoints( config, fallback=True ) - return version + return parsed_version def _version_missing(config: Configuration) -> NoReturn: @@ -119,17 +107,17 @@ def _version_missing(config: Configuration) -> NoReturn: def get_version( root: str = ".", - version_scheme: Callable[[ScmVersion], str] | str = DEFAULT_VERSION_SCHEME, - local_scheme: Callable[[ScmVersion], str] | str = DEFAULT_LOCAL_SCHEME, + version_scheme: _t.VERSION_SCHEME = DEFAULT_VERSION_SCHEME, + local_scheme: _t.VERSION_SCHEME = DEFAULT_LOCAL_SCHEME, write_to: _t.PathT | None = None, write_to_template: str | None = None, relative_to: str | None = None, - tag_regex: str = DEFAULT_TAG_REGEX, + tag_regex: str | Pattern[str] = DEFAULT_TAG_REGEX, parentdir_prefix_version: str | None = None, fallback_version: str | None = None, fallback_root: _t.PathT = ".", parse: Any | None = None, - git_describe_command: Any | None = None, + git_describe_command: _t.CMD_TYPE | None = None, dist_name: str | None = None, version_cls: Any | None = None, normalize: bool = True, @@ -141,9 +129,13 @@ def get_version( in the root of the repository to direct setuptools_scm to the root of the repository by supplying ``__file__``. """ - + version_cls = _validate_version_cls(version_cls, normalize) + del normalize + if isinstance(tag_regex, str): + tag_regex = re.compile(tag_regex) config = Configuration(**locals()) maybe_version = _get_version(config) + if maybe_version is None: _version_missing(config) return maybe_version @@ -153,7 +145,7 @@ def _get_version(config: Configuration) -> str | None: parsed_version = _do_parse(config) if parsed_version is None: return None - version_string = format_version( + version_string = _format_version( parsed_version, version_scheme=config.version_scheme, local_scheme=config.local_scheme, @@ -173,7 +165,6 @@ def _get_version(config: Configuration) -> str | None: __all__ = [ "get_version", "dump_version", - "version_from_scm", "Configuration", "DEFAULT_VERSION_SCHEME", "DEFAULT_LOCAL_SCHEME", @@ -182,10 +173,4 @@ def _get_version(config: Configuration) -> str | None: "PRETEND_KEY_NAMED", "Version", "NonNormalizedVersion", - # TODO: are the symbols below part of public API ? - "function_has_arg", - "trace", - "format_version", - "meta", - "iter_matching_entrypoints", ] diff --git a/src/setuptools_scm/_cli.py b/src/setuptools_scm/_cli.py index 8e01f245..953c4719 100644 --- a/src/setuptools_scm/_cli.py +++ b/src/setuptools_scm/_cli.py @@ -5,7 +5,7 @@ import sys from setuptools_scm import _get_version -from setuptools_scm.config import Configuration +from setuptools_scm import Configuration from setuptools_scm.discover import walk_potential_roots from setuptools_scm.integration import find_files @@ -17,7 +17,6 @@ def main(args: list[str] | None = None) -> None: pyproject = opts.config or _find_pyproject(inferred_root) try: - config = Configuration.from_file( pyproject, root=(os.path.abspath(opts.root) if opts.root is not None else None), diff --git a/src/setuptools_scm/_config.py b/src/setuptools_scm/_config.py new file mode 100644 index 00000000..84f72276 --- /dev/null +++ b/src/setuptools_scm/_config.py @@ -0,0 +1,132 @@ +""" configuration """ +from __future__ import annotations + +import dataclasses +import os +import re +import warnings +from typing import Any +from typing import Callable +from typing import Pattern + +from . import _types as _t +from ._integration.pyproject_reading import ( + get_args_for_pyproject as _get_args_for_pyproject, +) +from ._integration.pyproject_reading import read_pyproject as _read_pyproject +from ._overrides import read_toml_overrides +from ._version_cls import _validate_version_cls +from ._version_cls import _VersionT +from ._version_cls import Version as _Version +from .utils import trace + +DEFAULT_TAG_REGEX = re.compile( + r"^(?:[\w-]+-)?(?P[vV]?\d+(?:\.\d+){0,2}[^\+]*)(?:\+.*)?$" +) +DEFAULT_VERSION_SCHEME = "guess-next-dev" +DEFAULT_LOCAL_SCHEME = "node-and-date" + + +def _check_tag_regex(value: str | Pattern[str] | None) -> Pattern[str]: + if not value: + regex = DEFAULT_TAG_REGEX + else: + regex = re.compile(value) + + group_names = regex.groupindex.keys() + if regex.groups == 0 or (regex.groups > 1 and "version" not in group_names): + warnings.warn( + "Expected tag_regex to contain a single match group or a group named" + " 'version' to identify the version part of any tag." + ) + + return regex + + +def _check_absolute_root(root: _t.PathT, relative_to: _t.PathT | None) -> str: + trace("abs root", repr(locals())) + if relative_to: + if ( + os.path.isabs(root) + and os.path.isabs(relative_to) + and not os.path.commonpath([root, relative_to]) == root + ): + warnings.warn( + "absolute root path '%s' overrides relative_to '%s'" + % (root, relative_to) + ) + if os.path.isdir(relative_to): + warnings.warn( + "relative_to is expected to be a file," + " its the directory %r\n" + "assuming the parent directory was passed" % (relative_to,) + ) + trace("dir", relative_to) + root = os.path.join(relative_to, root) + else: + trace("file", relative_to) + root = os.path.join(os.path.dirname(relative_to), root) + return os.path.abspath(root) + + +@dataclasses.dataclass +class Configuration: + """Global configuration model""" + + relative_to: _t.PathT | None = None + root: _t.PathT = "." + version_scheme: _t.VERSION_SCHEME = DEFAULT_VERSION_SCHEME + local_scheme: _t.VERSION_SCHEME = DEFAULT_LOCAL_SCHEME + tag_regex: Pattern[str] = DEFAULT_TAG_REGEX + parentdir_prefix_version: str | None = None + fallback_version: str | None = None + fallback_root: _t.PathT = "." + write_to: _t.PathT | None = None + write_to_template: str | None = None + parse: Any | None = None + git_describe_command: _t.CMD_TYPE | None = None + dist_name: str | None = None + version_cls: type[_VersionT] = _Version + search_parent_directories: bool = False + + parent: _t.PathT | None = None + + @property + def absolute_root(self) -> str: + return _check_absolute_root(self.root, self.relative_to) + + @classmethod + def from_file( + cls, + name: str | os.PathLike[str] = "pyproject.toml", + dist_name: str | None = None, + _load_toml: Callable[[str], dict[str, Any]] | None = None, + **kwargs: Any, + ) -> Configuration: + """ + Read Configuration from pyproject.toml (or similar). + Raises exceptions when file is not found or toml is + not installed or the file has invalid format or does + not contain the [tool.setuptools_scm] section. + """ + + pyproject_data = _read_pyproject(name, _load_toml=_load_toml) + args = _get_args_for_pyproject(pyproject_data, dist_name, kwargs) + + args.update(read_toml_overrides(args["dist_name"])) + return cls.from_data(relative_to=name, data=args) + + @classmethod + def from_data( + cls, relative_to: str | os.PathLike[str], data: dict[str, Any] + ) -> Configuration: + tag_regex = _check_tag_regex(data.pop("tag_regex", None)) + version_cls = _validate_version_cls( + data.pop("version_cls", None), data.pop("normalize", True) + ) + return cls( + relative_to, + version_cls=version_cls, + tag_regex=tag_regex, + **data, + ) diff --git a/src/setuptools_scm/_entrypoints.py b/src/setuptools_scm/_entrypoints.py index 2efb9f8a..c236434b 100644 --- a/src/setuptools_scm/_entrypoints.py +++ b/src/setuptools_scm/_entrypoints.py @@ -2,16 +2,17 @@ import warnings 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 .utils import function_has_arg +from . import version from .utils import trace -from .version import ScmVersion if TYPE_CHECKING: - from .config import Configuration + from ._config import Configuration from typing_extensions import Protocol from . import _types as _t else: @@ -21,37 +22,9 @@ class Protocol: pass -class MaybeConfigFunction(Protocol): - __name__: str - - @overload - def __call__(self, root: _t.PathT, config: Configuration) -> ScmVersion | None: - pass - - @overload - def __call__(self, root: _t.PathT) -> ScmVersion | None: - pass - - -def _call_entrypoint_fn( - root: _t.PathT, config: Configuration, fn: MaybeConfigFunction -) -> ScmVersion | None: - if function_has_arg(fn, "config"): - return fn(root, config=config) - else: - warnings.warn( - f"parse function {fn.__module__}.{fn.__name__}" - " are required to provide a named argument" - " 'config', setuptools_scm>=8.0 will remove support.", - category=DeprecationWarning, - stacklevel=2, - ) - return fn(root) - - def _version_from_entrypoints( config: Configuration, fallback: bool = False -) -> ScmVersion | None: +) -> version.ScmVersion | None: if fallback: entrypoint = "setuptools_scm.parse_scm_fallback" root = config.fallback_root @@ -63,10 +36,11 @@ def _version_from_entrypoints( trace("version_from_ep", entrypoint, root) for ep in iter_matching_entrypoints(root, entrypoint, config): - version: ScmVersion | None = _call_entrypoint_fn(root, config, ep.load()) + fn = ep.load() + maybe_version: version.ScmVersion | None = fn(root, config=config) trace(ep, version) - if version: - return version + if maybe_version is not None: + return maybe_version return None @@ -97,3 +71,59 @@ def iter_entry_points( if name is None: return iter(eps) return (ep for ep in eps if ep.name == name) + + +def _get_ep(group: str, name: str) -> Any | None: + from ._entrypoints import iter_entry_points + + for ep in iter_entry_points(group, name): + trace("ep found:", ep.name) + return ep.load() + else: + return None + + +def _iter_version_schemes( + entrypoint: str, + scheme_value: _t.VERSION_SCHEMES, + _memo: set[object] | None = None, +) -> Iterator[Callable[[version.ScmVersion], str]]: + if _memo is None: + _memo = set() + if isinstance(scheme_value, str): + scheme_value = cast( + "_t.VERSION_SCHEMES", + _get_ep(entrypoint, scheme_value), + ) + + if isinstance(scheme_value, (list, tuple)): + for variant in scheme_value: + if variant not in _memo: + _memo.add(variant) + yield from _iter_version_schemes(entrypoint, variant, _memo=_memo) + elif callable(scheme_value): + yield scheme_value + + +@overload +def _call_version_scheme( + version: version.ScmVersion, entypoint: str, given_value: str, default: str +) -> str: + ... + + +@overload +def _call_version_scheme( + version: version.ScmVersion, entypoint: str, given_value: str, default: None +) -> str | None: + ... + + +def _call_version_scheme( + version: version.ScmVersion, entypoint: str, given_value: str, default: str | None +) -> str | None: + for scheme in _iter_version_schemes(entypoint, given_value): + result = scheme(version) + if result is not None: + return result + return default diff --git a/src/setuptools_scm/_integration/pyproject_reading.py b/src/setuptools_scm/_integration/pyproject_reading.py index d9208f1a..8730c0ec 100644 --- a/src/setuptools_scm/_integration/pyproject_reading.py +++ b/src/setuptools_scm/_integration/pyproject_reading.py @@ -1,17 +1,16 @@ from __future__ import annotations +import os import sys import warnings from typing import Any from typing import Callable from typing import Dict from typing import NamedTuple -from typing import TYPE_CHECKING -from .setuptools import read_dist_name_from_setup_cfg +from typing_extensions import TypeAlias -if TYPE_CHECKING: - from typing_extensions import TypeAlias +from .setuptools import read_dist_name_from_setup_cfg _ROOT = "root" TOML_RESULT: TypeAlias = Dict[str, Any] @@ -19,7 +18,7 @@ class PyProjectData(NamedTuple): - name: str + name: str | os.PathLike[str] tool_name: str project: TOML_RESULT section: TOML_RESULT @@ -39,7 +38,7 @@ def lazy_toml_load(data: str) -> TOML_RESULT: def read_pyproject( - name: str = "pyproject.toml", + name: str | os.PathLike[str] = "pyproject.toml", tool_name: str = "setuptools_scm", _load_toml: TOML_LOADER | None = None, ) -> PyProjectData: diff --git a/src/setuptools_scm/_integration/setuptools.py b/src/setuptools_scm/_integration/setuptools.py index 306ff73a..21f9591f 100644 --- a/src/setuptools_scm/_integration/setuptools.py +++ b/src/setuptools_scm/_integration/setuptools.py @@ -7,7 +7,6 @@ def read_dist_name_from_setup_cfg( input: str | os.PathLike[str] | IO[str] = "setup.cfg", ) -> str | None: - # minimal effort to read dist_name off setup.cfg metadata import configparser diff --git a/src/setuptools_scm/_modify_version.py b/src/setuptools_scm/_modify_version.py new file mode 100644 index 00000000..a364adc1 --- /dev/null +++ b/src/setuptools_scm/_modify_version.py @@ -0,0 +1,61 @@ +from __future__ import annotations + +import re + +from . import _types as _t + + +def _strip_local(version_string: str) -> str: + public, sep, local = version_string.partition("+") + return public + + +def _add_post(version: str) -> str: + if "post" in version: + raise ValueError( + f"{version} already is a post release, refusing to guess the update" + ) + return f"{version}.post1" + + +def _bump_dev(version: str) -> str | None: + if ".dev" not in version: + return None + + prefix, tail = version.rsplit(".dev", 1) + if tail != "0": + raise ValueError( + "choosing custom numbers for the `.devX` distance " + "is not supported.\n " + f"The {version} can't be bumped\n" + "Please drop the tag or create a new supported one ending in .dev0" + ) + return prefix + + +def _bump_regex(version: str) -> str: + match = re.match(r"(.*?)(\d+)$", version) + if match is None: + raise ValueError( + "{version} does not end with a number to bump, " + "please correct or use a custom version scheme".format(version=version) + ) + else: + prefix, tail = match.groups() + return "%s%d" % (prefix, int(tail) + 1) + + +def _format_local_with_time(version: _t.SCMVERSION, time_format: str) -> str: + if version.exact or version.node is None: + return version.format_choice( + "", "+d{time:{time_format}}", time_format=time_format + ) + else: + return version.format_choice( + "+{node}", "+{node}.d{time:{time_format}}", time_format=time_format + ) + + +def _dont_guess_next_version(tag_version: _t.SCMVERSION) -> str: + version = _strip_local(str(tag_version.tag)) + return _bump_dev(version) or _add_post(version) diff --git a/src/setuptools_scm/_overrides.py b/src/setuptools_scm/_overrides.py index f18b82c0..90404877 100644 --- a/src/setuptools_scm/_overrides.py +++ b/src/setuptools_scm/_overrides.py @@ -1,38 +1,54 @@ from __future__ import annotations import os +from typing import Any -from .config import Configuration +from . import _config +from . import version +from ._integration.pyproject_reading import lazy_toml_load from .utils import trace -from .version import meta -from .version import ScmVersion - PRETEND_KEY = "SETUPTOOLS_SCM_PRETEND_VERSION" PRETEND_KEY_NAMED = PRETEND_KEY + "_FOR_{name}" -def _read_pretended_version_for(config: Configuration) -> ScmVersion | None: +def read_named_env( + *, tool: str = "SETUPTOOLS_SCM", name: str, dist_name: str | None +) -> str | None: + if dist_name is not None: + val = os.environ.get(f"{tool}_{name}_FOR_{dist_name.upper()}") + if val is not None: + return val + return os.environ.get(f"{tool}_{name}") + + +def _read_pretended_version_for( + config: _config.Configuration, +) -> version.ScmVersion | None: """read a a overridden version from the environment tries ``SETUPTOOLS_SCM_PRETEND_VERSION`` and ``SETUPTOOLS_SCM_PRETEND_VERSION_FOR_$UPPERCASE_DIST_NAME`` """ trace("dist name:", config.dist_name) - pretended: str | None - if config.dist_name is not None: - pretended = os.environ.get( - PRETEND_KEY_NAMED.format(name=config.dist_name.upper()) - ) - else: - pretended = None - if pretended is None: - pretended = os.environ.get(PRETEND_KEY) + pretended = read_named_env(name="PRETEND_VERSION", dist_name=config.dist_name) if pretended: # we use meta here since the pretended version # must adhere to the pep to begin with - return meta(tag=pretended, preformatted=True, config=config) + return version.meta(tag=pretended, preformatted=True, config=config) else: return None + + +def read_toml_overrides(dist_name: str | None) -> dict[str, Any]: + data = read_named_env(name="OVERRIDES", dist_name=dist_name) + if data: + if data[0] == "{": + data = "cheat=" + data + loaded = lazy_toml_load(data) + return loaded["cheat"] # type: ignore[no-any-return] + return lazy_toml_load(data) + else: + return {} diff --git a/src/setuptools_scm/_types.py b/src/setuptools_scm/_types.py index 6c6bdf85..5c2cd334 100644 --- a/src/setuptools_scm/_types.py +++ b/src/setuptools_scm/_types.py @@ -1,24 +1,26 @@ from __future__ import annotations +import os from typing import Any from typing import Callable from typing import List -from typing import TYPE_CHECKING +from typing import Tuple from typing import TypeVar from typing import Union +from typing_extensions import ParamSpec +from typing_extensions import Protocol +from typing_extensions import TypeAlias -if TYPE_CHECKING: - from setuptools_scm import version - import os +from . import version -from typing_extensions import ParamSpec, TypeAlias, Protocol - -PathT = Union["os.PathLike[str]", str] +PathT: TypeAlias = Union["os.PathLike[str]", str] CMD_TYPE: TypeAlias = Union[List[str], str] -VERSION_SCHEME = Union[str, Callable[["version.ScmVersion"], str]] +VERSION_SCHEME: TypeAlias = Union[str, Callable[["version.ScmVersion"], str]] +VERSION_SCHEMES: TypeAlias = Union[List[str], Tuple[str, ...], VERSION_SCHEME] +SCMVERSION: TypeAlias = "version.ScmVersion" class EntrypointProtocol(Protocol): diff --git a/src/setuptools_scm/_version_cls.py b/src/setuptools_scm/_version_cls.py index 39e66b25..f6e87a8b 100644 --- a/src/setuptools_scm/_version_cls.py +++ b/src/setuptools_scm/_version_cls.py @@ -1,6 +1,9 @@ from __future__ import annotations from logging import getLogger +from typing import cast +from typing import Type +from typing import Union from packaging.version import InvalidVersion from packaging.version import Version as Version @@ -35,7 +38,6 @@ def _version_as_tuple(version_str: str) -> tuple[int | str, ...]: try: parsed_version = Version(version_str) except InvalidVersion: - log = getLogger("setuptools_scm") log.exception("failed to parse version %s", version_str) return (version_str,) @@ -46,3 +48,37 @@ def _version_as_tuple(version_str: str) -> tuple[int | str, ...]: if parsed_version.local is not None: version_fields += (parsed_version.local,) return version_fields + + +_VersionT = Union[Version, NonNormalizedVersion] + + +def import_name(name: str) -> object: + import importlib + + pkg_name, cls_name = name.rsplit(".", 1) + pkg = importlib.import_module(pkg_name) + return getattr(pkg, cls_name) + + +def _validate_version_cls( + version_cls: type[_VersionT] | str | None, normalize: bool +) -> type[_VersionT]: + if not normalize: + if version_cls is not None: + raise ValueError( + "Providing a custom `version_cls` is not permitted when " + "`normalize=False`" + ) + return NonNormalizedVersion + else: + # Use `version_cls` if provided, default to packaging or pkg_resources + if version_cls is None: + return Version + elif isinstance(version_cls, str): + try: + return cast(Type[_VersionT], import_name(version_cls)) + except: # noqa + raise ValueError(f"Unable to import version_cls='{version_cls}'") + else: + return version_cls diff --git a/src/setuptools_scm/config.py b/src/setuptools_scm/config.py deleted file mode 100644 index 3bf250a4..00000000 --- a/src/setuptools_scm/config.py +++ /dev/null @@ -1,216 +0,0 @@ -""" configuration """ -from __future__ import annotations - -import os -import re -import warnings -from typing import Any -from typing import Callable -from typing import cast -from typing import Pattern -from typing import Type -from typing import TYPE_CHECKING -from typing import Union - -from ._integration.pyproject_reading import ( - get_args_for_pyproject as _get_args_for_pyproject, -) -from ._integration.pyproject_reading import read_pyproject as _read_pyproject -from ._version_cls import NonNormalizedVersion -from ._version_cls import Version -from .utils import trace - - -if TYPE_CHECKING: - from . import _types as _t - from setuptools_scm.version import ScmVersion - -DEFAULT_TAG_REGEX = r"^(?:[\w-]+-)?(?P[vV]?\d+(?:\.\d+){0,2}[^\+]*)(?:\+.*)?$" -DEFAULT_VERSION_SCHEME = "guess-next-dev" -DEFAULT_LOCAL_SCHEME = "node-and-date" - - -def _check_tag_regex(value: str | Pattern[str] | None) -> Pattern[str]: - if not value: - value = DEFAULT_TAG_REGEX - regex = re.compile(value) - - group_names = regex.groupindex.keys() - if regex.groups == 0 or (regex.groups > 1 and "version" not in group_names): - warnings.warn( - "Expected tag_regex to contain a single match group or a group named" - " 'version' to identify the version part of any tag." - ) - - return regex - - -def _check_absolute_root(root: _t.PathT, relative_to: _t.PathT | None) -> str: - trace("abs root", repr(locals())) - if relative_to: - if ( - os.path.isabs(root) - and os.path.isabs(relative_to) - and not os.path.commonpath([root, relative_to]) == root - ): - warnings.warn( - "absolute root path '%s' overrides relative_to '%s'" - % (root, relative_to) - ) - if os.path.isdir(relative_to): - warnings.warn( - "relative_to is expected to be a file," - " its the directory %r\n" - "assuming the parent directory was passed" % (relative_to,) - ) - trace("dir", relative_to) - root = os.path.join(relative_to, root) - else: - trace("file", relative_to) - root = os.path.join(os.path.dirname(relative_to), root) - return os.path.abspath(root) - - -_VersionT = Union[Version, NonNormalizedVersion] - - -def _validate_version_cls( - version_cls: type[_VersionT] | str | None, normalize: bool -) -> type[_VersionT]: - if not normalize: - # `normalize = False` means `version_cls = NonNormalizedVersion` - if version_cls is not None: - raise ValueError( - "Providing a custom `version_cls` is not permitted when " - "`normalize=False`" - ) - return NonNormalizedVersion - else: - # Use `version_cls` if provided, default to packaging or pkg_resources - if version_cls is None: - return Version - elif isinstance(version_cls, str): - try: - # Not sure this will work in old python - import importlib - - pkg, cls_name = version_cls.rsplit(".", 1) - version_cls_host = importlib.import_module(pkg) - return cast(Type[_VersionT], getattr(version_cls_host, cls_name)) - except: # noqa - raise ValueError(f"Unable to import version_cls='{version_cls}'") - else: - return version_cls - - -class Configuration: - """Global configuration model""" - - parent: _t.PathT | None - _root: str - _relative_to: str | None - version_cls: type[_VersionT] - - def __init__( - self, - relative_to: _t.PathT | None = None, - root: _t.PathT = ".", - version_scheme: ( - str | Callable[[ScmVersion], str | None] - ) = DEFAULT_VERSION_SCHEME, - local_scheme: (str | Callable[[ScmVersion], str | None]) = DEFAULT_LOCAL_SCHEME, - write_to: _t.PathT | None = None, - write_to_template: str | None = None, - tag_regex: str | Pattern[str] = DEFAULT_TAG_REGEX, - parentdir_prefix_version: str | None = None, - fallback_version: str | None = None, - fallback_root: _t.PathT = ".", - parse: Any | None = None, - git_describe_command: _t.CMD_TYPE | None = None, - dist_name: str | None = None, - version_cls: type[_VersionT] | type | str | None = None, - normalize: bool = True, - search_parent_directories: bool = False, - ): - # TODO: - self._relative_to = None if relative_to is None else os.fspath(relative_to) - self._root = "." - - self.root = os.fspath(root) - self.version_scheme = version_scheme - self.local_scheme = local_scheme - self.write_to = write_to - self.write_to_template = write_to_template - self.parentdir_prefix_version = parentdir_prefix_version - self.fallback_version = fallback_version - self.fallback_root = fallback_root # type: ignore - self.parse = parse - self.tag_regex = tag_regex # type: ignore - self.git_describe_command = git_describe_command - self.dist_name = dist_name - self.search_parent_directories = search_parent_directories - self.parent = None - - self.version_cls = _validate_version_cls(version_cls, normalize) - - @property - def fallback_root(self) -> str: - return self._fallback_root - - @fallback_root.setter - def fallback_root(self, value: _t.PathT) -> None: - self._fallback_root = os.path.abspath(value) - - @property - def absolute_root(self) -> str: - return self._absolute_root - - @property - def relative_to(self) -> str | None: - return self._relative_to - - @relative_to.setter - def relative_to(self, value: _t.PathT) -> None: - self._absolute_root = _check_absolute_root(self._root, value) - self._relative_to = os.fspath(value) - trace("root", repr(self._absolute_root)) - trace("relative_to", repr(value)) - - @property - def root(self) -> str: - return self._root - - @root.setter - def root(self, value: _t.PathT) -> None: - self._absolute_root = _check_absolute_root(value, self._relative_to) - self._root = os.fspath(value) - trace("root", repr(self._absolute_root)) - trace("relative_to", repr(self._relative_to)) - - @property - def tag_regex(self) -> Pattern[str]: - return self._tag_regex - - @tag_regex.setter - def tag_regex(self, value: str | Pattern[str]) -> None: - self._tag_regex = _check_tag_regex(value) - - @classmethod - def from_file( - cls, - name: str = "pyproject.toml", - dist_name: str | None = None, - _load_toml: Callable[[str], dict[str, Any]] | None = None, - **kwargs: Any, - ) -> Configuration: - """ - Read Configuration from pyproject.toml (or similar). - Raises exceptions when file is not found or toml is - not installed or the file has invalid format or does - not contain the [tool.setuptools_scm] section. - """ - - pyproject_data = _read_pyproject(name, _load_toml=_load_toml) - args = _get_args_for_pyproject(pyproject_data, dist_name, kwargs) - - return cls(relative_to=name, **args) diff --git a/src/setuptools_scm/discover.py b/src/setuptools_scm/discover.py index f7843ee8..58384aa4 100644 --- a/src/setuptools_scm/discover.py +++ b/src/setuptools_scm/discover.py @@ -3,11 +3,9 @@ import os from typing import Iterable from typing import Iterator -from typing import TYPE_CHECKING -if TYPE_CHECKING: - from . import _types as _t -from .config import Configuration +from . import _types as _t +from ._config import Configuration from .utils import trace diff --git a/src/setuptools_scm/file_finder.py b/src/setuptools_scm/file_finder.py index f14a946b..27196c0c 100644 --- a/src/setuptools_scm/file_finder.py +++ b/src/setuptools_scm/file_finder.py @@ -1,12 +1,10 @@ from __future__ import annotations import os -from typing import TYPE_CHECKING -if TYPE_CHECKING: - from typing_extensions import TypeGuard - from . import _types as _t +from typing_extensions import TypeGuard +from . import _types as _t from .utils import trace @@ -78,7 +76,9 @@ def is_toplevel_acceptable(toplevel: str | None) -> TypeGuard[str]: if toplevel is None: return False - ignored = os.environ.get("SETUPTOOLS_SCM_IGNORE_VCS_ROOTS", "").split(os.pathsep) + ignored: list[str] = os.environ.get("SETUPTOOLS_SCM_IGNORE_VCS_ROOTS", "").split( + os.pathsep + ) ignored = [os.path.normcase(p) for p in ignored] trace(toplevel, ignored) diff --git a/src/setuptools_scm/file_finder_git.py b/src/setuptools_scm/file_finder_git.py index 775c49da..65aa9997 100644 --- a/src/setuptools_scm/file_finder_git.py +++ b/src/setuptools_scm/file_finder_git.py @@ -5,17 +5,14 @@ import subprocess import tarfile from typing import IO -from typing import TYPE_CHECKING +from . import _types as _t from .file_finder import is_toplevel_acceptable from .file_finder import scm_find_files from .utils import data_from_mime from .utils import do_ex from .utils import trace -if TYPE_CHECKING: - from . import _types as _t - log = logging.getLogger(__name__) diff --git a/src/setuptools_scm/file_finder_hg.py b/src/setuptools_scm/file_finder_hg.py index 2ce974fc..b750feac 100644 --- a/src/setuptools_scm/file_finder_hg.py +++ b/src/setuptools_scm/file_finder_hg.py @@ -2,17 +2,14 @@ import os import subprocess -from typing import TYPE_CHECKING +from . import _types as _t from .file_finder import is_toplevel_acceptable from .file_finder import scm_find_files from .utils import data_from_mime from .utils import do_ex from .utils import trace -if TYPE_CHECKING: - from . import _types as _t - def _hg_toplevel(path: str) -> str | None: try: diff --git a/src/setuptools_scm/git.py b/src/setuptools_scm/git.py index 16ca3789..1859671a 100644 --- a/src/setuptools_scm/git.py +++ b/src/setuptools_scm/git.py @@ -11,7 +11,8 @@ from typing import Callable from typing import TYPE_CHECKING -from .config import Configuration +from . import _types as _t +from . import Configuration from .scm_workdir import Workdir from .utils import _CmdResult from .utils import data_from_mime @@ -20,12 +21,11 @@ from .utils import trace from .version import meta from .version import ScmVersion -from .version import tags_to_versions +from .version import tag_to_version if TYPE_CHECKING: - from . import _types as _t + from . import hg_git - from setuptools_scm.hg_git import GitWorkdirHgClient REF_TAG_RE = re.compile(r"(?<=\btag: )([^,]+)\b") DESCRIBE_UNSUPPORTED = "%(describe" @@ -150,33 +150,30 @@ def fail_on_shallow(wd: GitWorkdir) -> None: ) -def get_working_directory(config: Configuration) -> GitWorkdir | None: +def get_working_directory(config: Configuration, root: str) -> GitWorkdir | None: """ Return the working directory (``GitWorkdir``). """ - if config.parent: + if config.parent: # todo broken return GitWorkdir.from_potential_worktree(config.parent) if config.search_parent_directories: - return search_parent(config.absolute_root) + return search_parent(root) - return GitWorkdir.from_potential_worktree(config.absolute_root) + return GitWorkdir.from_potential_worktree(root) def parse( root: str, + config: Configuration, describe_command: str | list[str] | None = None, pre_parse: Callable[[GitWorkdir], None] = warn_on_shallow, - config: Configuration | None = None, ) -> ScmVersion | None: """ :param pre_parse: experimental pre_parse action, may change at any time """ - if not config: - config = Configuration(root=root) - - wd = get_working_directory(config) + wd = get_working_directory(config, root) if wd: return _git_parse_inner( config, wd, describe_command=describe_command, pre_parse=pre_parse @@ -187,8 +184,8 @@ def parse( def _git_parse_inner( config: Configuration, - wd: GitWorkdir | GitWorkdirHgClient, - pre_parse: None | (Callable[[GitWorkdir | GitWorkdirHgClient], None]) = None, + wd: GitWorkdir | hg_git.GitWorkdirHgClient, + pre_parse: None | (Callable[[GitWorkdir | hg_git.GitWorkdirHgClient], None]) = None, describe_command: _t.CMD_TYPE | None = None, ) -> ScmVersion: if pre_parse: @@ -269,7 +266,6 @@ def search_parent(dirname: _t.PathT) -> GitWorkdir | None: curpath = os.path.abspath(dirname) while curpath: - try: wd = GitWorkdir.from_potential_worktree(curpath) except Exception: @@ -286,7 +282,7 @@ def search_parent(dirname: _t.PathT) -> GitWorkdir | None: def archival_to_version( - data: dict[str, str], config: Configuration | None = None + data: dict[str, str], config: Configuration ) -> ScmVersion | None: node: str | None trace("data", data) @@ -301,9 +297,11 @@ def archival_to_version( distance=None if number == 0 else number, node=node, ) - versions = tags_to_versions(REF_TAG_RE.findall(data.get("ref-names", ""))) - if versions: - return meta(versions[0], config=config) + + for ref in REF_TAG_RE.findall(data.get("ref-names", "")): + version = tag_to_version(ref, config) + if version is not None: + return meta(version, config=config) else: node = data.get("node") if node is None: @@ -315,9 +313,7 @@ def archival_to_version( return meta("0.0", node=node, config=config) -def parse_archival( - root: _t.PathT, config: Configuration | None = None -) -> ScmVersion | None: +def parse_archival(root: _t.PathT, config: Configuration) -> ScmVersion | None: archival = os.path.join(root, ".git_archival.txt") data = data_from_mime(archival) return archival_to_version(data, config=config) diff --git a/src/setuptools_scm/hacks.py b/src/setuptools_scm/hacks.py index 9ca0df98..1ddcdfa3 100644 --- a/src/setuptools_scm/hacks.py +++ b/src/setuptools_scm/hacks.py @@ -5,7 +5,7 @@ if TYPE_CHECKING: from . import _types as _t -from .config import Configuration +from . import Configuration from .utils import data_from_mime from .utils import trace from .version import meta @@ -15,10 +15,7 @@ _UNKNOWN = "UNKNOWN" -def parse_pkginfo( - root: _t.PathT, config: Configuration | None = None -) -> ScmVersion | None: - +def parse_pkginfo(root: _t.PathT, config: Configuration) -> ScmVersion | None: pkginfo = os.path.join(root, "PKG-INFO") trace("pkginfo", pkginfo) data = data_from_mime(pkginfo) @@ -29,9 +26,7 @@ def parse_pkginfo( return None -def parse_pip_egg_info( - root: _t.PathT, config: Configuration | None = None -) -> ScmVersion | None: +def parse_pip_egg_info(root: _t.PathT, config: Configuration) -> ScmVersion | None: pipdir = os.path.join(root, "pip-egg-info") if not os.path.isdir(pipdir): return None diff --git a/src/setuptools_scm/hg.py b/src/setuptools_scm/hg.py index 3616b1ac..a6f57250 100644 --- a/src/setuptools_scm/hg.py +++ b/src/setuptools_scm/hg.py @@ -5,8 +5,8 @@ from pathlib import Path from typing import TYPE_CHECKING +from . import Configuration from ._version_cls import Version -from .config import Configuration from .scm_workdir import Workdir from .utils import data_from_mime from .utils import do_ex @@ -21,7 +21,6 @@ class HgWorkdir(Workdir): - COMMAND = "hg" @classmethod @@ -33,7 +32,6 @@ def from_potential_worktree(cls, wd: _t.PathT) -> HgWorkdir | None: return cls(root) def get_meta(self, config: Configuration) -> ScmVersion | None: - node: str tags_str: str bookmark: str @@ -69,7 +67,7 @@ def get_meta(self, config: Configuration) -> ScmVersion | None: tags.remove("tip") if tags: - tag = tag_to_version(tags[0]) + tag = tag_to_version(tags[0], config) if tag: return meta(tag, dirty=dirty, branch=branch, config=config) @@ -122,13 +120,11 @@ def get_latest_normalizable_tag(self) -> str | None: return tag def get_distance_revs(self, rev1: str, rev2: str = ".") -> int: - revset = f"({rev1}::{rev2})" out = self.hg_log(revset, ".") return len(out) - 1 def check_changes_since_tag(self, tag: str | None) -> bool: - if tag == "0.0" or tag is None: return True @@ -143,10 +139,7 @@ def check_changes_since_tag(self, tag: str | None) -> bool: return bool(self.hg_log(revset, ".")) -def parse(root: _t.PathT, config: Configuration | None = None) -> ScmVersion | None: - if not config: - config = Configuration(root=root) - +def parse(root: _t.PathT, config: Configuration) -> ScmVersion | None: if os.path.exists(os.path.join(root, ".hg/git")): paths, _, ret = do_ex("hg path", root) if not ret: @@ -169,9 +162,7 @@ def parse(root: _t.PathT, config: Configuration | None = None) -> ScmVersion | N return wd.get_meta(config) -def archival_to_version( - data: dict[str, str], config: Configuration | None = None -) -> ScmVersion: +def archival_to_version(data: dict[str, str], config: Configuration) -> ScmVersion: trace("data", data) node = data.get("node", "")[:12] if node: @@ -189,7 +180,7 @@ def archival_to_version( return meta("0.0", node=node, config=config) -def parse_archival(root: _t.PathT, config: Configuration | None = None) -> ScmVersion: +def parse_archival(root: _t.PathT, config: Configuration) -> ScmVersion: archival = os.path.join(root, ".hg_archival.txt") data = data_from_mime(archival) return archival_to_version(data, config=config) diff --git a/src/setuptools_scm/integration.py b/src/setuptools_scm/integration.py index 2134ff15..e9c6c129 100644 --- a/src/setuptools_scm/integration.py +++ b/src/setuptools_scm/integration.py @@ -11,11 +11,12 @@ from . import _get_version from . import _version_missing +from . import Configuration from ._entrypoints import iter_entry_points from ._integration.setuptools import ( read_dist_name_from_setup_cfg as _read_dist_name_from_setup_cfg, ) -from .config import Configuration +from ._version_cls import _validate_version_cls from .utils import do from .utils import trace @@ -62,6 +63,7 @@ def _assign_version(dist: setuptools.Distribution, config: Configuration) -> Non if maybe_version is None: _version_missing(config) else: + assert dist.metadata.version is None dist.metadata.version = maybe_version @@ -79,15 +81,22 @@ def version_keyword( assert ( "dist_name" not in value ), "dist_name may not be specified in the setup keyword " - + dist_name: str | None = dist.metadata.name + if dist.metadata.version is not None: + warnings.warn(f"version of {dist_name} already set") + return trace( "version keyword", vars(dist.metadata), ) - dist_name = dist.metadata.name # type: str | None + trace("dist", id(dist), id(dist.metadata)) + if dist_name is None: dist_name = _read_dist_name_from_setup_cfg() - config = Configuration(dist_name=dist_name, **value) + version_cls = value.pop("version_cls", None) + normalize = value.pop("normalize", True) + final_version = _validate_version_cls(version_cls, normalize) + config = Configuration(dist_name=dist_name, version_cls=final_version, **value) _assign_version(dist, config) @@ -112,6 +121,9 @@ def infer_version(dist: setuptools.Distribution) -> None: "finalize hook", vars(dist.metadata), ) + trace("dist", id(dist), id(dist.metadata)) + if dist.metadata.version is not None: + return # metadata already added by hook dist_name = dist.metadata.name if dist_name is None: dist_name = _read_dist_name_from_setup_cfg() diff --git a/src/setuptools_scm/utils.py b/src/setuptools_scm/utils.py index 7c690b84..52aa5e25 100644 --- a/src/setuptools_scm/utils.py +++ b/src/setuptools_scm/utils.py @@ -18,7 +18,6 @@ from typing import TYPE_CHECKING if TYPE_CHECKING: - from . import _types as _t DEBUG = bool(os.environ.get("SETUPTOOLS_SCM_DEBUG")) @@ -174,7 +173,6 @@ def require_command(name: str) -> None: def iter_entry_points( group: str, name: str | None = None ) -> Iterator[_t.EntrypointProtocol]: - from ._entrypoints import iter_entry_points return iter_entry_points(group, name) diff --git a/src/setuptools_scm/version.py b/src/setuptools_scm/version.py index 114e16bf..7c6abd30 100644 --- a/src/setuptools_scm/version.py +++ b/src/setuptools_scm/version.py @@ -1,5 +1,6 @@ from __future__ import annotations +import dataclasses import os import re import warnings @@ -8,22 +9,25 @@ from datetime import timezone from typing import Any from typing import Callable -from typing import cast -from typing import Iterator -from typing import List from typing import Match -from typing import overload -from typing import Tuple from typing import TYPE_CHECKING +from . import _entrypoints +from ._modify_version import _bump_dev +from ._modify_version import _bump_regex +from ._modify_version import _dont_guess_next_version +from ._modify_version import _format_local_with_time +from ._modify_version import _strip_local + if TYPE_CHECKING: from typing_extensions import Concatenate from . import _types as _t -from ._version_cls import Version as PkgVersion -from .config import Configuration -from .config import _VersionT + +from ._version_cls import Version as PkgVersion, _VersionT +from . import _version_cls as _v +from . import _config from .utils import trace SEMVER_MINOR = 2 @@ -32,7 +36,7 @@ def _parse_version_tag( - tag: str | object, config: Configuration + tag: str | object, config: _config.Configuration ) -> dict[str, str] | None: tagstring = tag if isinstance(tag, str) else str(tag) match = config.tag_regex.match(tagstring) @@ -68,17 +72,13 @@ def callable_or_entrypoint(group: str, callable_or_name: str | Any) -> Any: def tag_to_version( - tag: _VersionT | str, config: Configuration | None = None + tag: _VersionT | str, config: _config.Configuration ) -> _VersionT | None: """ take a tag that might be prefixed with a keyword and return only the version part - :param config: optional configuration object """ trace("tag", tag) - if not config: - config = Configuration() - tagdict = _parse_version_tag(tag, config) if not isinstance(tagdict, dict) or not tagdict.get("version", None): warnings.warn(f"tag {tag!r} no version found") @@ -94,68 +94,37 @@ def tag_to_version( ) ) - version = config.version_cls(version_str) + version: _VersionT = config.version_cls(version_str) trace("version", repr(version)) return version -def tags_to_versions( - tags: list[str], config: Configuration | None = None -) -> list[_VersionT]: - """ - take tags that might be prefixed with a keyword and return only the version part - :param tags: an iterable of tags - :param config: optional configuration object - """ - result: list[_VersionT] = [] - for tag in tags: - parsed = tag_to_version(tag, config=config) - if parsed: - result.append(parsed) - return result +def _source_epoch_or_utc_now() -> datetime: + if "SOURCE_DATE_EPOCH" in os.environ: + date_epoch = int(os.environ["SOURCE_DATE_EPOCH"]) + return datetime.fromtimestamp(date_epoch, timezone.utc) + else: + return datetime.now(timezone.utc) +@dataclasses.dataclass class ScmVersion: - def __init__( - self, - tag_version: Any, - config: Configuration, - distance: int | None = None, - node: str | None = None, - dirty: bool = False, - preformatted: bool = False, - branch: str | None = None, - node_date: date | None = None, - **kw: object, - ): - if kw: - trace("unknown args", kw) - self.tag = tag_version - if dirty and distance is None: - distance = 0 - self.distance = distance - self.node = node - self.node_date = node_date - if "SOURCE_DATE_EPOCH" in os.environ: - date_epoch = int(os.environ["SOURCE_DATE_EPOCH"]) - self.time = datetime.fromtimestamp(date_epoch, timezone.utc) - else: - self.time = datetime.now(timezone.utc) - self._extra = kw - self.dirty = dirty - self.preformatted = preformatted - self.branch = branch - self.config = config + tag: _v.Version | _v.NonNormalizedVersion | str + config: _config.Configuration + distance: int | None = None + node: str | None = None + dirty: bool = False + preformatted: bool = False + branch: str | None = None + node_date: date | None = None + time: datetime = dataclasses.field( + init=False, default_factory=_source_epoch_or_utc_now + ) - @property - def extra(self) -> dict[str, Any]: - warnings.warn( - "ScmVersion.extra is deprecated and will be removed in future", - category=DeprecationWarning, - stacklevel=2, - ) - return self._extra + def __post_init__(self) -> None: + if self.dirty and self.distance is None: + self.distance = 0 @property def exact(self) -> bool: @@ -194,11 +163,11 @@ def format_next_version( def _parse_tag( - tag: _VersionT | str, preformatted: bool, config: Configuration | None + tag: _VersionT | str, preformatted: bool, config: _config.Configuration ) -> _VersionT | str: if preformatted: return tag - elif config is None or not isinstance(tag, config.version_cls): + elif not isinstance(tag, config.version_cls): version = tag_to_version(tag, config) assert version is not None return version @@ -208,21 +177,15 @@ def _parse_tag( def meta( tag: str | _VersionT, + *, distance: int | None = None, dirty: bool = False, node: str | None = None, preformatted: bool = False, branch: str | None = None, - config: Configuration | None = None, + config: _config.Configuration, node_date: date | None = None, - **kw: Any, ) -> ScmVersion: - if not config: - warnings.warn( - "meta invoked without explicit configuration," - " will use defaults where required." - ) - config = Configuration() parsed_version = _parse_tag(tag, preformatted, config) trace("version", tag, "->", parsed_version) assert parsed_version is not None, "Can't parse version %s" % tag @@ -235,7 +198,6 @@ def meta( branch=branch, config=config, node_date=node_date, - **kw, ) @@ -244,51 +206,6 @@ def guess_next_version(tag_version: ScmVersion) -> str: return _bump_dev(version) or _bump_regex(version) -def _dont_guess_next_version(tag_version: ScmVersion) -> str: - version = _strip_local(str(tag_version.tag)) - return _bump_dev(version) or _add_post(version) - - -def _strip_local(version_string: str) -> str: - public, sep, local = version_string.partition("+") - return public - - -def _add_post(version: str) -> str: - if "post" in version: - raise ValueError( - f"{version} already is a post release, refusing to guess the update" - ) - return f"{version}.post1" - - -def _bump_dev(version: str) -> str | None: - if ".dev" not in version: - return None - - prefix, tail = version.rsplit(".dev", 1) - if tail != "0": - raise ValueError( - "choosing custom numbers for the `.devX` distance " - "is not supported.\n " - f"The {version} can't be bumped\n" - "Please drop the tag or create a new supported one ending in .dev0" - ) - return prefix - - -def _bump_regex(version: str) -> str: - match = re.match(r"(.*?)(\d+)$", version) - if match is None: - raise ValueError( - "{version} does not end with a number to bump, " - "please correct or use a custom version scheme".format(version=version) - ) - else: - prefix, tail = match.groups() - return "%s%d" % (prefix, int(tail) + 1) - - def guess_next_dev_version(version: ScmVersion) -> str: if version.exact: return version.format_with("{tag}") @@ -368,15 +285,13 @@ def no_guess_dev_version(version: ScmVersion) -> str: return version.format_next_version(_dont_guess_next_version) +_DATE_REGEX = re.compile( + r"^(?P(?P\d{2}|\d{4})(?:\.\d{1,2}){2})(?:\.(?P\d*))?$" +) + + def date_ver_match(ver: str) -> Match[str] | None: - match = re.match( - ( - r"^(?P(?P\d{2}|\d{4})(?:\.\d{1,2}){2})" - r"(?:\.(?P\d*)){0,1}?$" - ), - ver, - ) - return match + return _DATE_REGEX.match(ver) def guess_next_date_ver( @@ -450,18 +365,6 @@ def calver_by_date(version: ScmVersion) -> str: ) -def _format_local_with_time(version: ScmVersion, time_format: str) -> str: - - if version.exact or version.node is None: - return version.format_choice( - "", "+d{time:{time_format}}", time_format=time_format - ) - else: - return version.format_choice( - "+{node}", "+{node}.d{time:{time_format}}", time_format=time_format - ) - - def get_local_node_and_date(version: ScmVersion) -> str: return _format_local_with_time(version, time_format="%Y%m%d") @@ -485,78 +388,18 @@ def postrelease_version(version: ScmVersion) -> str: return version.format_with("{tag}.post{distance}") -def _get_ep(group: str, name: str) -> Any | None: - from ._entrypoints import iter_entry_points - - for ep in iter_entry_points(group, name): - trace("ep found:", ep.name) - return ep.load() - else: - return None - - -def _iter_version_schemes( - entrypoint: str, - scheme_value: str - | list[str] - | tuple[str, ...] - | Callable[[ScmVersion], str] - | None, - _memo: set[object] | None = None, -) -> Iterator[Callable[[ScmVersion], str]]: - if _memo is None: - _memo = set() - if isinstance(scheme_value, str): - scheme_value = cast( - 'str|List[str]|Tuple[str, ...]|Callable[["ScmVersion"], str]|None', - _get_ep(entrypoint, scheme_value), - ) - - if isinstance(scheme_value, (list, tuple)): - for variant in scheme_value: - if variant not in _memo: - _memo.add(variant) - yield from _iter_version_schemes(entrypoint, variant, _memo=_memo) - elif callable(scheme_value): - yield scheme_value - - -@overload -def _call_version_scheme( - version: ScmVersion, entypoint: str, given_value: str, default: str -) -> str: - ... - - -@overload -def _call_version_scheme( - version: ScmVersion, entypoint: str, given_value: str, default: None -) -> str | None: - ... - - -def _call_version_scheme( - version: ScmVersion, entypoint: str, given_value: str, default: str | None -) -> str | None: - for scheme in _iter_version_schemes(entypoint, given_value): - result = scheme(version) - if result is not None: - return result - return default - - def format_version(version: ScmVersion, **config: Any) -> str: trace("scm version", version) trace("config", config) if version.preformatted: assert isinstance(version.tag, str) return version.tag - main_version = _call_version_scheme( + main_version = _entrypoints._call_version_scheme( version, "setuptools_scm.version_scheme", config["version_scheme"], None ) trace("version", main_version) assert main_version is not None - local_version = _call_version_scheme( + local_version = _entrypoints._call_version_scheme( version, "setuptools_scm.local_scheme", config["local_scheme"], "+unknown" ) trace("local_version", local_version) diff --git a/testing/conftest.py b/testing/conftest.py index d29b5dd5..3928f751 100644 --- a/testing/conftest.py +++ b/testing/conftest.py @@ -23,7 +23,7 @@ def pytest_report_header() -> list[str]: try: from importlib.metadata import version # type: ignore except ImportError: - from importlib_metadata import version # type: ignore + from importlib_metadata import version res = [] for pkg in VERSION_PKGS: pkg_version = version(pkg) diff --git a/testing/test_basic_api.py b/testing/test_basic_api.py index ba11a23a..4bd72fa9 100644 --- a/testing/test_basic_api.py +++ b/testing/test_basic_api.py @@ -8,8 +8,8 @@ import pytest import setuptools_scm +from setuptools_scm import Configuration from setuptools_scm import dump_version -from setuptools_scm.config import Configuration from setuptools_scm.utils import data_from_mime from setuptools_scm.utils import do from setuptools_scm.version import ScmVersion @@ -57,11 +57,6 @@ def test_root_parameter_creation(monkeypatch: pytest.MonkeyPatch) -> None: setuptools_scm.get_version() -def test_version_from_scm(wd: WorkDir) -> None: - with pytest.warns(DeprecationWarning, match=".*version_from_scm.*"): - setuptools_scm.version_from_scm(str(wd)) - - def test_root_parameter_pass_by( monkeypatch: pytest.MonkeyPatch, tmp_path: Path ) -> None: @@ -157,7 +152,6 @@ def test_root_relative_to(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> No def test_dump_version(tmp_path: Path) -> None: - dump_version(tmp_path, "1.0", "first.txt") def read(name: str) -> str: diff --git a/testing/test_cli.py b/testing/test_cli.py index 1eb9ead6..cc5a0ef0 100644 --- a/testing/test_cli.py +++ b/testing/test_cli.py @@ -17,7 +17,6 @@ def get_output(args: list[str]) -> str: - with redirect_stdout(io.StringIO()) as out: main(args) return out.getvalue() @@ -52,7 +51,6 @@ def test_cli_find_pyproject( print(get_output(["-c", PYPROJECT_TOML])) with exits_with_not_found, warns_absolute_root_override: - get_output(["-c", PYPROJECT_TOML, "--root=.."]) with warns_cli_root_override: diff --git a/testing/test_config.py b/testing/test_config.py index 97bb36ec..211a853c 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -6,7 +6,7 @@ import pytest -from setuptools_scm.config import Configuration +from setuptools_scm import Configuration @pytest.mark.parametrize( @@ -74,3 +74,26 @@ def test_config_from_file_protects_relative_to(tmp_path: Path) -> None: " as its always relative to the config file", ): assert Configuration.from_file(str(fn)) + + +def test_config_overrides(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + fn = tmp_path / "pyproject.toml" + fn.write_text( + textwrap.dedent( + """ + [tool.setuptools_scm] + root = "." + [project] + name = "test_a" + """ + ), + encoding="utf-8", + ) + pristine = Configuration.from_file(fn) + monkeypatch.setenv( + "SETUPTOOLS_SCM_OVERRIDES_FOR_TEST_A", '{root="..", fallback_root=".."}' + ) + overriden = Configuration.from_file(fn) + + assert pristine.root != overriden.root + assert pristine.fallback_root != overriden.fallback_root diff --git a/testing/test_functions.py b/testing/test_functions.py index 53bc92b1..ceb6cd3d 100644 --- a/testing/test_functions.py +++ b/testing/test_functions.py @@ -4,10 +4,10 @@ import pytest +from setuptools_scm import Configuration from setuptools_scm import dump_version from setuptools_scm import get_version from setuptools_scm import PRETEND_KEY -from setuptools_scm.config import Configuration from setuptools_scm.utils import has_command from setuptools_scm.version import format_version from setuptools_scm.version import guess_next_version @@ -103,5 +103,5 @@ def test_has_command() -> None: ], ) def test_tag_to_version(tag: str, expected_version: str) -> None: - version = str(tag_to_version(tag)) + version = str(tag_to_version(tag, c)) assert version == expected_version diff --git a/testing/test_git.py b/testing/test_git.py index 412ce753..35ea1ec4 100644 --- a/testing/test_git.py +++ b/testing/test_git.py @@ -18,7 +18,6 @@ from .conftest import DebugMode from .wd_wrapper import WorkDir from setuptools_scm import Configuration -from setuptools_scm import format_version from setuptools_scm import git from setuptools_scm import integration from setuptools_scm import NonNormalizedVersion @@ -26,6 +25,7 @@ from setuptools_scm.git import archival_to_version from setuptools_scm.utils import do from setuptools_scm.utils import has_command +from setuptools_scm.version import format_version pytestmark = pytest.mark.skipif( not has_command("git", warn=False), reason="git executable not found" @@ -91,7 +91,7 @@ def test_root_search_parent_directories( def test_git_gone(wd: WorkDir, monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setenv("PATH", str(wd.cwd / "not-existing")) with pytest.raises(EnvironmentError, match="'git' was not found"): - git.parse(str(wd.cwd), git.DEFAULT_DESCRIBE) + git.parse(str(wd.cwd), Configuration(), git.DEFAULT_DESCRIBE) @pytest.mark.issue("https://github.com/pypa/setuptools_scm/issues/298") @@ -105,7 +105,7 @@ def test_file_finder_no_history(wd: WorkDir, caplog: pytest.LogCaptureFixture) - @pytest.mark.issue("https://github.com/pypa/setuptools_scm/issues/281") def test_parse_call_order(wd: WorkDir) -> None: - git.parse(str(wd.cwd), git.DEFAULT_DESCRIBE) + git.parse(str(wd.cwd), Configuration(), git.DEFAULT_DESCRIBE) @pytest.mark.issue("https://github.com/pypa/setuptools_scm/issues/707") @@ -130,7 +130,7 @@ def test_not_owner(wd: WorkDir) -> None: stdin=subprocess.DEVNULL, check=True, ) - assert git.parse(str(wd.cwd)) + assert git.parse(str(wd.cwd), Configuration()) finally: # Restore the ownership subprocess.run( @@ -148,8 +148,8 @@ def test_not_owner(wd: WorkDir) -> None: def test_version_from_git(wd: WorkDir) -> None: assert wd.version == "0.1.dev0" - parsed = git.parse(str(wd.cwd), git.DEFAULT_DESCRIBE) - assert parsed is not None and parsed.branch == "master" + parsed = git.parse(str(wd.cwd), Configuration(), git.DEFAULT_DESCRIBE) + assert parsed is not None and parsed.branch in ("master", "main") wd.commit_testfile() assert wd.version.startswith("0.1.dev1+g") @@ -226,8 +226,8 @@ def test_git_version_unnormalized_setuptools( the version is not normalized in write_to files, but still normalized by setuptools for the final dist metadata. """ - monkeypatch.delenv("SETUPTOOLS_SCM_DEBUG") - + # monkeypatch.delenv("SETUPTOOLS_SCM_DEBUG") + monkeypatch.chdir(wd.cwd) wd.write("setup.py", dedent(setup_py_txt)) # do git operations and tag @@ -303,23 +303,23 @@ def shallow_wd(wd: WorkDir, tmp_path: Path) -> Path: def test_git_parse_shallow_warns( shallow_wd: Path, recwarn: pytest.WarningsRecorder ) -> None: - git.parse(str(shallow_wd)) + git.parse(str(shallow_wd), Configuration()) msg = recwarn.pop() assert "is shallow and may cause errors" in str(msg.message) def test_git_parse_shallow_fail(shallow_wd: Path) -> None: with pytest.raises(ValueError, match="git fetch"): - git.parse(str(shallow_wd), pre_parse=git.fail_on_shallow) + git.parse(str(shallow_wd), Configuration(), pre_parse=git.fail_on_shallow) def test_git_shallow_autocorrect( shallow_wd: Path, recwarn: pytest.WarningsRecorder ) -> None: - git.parse(str(shallow_wd), pre_parse=git.fetch_on_shallow) + git.parse(str(shallow_wd), Configuration(), pre_parse=git.fetch_on_shallow) msg = recwarn.pop() assert "git fetch was used to rectify" in str(msg.message) - git.parse(str(shallow_wd), pre_parse=git.fail_on_shallow) + git.parse(str(shallow_wd), Configuration(), pre_parse=git.fail_on_shallow) def test_find_files_stop_at_root_git(wd: WorkDir) -> None: @@ -332,7 +332,7 @@ def test_find_files_stop_at_root_git(wd: WorkDir) -> None: @pytest.mark.issue(128) def test_parse_no_worktree(tmp_path: Path) -> None: - ret = git.parse(str(tmp_path)) + ret = git.parse(str(tmp_path), Configuration(root=str(tmp_path))) assert ret is None @@ -452,7 +452,7 @@ def test_git_getdate(wd: WorkDir) -> None: today = date.today() def parse_date() -> date: - parsed = git.parse(os.fspath(wd.cwd)) + parsed = git.parse(os.fspath(wd.cwd), Configuration()) assert parsed is not None assert parsed.node_date is not None return parsed.node_date diff --git a/testing/test_integration.py b/testing/test_integration.py index b110fa37..0ab15487 100644 --- a/testing/test_integration.py +++ b/testing/test_integration.py @@ -149,7 +149,7 @@ def test_distribution_procides_extras() -> None: try: from importlib.metadata import distribution # type: ignore except ImportError: - from importlib_metadata import distribution # type: ignore + from importlib_metadata import distribution dist = distribution("setuptools_scm") assert sorted(dist.metadata.get_all("Provides-Extra")) == ["test", "toml"] diff --git a/testing/test_mercurial.py b/testing/test_mercurial.py index 5e0bc027..144e4234 100644 --- a/testing/test_mercurial.py +++ b/testing/test_mercurial.py @@ -5,12 +5,12 @@ import pytest -from setuptools_scm import format_version +from setuptools_scm import Configuration from setuptools_scm import integration -from setuptools_scm.config import Configuration from setuptools_scm.hg import archival_to_version from setuptools_scm.hg import parse from setuptools_scm.utils import has_command +from setuptools_scm.version import format_version from testing.wd_wrapper import WorkDir @@ -54,8 +54,9 @@ def test_archival_to_version(expected: str, data: dict[str, str]) -> None: def test_hg_gone(wd: WorkDir, monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setenv("PATH", str(wd.cwd / "not-existing")) + config = Configuration() with pytest.raises(EnvironmentError, match="'hg' was not found"): - parse(str(wd.cwd)) + parse(str(wd.cwd), config=config) def test_find_files_stop_at_root_hg( @@ -129,7 +130,8 @@ def test_version_in_merge(wd: WorkDir) -> None: @pytest.mark.issue(128) def test_parse_no_worktree(tmp_path: Path) -> None: - ret = parse(os.fspath(tmp_path)) + config = Configuration() + ret = parse(os.fspath(tmp_path), config) assert ret is None @@ -189,7 +191,6 @@ def test_latest_tag_detection(wd: WorkDir) -> None: @pytest.mark.usefixtures("version_1_0") def test_feature_branch_increments_major(wd: WorkDir) -> None: - wd.commit_testfile() assert wd.get_version(version_scheme="python-simplified-semver").startswith("1.0.1") wd("hg branch feature/fun") diff --git a/testing/test_regressions.py b/testing/test_regressions.py index 6de71410..6cbabcce 100644 --- a/testing/test_regressions.py +++ b/testing/test_regressions.py @@ -1,12 +1,21 @@ from __future__ import annotations import os +import pprint import subprocess import sys +from typing import Callable + +if sys.version_info >= (3, 8): + distribution: Callable[[str], EntryPoint] + from importlib.metadata import distribution, EntryPoint +else: + from importlib_metadata import distribution, EntryPoint from pathlib import Path import pytest +from setuptools_scm import Configuration from setuptools_scm import get_version from setuptools_scm.git import parse from setuptools_scm.utils import do @@ -100,5 +109,19 @@ def test_case_mismatch_on_windows_git(tmp_path: Path) -> None: camel_case_path = tmp_path / "CapitalizedDir" camel_case_path.mkdir() do("git init", camel_case_path) - res = parse(str(camel_case_path).lower()) + res = parse(str(camel_case_path).lower(), Configuration()) assert res is not None + + +def test_entrypoints_load() -> None: + d = distribution("setuptools-scm") # type: ignore [no-untyped-call] + + eps = d.entry_points + failed: list[tuple[EntryPoint, Exception]] = [] + for ep in eps: + try: + ep.load() + except Exception as e: + failed.append((ep, e)) + if failed: + pytest.fail(pprint.pformat(failed)) diff --git a/testing/test_setuptools_support.py b/testing/test_setuptools_support.py deleted file mode 100644 index 212b4811..00000000 --- a/testing/test_setuptools_support.py +++ /dev/null @@ -1,202 +0,0 @@ -""" -integration tests that check setuptools version support -""" -from __future__ import annotations - -import os -import pathlib -import subprocess -import sys -from typing import Any -from typing import Callable - -import _pytest.config -import pytest - - -def cli_run(*k: Any, **kw: Any) -> None: - """this defers the virtualenv import - it helps to avoid warnings from the furthermore imported setuptools - """ - global cli_run - from virtualenv.run import cli_run # type: ignore - - cli_run(*k, **kw) - - -pytestmark = pytest.mark.filterwarnings( - r"ignore:.*tool\.setuptools_scm.*", r"always:.*setup.py install is deprecated.*" -) - - -ROOT = pathlib.Path(__file__).parent.parent - - -class Venv: - location: pathlib.Path - - def __init__(self, location: pathlib.Path): - self.location = location - - @property - def python(self) -> pathlib.Path: - return self.location / "bin/python" - - -class VenvMaker: - def __init__(self, base: pathlib.Path): - self.base = base - - def __repr__(self) -> str: - return f"" - - def get_venv( - self, python: str, pip: str, setuptools: str, prefix: str = "scm" - ) -> Venv: - name = f"{prefix}-py={python}-pip={pip}-setuptools={setuptools}" - path = self.base / name - if not path.is_dir(): - cli_run( - [ - str(path), - "--python", - python, - "--pip", - pip, - "--setuptools", - setuptools, - ], - setup_logging=False, - ) - venv = Venv(path) - subprocess.run([venv.python, "-m", "pip", "install", "-e", str(ROOT)]) - # fixup pip - subprocess.check_call([venv.python, "-m", "pip", "install", f"pip=={pip}"]) - subprocess.check_call( - [venv.python, "-m", "pip", "install", f"setuptools~={setuptools}"] - ) - return venv - - -@pytest.fixture -def venv_maker(pytestconfig: _pytest.config.Config) -> VenvMaker: - if not pytestconfig.getoption("--test-legacy"): - pytest.skip( - "testing on legacy setuptools disabled, pass --test-legacy to run them" - ) - assert pytestconfig.cache is not None - path = pytestconfig.cache.mkdir("setuptools_scm_venvs") - return VenvMaker(path) - - -SCRIPT = """ -from __future__ import print_function -import sys -import setuptools -print(setuptools.__version__, 'expected', sys.argv[1]) -import setuptools_scm.version -from setuptools_scm.__main__ import main -main() -""" - - -def check(venv: Venv, expected_version: str, **env: str) -> None: - - subprocess.check_call( - [venv.python, "-c", SCRIPT, expected_version], - env=dict(os.environ, **env), - ) - - -@pytest.mark.skipif( - sys.version_info[:2] >= (3, 10), reason="old setuptools won't work on python 3.10" -) -def test_distlib_setuptools_works(venv_maker: VenvMaker) -> None: - venv = venv_maker.get_venv(setuptools="45.0.0", pip="9.0", python="3.6") - subprocess.run([venv.python, "-m", "pip", "install", "-e", str(ROOT)]) - - check(venv, "45.0.0") - - -SETUP_PY_NAME = """ -from setuptools import setup -setup(name='setuptools_scm_test_package') -""" - -SETUP_PY_KEYWORD = """ -from setuptools import setup -setup(use_scm_version={"write_to": "pkg_version.py"}) -""" - -PYPROJECT_TOML_WITH_KEY = """ -[build-system] -# Minimum requirements for the build system to execute. -requires = ["setuptools>45"] # PEP 508 specifications. -[tool.setuptools_scm] -write_to = "pkg_version.py" -""" - -SETUP_CFG_NAME = """ -[metadata] -name = setuptools_scm_test_package -""" - - -def prepare_expecting_pyproject_support(pkg: pathlib.Path) -> None: - pkg.mkdir() - pkg.joinpath("setup.py").write_text(SETUP_PY_NAME) - pkg.joinpath("pyproject.toml").write_text(PYPROJECT_TOML_WITH_KEY) - pkg.joinpath("PKG-INFO").write_text("Version: 1.0.0") - - -def prepare_setup_py_config(pkg: pathlib.Path) -> None: - pkg.mkdir() - pkg.joinpath("setup.py").write_text(SETUP_PY_KEYWORD) - pkg.joinpath("setup.cfg").write_text(SETUP_CFG_NAME) - - pkg.joinpath("PKG-INFO").write_text("Version: 1.0.0") - - -@pytest.mark.skipif( - sys.version_info[:2] >= (3, 10), reason="old setuptools won't work on python 3.10" -) -@pytest.mark.parametrize("setuptools", [f"{v}.0" for v in range(31, 45)]) -@pytest.mark.parametrize( - "project_create", - [ - pytest.param( - prepare_expecting_pyproject_support, - marks=pytest.mark.xfail(reason="pyproject requires setuptools > 42"), - ), - prepare_setup_py_config, - ], -) -def test_on_old_setuptools( - venv_maker: VenvMaker, - tmp_path: pathlib.Path, - setuptools: str, - project_create: Callable[[pathlib.Path], None], -) -> None: - pkg = tmp_path.joinpath("pkg") - project_create(pkg) - venv = venv_maker.get_venv(setuptools=setuptools, pip="9.0", python="3.6") - - # monkeypatch.delenv("SETUPTOOLS_SCM_DEBUG", raising=False) - - def run_and_output(cmd: list[str | pathlib.Path]) -> bytes: - res = subprocess.run(cmd, cwd=str(pkg), stdout=subprocess.PIPE) - if not res.returncode: - return res.stdout.strip() - else: - print(res.stdout) - pytest.fail(str(cmd), pytrace=False) - - version = run_and_output([venv.python, "setup.py", "--version"]) - name = run_and_output([venv.python, "setup.py", "--name"]) - assert (name, version) == (b"setuptools_scm_test_package", b"1.0.0") - - # monkeypatch.setenv( - # "SETUPTOOLS_SCM_PRETEND_VERSION_FOR_setuptools_scm_test_package", "2.0,0") - - # version_pretend = run_and_output([venv.python, "setup.py", "--version"]) - # assert version_pretend == b"2.0.0" diff --git a/testing/test_version.py b/testing/test_version.py index 0a070959..7c68d425 100644 --- a/testing/test_version.py +++ b/testing/test_version.py @@ -6,7 +6,7 @@ import pytest -from setuptools_scm.config import Configuration +from setuptools_scm import Configuration from setuptools_scm.version import calver_by_date from setuptools_scm.version import format_version from setuptools_scm.version import guess_next_version @@ -15,7 +15,6 @@ from setuptools_scm.version import release_branch_semver_version from setuptools_scm.version import ScmVersion from setuptools_scm.version import simplified_semver_version -from setuptools_scm.version import tags_to_versions c = Configuration() @@ -59,7 +58,6 @@ def test_next_semver(version: ScmVersion, expected_next: str) -> None: def test_next_semver_bad_tag() -> None: - version = meta("1.0.0-foo", preformatted=True, config=c) with pytest.raises( ValueError, match=r"1\.0\.0-foo.* can't be parsed as numeric version" @@ -183,16 +181,10 @@ def test_tag_regex1(tag: str, expected: str) -> None: result = meta(tag, config=c) else: result = meta(tag, config=c) - + assert not isinstance(result.tag, str) assert result.tag.public == expected -@pytest.mark.issue("https://github.com/pypa/setuptools_scm/issues/286") -def test_tags_to_versions() -> None: - versions = tags_to_versions(["1.0", "2.0", "3.0"], config=c) - assert isinstance(versions, list) # enable subscription - - @pytest.mark.issue("https://github.com/pypa/setuptools_scm/issues/471") def test_version_bump_bad() -> None: class YikesVersion: @@ -204,13 +196,12 @@ def __init__(self, val: str): def __str__(self) -> str: return self.val - config = Configuration(version_cls=YikesVersion) + config = Configuration(version_cls=YikesVersion) # type: ignore[arg-type] with pytest.raises( ValueError, match=".*does not end with a number to bump, " "please correct or use a custom version scheme", ): - guess_next_version(tag_version=meta("2.0.0-alpha.5-PMC", config=config)) @@ -348,7 +339,8 @@ def __str__(self) -> str: def __repr__(self) -> str: return "MyVersion" % self.tag - scm_version = meta("1.0.0-foo", config=Configuration(version_cls=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" diff --git a/tox.ini b/tox.ini index ae698972..581f62c3 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist=py{37,38,39,310,311}-{test,selfcheck},check_readme,check-dist,check-bootstrap +envlist=py{37,38,39,310,311}-{test,selfcheck},check_readme,check-dist [pytest] testpaths=testing @@ -37,16 +37,18 @@ commands= [testenv:check_readme] skip_install=True -setenv = SETUPTOOLS_SCM_PRETEND_VERSION=2.0 deps= check-manifest docutils pygments + setuptools>45 + typing_extensions commands= rst2html.py README.rst {envlogdir}/README.html --strict [] - check-manifest + check-manifest --no-build-isolation [testenv:check_dist] +skip_install = true deps= build twine @@ -54,14 +56,7 @@ commands= python -m build twine check dist/* -[testenv:check-bootstrap] -deps = - setuptools > 45 - packaging>20 -skip_install = true -recreate = true -commands = - python setup.py bdist_wheel + #XXX: envs for hg versions