diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/setuptools_scm/__init__.py | 75 | ||||
-rw-r--r-- | src/setuptools_scm/_cli.py | 3 | ||||
-rw-r--r-- | src/setuptools_scm/_config.py | 132 | ||||
-rw-r--r-- | src/setuptools_scm/_entrypoints.py | 112 | ||||
-rw-r--r-- | src/setuptools_scm/_integration/pyproject_reading.py | 11 | ||||
-rw-r--r-- | src/setuptools_scm/_integration/setuptools.py | 1 | ||||
-rw-r--r-- | src/setuptools_scm/_modify_version.py | 61 | ||||
-rw-r--r-- | src/setuptools_scm/_overrides.py | 46 | ||||
-rw-r--r-- | src/setuptools_scm/_types.py | 18 | ||||
-rw-r--r-- | src/setuptools_scm/_version_cls.py | 38 | ||||
-rw-r--r-- | src/setuptools_scm/config.py | 216 | ||||
-rw-r--r-- | src/setuptools_scm/discover.py | 6 | ||||
-rw-r--r-- | src/setuptools_scm/file_finder.py | 10 | ||||
-rw-r--r-- | src/setuptools_scm/file_finder_git.py | 5 | ||||
-rw-r--r-- | src/setuptools_scm/file_finder_hg.py | 5 | ||||
-rw-r--r-- | src/setuptools_scm/git.py | 42 | ||||
-rw-r--r-- | src/setuptools_scm/hacks.py | 11 | ||||
-rw-r--r-- | src/setuptools_scm/hg.py | 19 | ||||
-rw-r--r-- | src/setuptools_scm/integration.py | 20 | ||||
-rw-r--r-- | src/setuptools_scm/utils.py | 2 | ||||
-rw-r--r-- | src/setuptools_scm/version.py | 265 |
21 files changed, 484 insertions, 614 deletions
diff --git a/src/setuptools_scm/__init__.py b/src/setuptools_scm/__init__.py index aeb4ab6..d6def65 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 @@ __version_tuple__ = version_tuple = {version_tuple!r} } -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 @@ __all__ = [ "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 8e01f24..953c471 100644 --- a/src/setuptools_scm/_cli.py +++ b/src/setuptools_scm/_cli.py @@ -5,7 +5,7 @@ import os 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 0000000..84f7227 --- /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<version>[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 2efb9f8..4f4ba8b 100644 --- a/src/setuptools_scm/_entrypoints.py +++ b/src/setuptools_scm/_entrypoints.py @@ -2,16 +2,17 @@ from __future__ import annotations 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 @@ else: 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,71 @@ 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 _get_from_object_reference_str(path: str) -> Any | None: + try: + from importlib.metadata import EntryPoint + except ImportError: + from importlib_metadata import EntryPoint + try: + return EntryPoint(path, path, None).load() + except (AttributeError, ModuleNotFoundError): + return None + + +def _iter_version_schemes( + entrypoint: str, + scheme_value: _t.VERSION_SCHEMES, + _memo: set[object] | None = None, +) -> Iterator[Callable[[version.ScmVersion], str]]: + if _memo is None: + _memo = set() + if isinstance(scheme_value, str): + scheme_value = cast( + "_t.VERSION_SCHEMES", + _get_ep(entrypoint, scheme_value) + or _get_from_object_reference_str(scheme_value), + ) + + 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 d9208f1..8730c0e 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 @@ TOML_LOADER: TypeAlias = Callable[[str], TOML_RESULT] 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 306ff73..21f9591 100644 --- a/src/setuptools_scm/_integration/setuptools.py +++ b/src/setuptools_scm/_integration/setuptools.py @@ -7,7 +7,6 @@ from typing import IO 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 0000000..a364adc --- /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 f18b82c..9040487 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 6c6bdf8..5c2cd33 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 39e66b2..f6e87a8 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 3bf250a..0000000 --- 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<version>[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 f7843ee..58384aa 100644 --- a/src/setuptools_scm/discover.py +++ b/src/setuptools_scm/discover.py @@ -3,11 +3,9 @@ from __future__ import annotations 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 f14a946..27196c0 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 775c49d..65aa999 100644 --- a/src/setuptools_scm/file_finder_git.py +++ b/src/setuptools_scm/file_finder_git.py @@ -5,17 +5,14 @@ import os 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 2ce974f..b750fea 100644 --- a/src/setuptools_scm/file_finder_hg.py +++ b/src/setuptools_scm/file_finder_hg.py @@ -2,17 +2,14 @@ from __future__ import annotations 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 16ca378..1859671 100644 --- a/src/setuptools_scm/git.py +++ b/src/setuptools_scm/git.py @@ -11,7 +11,8 @@ from os.path import samefile 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 require_command 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 9ca0df9..1ddcdfa 100644 --- a/src/setuptools_scm/hacks.py +++ b/src/setuptools_scm/hacks.py @@ -5,7 +5,7 @@ from typing import TYPE_CHECKING 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 @@ from .version import tag_to_version _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 3616b1a..a6f5725 100644 --- a/src/setuptools_scm/hg.py +++ b/src/setuptools_scm/hg.py @@ -5,8 +5,8 @@ import os 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 @@ if TYPE_CHECKING: class HgWorkdir(Workdir): - COMMAND = "hg" @classmethod @@ -33,7 +32,6 @@ class HgWorkdir(Workdir): return cls(root) def get_meta(self, config: Configuration) -> ScmVersion | None: - node: str tags_str: str bookmark: str @@ -69,7 +67,7 @@ class HgWorkdir(Workdir): 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 @@ class HgWorkdir(Workdir): 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 @@ class HgWorkdir(Workdir): 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 2134ff1..e9c6c12 100644 --- a/src/setuptools_scm/integration.py +++ b/src/setuptools_scm/integration.py @@ -11,11 +11,12 @@ import setuptools 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 7c690b8..52aa5e2 100644 --- a/src/setuptools_scm/utils.py +++ b/src/setuptools_scm/utils.py @@ -18,7 +18,6 @@ from typing import NamedTuple 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 d4073f7..7c6abd3 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 datetime 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 @@ SEMVER_LEN = 3 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 @@ class ScmVersion: 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<date>(?P<year>\d{2}|\d{4})(?:\.\d{1,2}){2})(?:\.(?P<patch>\d*))?$" +) + + def date_ver_match(ver: str) -> Match[str] | None: - match = re.match( - ( - r"^(?P<date>(?P<year>\d{2}|\d{4})(?:\.\d{1,2}){2})" - r"(?:\.(?P<patch>\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,90 +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 _get_from_object_reference_str(path: str) -> Any | None: - try: - from importlib.metadata import EntryPoint - except ImportError: - from importlib_metadata import EntryPoint - try: - return EntryPoint(path, path, None).load() - except (AttributeError, ModuleNotFoundError): - 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) - or _get_from_object_reference_str(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) |