summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/setuptools_scm/__init__.py75
-rw-r--r--src/setuptools_scm/_cli.py3
-rw-r--r--src/setuptools_scm/_config.py132
-rw-r--r--src/setuptools_scm/_entrypoints.py112
-rw-r--r--src/setuptools_scm/_integration/pyproject_reading.py11
-rw-r--r--src/setuptools_scm/_integration/setuptools.py1
-rw-r--r--src/setuptools_scm/_modify_version.py61
-rw-r--r--src/setuptools_scm/_overrides.py46
-rw-r--r--src/setuptools_scm/_types.py18
-rw-r--r--src/setuptools_scm/_version_cls.py38
-rw-r--r--src/setuptools_scm/config.py216
-rw-r--r--src/setuptools_scm/discover.py6
-rw-r--r--src/setuptools_scm/file_finder.py10
-rw-r--r--src/setuptools_scm/file_finder_git.py5
-rw-r--r--src/setuptools_scm/file_finder_hg.py5
-rw-r--r--src/setuptools_scm/git.py42
-rw-r--r--src/setuptools_scm/hacks.py11
-rw-r--r--src/setuptools_scm/hg.py19
-rw-r--r--src/setuptools_scm/integration.py20
-rw-r--r--src/setuptools_scm/utils.py2
-rw-r--r--src/setuptools_scm/version.py265
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)