setuptools-git-versioning 2.0.0__py3-none-any.whl → 3.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- setuptools_git_versioning/__init__.py +41 -0
- setuptools_git_versioning/__main__.py +3 -0
- setuptools_git_versioning/cli.py +36 -0
- setuptools_git_versioning/defaults.py +26 -0
- setuptools_git_versioning/factories.py +160 -0
- setuptools_git_versioning/git.py +87 -0
- setuptools_git_versioning/log.py +18 -0
- setuptools_git_versioning/setup.py +150 -0
- setuptools_git_versioning/subst.py +71 -0
- setuptools_git_versioning/version.py +213 -0
- {setuptools_git_versioning-2.0.0.dist-info → setuptools_git_versioning-3.0.0.dist-info}/METADATA +37 -27
- setuptools_git_versioning-3.0.0.dist-info/RECORD +16 -0
- {setuptools_git_versioning-2.0.0.dist-info → setuptools_git_versioning-3.0.0.dist-info}/WHEEL +1 -1
- {setuptools_git_versioning-2.0.0.dist-info → setuptools_git_versioning-3.0.0.dist-info}/entry_points.txt +1 -1
- setuptools_git_versioning-2.0.0.dist-info/RECORD +0 -7
- setuptools_git_versioning.py +0 -684
- {setuptools_git_versioning-2.0.0.dist-info → setuptools_git_versioning-3.0.0.dist-info/licenses}/LICENSE +0 -0
- {setuptools_git_versioning-2.0.0.dist-info → setuptools_git_versioning-3.0.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Any
|
|
4
|
+
|
|
5
|
+
from setuptools_git_versioning.git import (
|
|
6
|
+
count_since,
|
|
7
|
+
get_all_tags,
|
|
8
|
+
get_branch,
|
|
9
|
+
get_branches,
|
|
10
|
+
get_latest_file_commit,
|
|
11
|
+
get_sha,
|
|
12
|
+
get_tag,
|
|
13
|
+
get_tags,
|
|
14
|
+
is_dirty,
|
|
15
|
+
)
|
|
16
|
+
from setuptools_git_versioning.setup import get_version, infer_version
|
|
17
|
+
from setuptools_git_versioning.version import version_from_git
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from setuptools.dist import Distribution
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def parse_config(dist: Distribution, attr: Any, value: Any) -> None:
|
|
24
|
+
"Dummy function used only to register in distutils"
|
|
25
|
+
return
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
__all__ = [
|
|
29
|
+
"count_since",
|
|
30
|
+
"get_all_tags",
|
|
31
|
+
"get_branch",
|
|
32
|
+
"get_branches",
|
|
33
|
+
"get_latest_file_commit",
|
|
34
|
+
"get_sha",
|
|
35
|
+
"get_tag",
|
|
36
|
+
"get_tags",
|
|
37
|
+
"get_version",
|
|
38
|
+
"infer_version",
|
|
39
|
+
"is_dirty",
|
|
40
|
+
"version_from_git",
|
|
41
|
+
]
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import logging
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
from setuptools_git_versioning.log import DEBUG, LOG_FORMAT, VERBOSITY_LEVELS
|
|
6
|
+
from setuptools_git_versioning.setup import get_version
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def get_parser() -> argparse.ArgumentParser:
|
|
10
|
+
parser = argparse.ArgumentParser(
|
|
11
|
+
prog="setuptools-git-versioning",
|
|
12
|
+
description="Get version based on git information and 'setup.py' or 'pyproject.toml' config",
|
|
13
|
+
)
|
|
14
|
+
parser.add_argument(
|
|
15
|
+
"root",
|
|
16
|
+
type=str,
|
|
17
|
+
default=None,
|
|
18
|
+
nargs="?",
|
|
19
|
+
help="Path to folder containing 'setup.py' or 'pyproject.toml' file. Default: current dir",
|
|
20
|
+
)
|
|
21
|
+
parser.add_argument(
|
|
22
|
+
"--verbose",
|
|
23
|
+
"-v",
|
|
24
|
+
action="count",
|
|
25
|
+
default=0,
|
|
26
|
+
help="Increase verbosity level. Can be used multiple times: -v for INFO messages, -vv for DEBUG",
|
|
27
|
+
)
|
|
28
|
+
return parser
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def main():
|
|
32
|
+
parser = get_parser()
|
|
33
|
+
namespace = parser.parse_args()
|
|
34
|
+
log_level = VERBOSITY_LEVELS.get(namespace.verbose, DEBUG)
|
|
35
|
+
logging.basicConfig(level=log_level, format=LOG_FORMAT, stream=sys.stderr)
|
|
36
|
+
print(str(get_version(root=namespace.root))) # noqa: T201
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
DEFAULT_TEMPLATE = "{tag}"
|
|
4
|
+
DEFAULT_DEV_TEMPLATE = "{tag}.post{ccount}+git.{sha}"
|
|
5
|
+
DEFAULT_DIRTY_TEMPLATE = "{tag}.post{ccount}+git.{sha}.dirty"
|
|
6
|
+
DEFAULT_STARTING_VERSION = "0.0.1"
|
|
7
|
+
DEFAULT_SORT_BY = "creatordate"
|
|
8
|
+
DEFAULT_CONFIG = {
|
|
9
|
+
"template": DEFAULT_TEMPLATE,
|
|
10
|
+
"dev_template": DEFAULT_DEV_TEMPLATE,
|
|
11
|
+
"dirty_template": DEFAULT_DIRTY_TEMPLATE,
|
|
12
|
+
"starting_version": DEFAULT_STARTING_VERSION,
|
|
13
|
+
"version_callback": None,
|
|
14
|
+
"version_file": None,
|
|
15
|
+
"count_commits_from_version_file": False,
|
|
16
|
+
"tag_formatter": None,
|
|
17
|
+
"branch_formatter": None,
|
|
18
|
+
"tag_filter": None,
|
|
19
|
+
"sort_by": DEFAULT_SORT_BY,
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def set_default_options(config: dict):
|
|
24
|
+
for key, value in DEFAULT_CONFIG.items():
|
|
25
|
+
config.setdefault(key, value)
|
|
26
|
+
return config
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import importlib
|
|
4
|
+
import inspect
|
|
5
|
+
import logging
|
|
6
|
+
import os
|
|
7
|
+
import re
|
|
8
|
+
import sys
|
|
9
|
+
from typing import Any, Callable
|
|
10
|
+
|
|
11
|
+
from setuptools_git_versioning.log import DEBUG, INFO
|
|
12
|
+
|
|
13
|
+
log = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def add_to_sys_path(root: str | os.PathLike | None) -> None:
|
|
17
|
+
project_root = os.fspath(root) if root else os.getcwd() # noqa: PTH109
|
|
18
|
+
if project_root not in sys.path:
|
|
19
|
+
log.log(DEBUG, "Adding '%s' folder to sys.path", project_root)
|
|
20
|
+
sys.path.insert(0, project_root)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def import_reference(
|
|
24
|
+
ref: str,
|
|
25
|
+
package_name: str | None = None,
|
|
26
|
+
root: str | os.PathLike | None = None,
|
|
27
|
+
) -> Any:
|
|
28
|
+
if ":" not in ref:
|
|
29
|
+
msg = f"Wrong reference name: {ref}"
|
|
30
|
+
raise NameError(msg)
|
|
31
|
+
|
|
32
|
+
add_to_sys_path(root)
|
|
33
|
+
|
|
34
|
+
module_name, attr = ref.split(":")
|
|
35
|
+
log.log(DEBUG, "Executing 'from %s.%s import %s'", package_name or "", module_name, attr)
|
|
36
|
+
module = importlib.import_module(module_name, package_name)
|
|
37
|
+
|
|
38
|
+
return getattr(module, attr)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def load_callable(
|
|
42
|
+
inp: str,
|
|
43
|
+
package_name: str | None = None,
|
|
44
|
+
root: str | os.PathLike | None = None,
|
|
45
|
+
) -> Callable:
|
|
46
|
+
ref = import_reference(inp, package_name, root=root)
|
|
47
|
+
if not callable(ref):
|
|
48
|
+
msg = f"{ref} of type {type(ref)} is not callable"
|
|
49
|
+
raise TypeError(msg)
|
|
50
|
+
|
|
51
|
+
return ref
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _callable_factory(
|
|
55
|
+
callable_name: str,
|
|
56
|
+
regexp_or_ref: str | Callable,
|
|
57
|
+
callable_factory: Callable[[str], Callable],
|
|
58
|
+
package_name: str | None = None,
|
|
59
|
+
root: str | os.PathLike | None = None,
|
|
60
|
+
) -> Callable:
|
|
61
|
+
log.log(INFO, "Parsing %s %r of type %r", callable_name, regexp_or_ref, type(regexp_or_ref).__name__)
|
|
62
|
+
|
|
63
|
+
if callable(regexp_or_ref):
|
|
64
|
+
log.log(DEBUG, "Value is callable with signature %s", inspect.Signature.from_callable(regexp_or_ref))
|
|
65
|
+
return regexp_or_ref
|
|
66
|
+
|
|
67
|
+
try:
|
|
68
|
+
return load_callable(regexp_or_ref, package_name, root=root)
|
|
69
|
+
except (ImportError, NameError) as e:
|
|
70
|
+
log.log(DEBUG, "%s is not a valid function reference: %s", callable_name, e)
|
|
71
|
+
|
|
72
|
+
try:
|
|
73
|
+
return callable_factory(regexp_or_ref)
|
|
74
|
+
except re.error as e:
|
|
75
|
+
log.exception("%s is not valid regexp neither a valid function reference", callable_name)
|
|
76
|
+
msg = f"Cannot parse {callable_name}"
|
|
77
|
+
raise ValueError(msg) from e
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def branch_formatter_factory(regexp: str) -> Callable[[str], str]:
|
|
81
|
+
pattern = re.compile(regexp)
|
|
82
|
+
|
|
83
|
+
def branch_formatter(branch: str) -> str:
|
|
84
|
+
match = pattern.match(branch)
|
|
85
|
+
if match:
|
|
86
|
+
return match.group("branch")
|
|
87
|
+
|
|
88
|
+
msg = f"Branch name {branch} does not match regexp '{regexp}'"
|
|
89
|
+
raise ValueError(msg)
|
|
90
|
+
|
|
91
|
+
return branch_formatter
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def create_branch_formatter(
|
|
95
|
+
branch_formatter: Callable[[str], str] | str,
|
|
96
|
+
package_name: str | None = None,
|
|
97
|
+
root: str | os.PathLike | None = None,
|
|
98
|
+
):
|
|
99
|
+
return _callable_factory(
|
|
100
|
+
callable_name="branch_formatter",
|
|
101
|
+
regexp_or_ref=branch_formatter,
|
|
102
|
+
callable_factory=branch_formatter_factory,
|
|
103
|
+
package_name=package_name,
|
|
104
|
+
root=root,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def tag_formatter_factory(regexp: str) -> Callable[[str], str]:
|
|
109
|
+
pattern = re.compile(regexp)
|
|
110
|
+
|
|
111
|
+
def tag_formatter(tag: str) -> str:
|
|
112
|
+
match = pattern.match(tag)
|
|
113
|
+
if match:
|
|
114
|
+
return match.group("tag")
|
|
115
|
+
|
|
116
|
+
msg = f"Tag name {tag} does not match regexp '{regexp}'"
|
|
117
|
+
raise ValueError(msg)
|
|
118
|
+
|
|
119
|
+
return tag_formatter
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def create_tag_formatter(
|
|
123
|
+
tag_formatter: Callable[[str], str] | str,
|
|
124
|
+
package_name: str | None = None,
|
|
125
|
+
root: str | os.PathLike | None = None,
|
|
126
|
+
):
|
|
127
|
+
return _callable_factory(
|
|
128
|
+
callable_name="tag_formatter",
|
|
129
|
+
regexp_or_ref=tag_formatter,
|
|
130
|
+
callable_factory=tag_formatter_factory,
|
|
131
|
+
package_name=package_name,
|
|
132
|
+
root=root,
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def tag_filter_factory(regexp: str) -> Callable[[str], str | None]:
|
|
137
|
+
pattern = re.compile(regexp)
|
|
138
|
+
|
|
139
|
+
def tag_filter(tag: str) -> str | None:
|
|
140
|
+
match = pattern.match(tag)
|
|
141
|
+
if match:
|
|
142
|
+
log.info("Matched %s", tag)
|
|
143
|
+
return tag
|
|
144
|
+
return None
|
|
145
|
+
|
|
146
|
+
return tag_filter
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def create_tag_filter(
|
|
150
|
+
tag_filter: Callable[[str], str | None] | str,
|
|
151
|
+
package_name: str | None = None,
|
|
152
|
+
root: str | os.PathLike | None = None,
|
|
153
|
+
):
|
|
154
|
+
return _callable_factory(
|
|
155
|
+
callable_name="tag_filter",
|
|
156
|
+
regexp_or_ref=tag_filter,
|
|
157
|
+
callable_factory=tag_filter_factory,
|
|
158
|
+
package_name=package_name,
|
|
159
|
+
root=root,
|
|
160
|
+
)
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import os
|
|
5
|
+
import subprocess # nosec
|
|
6
|
+
from contextlib import suppress
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Callable
|
|
9
|
+
|
|
10
|
+
from setuptools_git_versioning.defaults import DEFAULT_SORT_BY
|
|
11
|
+
from setuptools_git_versioning.log import DEBUG
|
|
12
|
+
|
|
13
|
+
log = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _exec(*cmd: str, root: str | os.PathLike | None = None) -> list[str]:
|
|
17
|
+
log.log(DEBUG, "Executing %r at '%s'", cmd, root or Path.cwd())
|
|
18
|
+
try:
|
|
19
|
+
stdout = subprocess.check_output(cmd, text=True, cwd=root) # noqa: S603
|
|
20
|
+
except subprocess.CalledProcessError as e:
|
|
21
|
+
stdout = e.output
|
|
22
|
+
lines = stdout.splitlines()
|
|
23
|
+
return [line.rstrip() for line in lines if line.rstrip()]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def get_branches(root: str | os.PathLike | None = None) -> list[str]:
|
|
27
|
+
"""Return list of branch names in the git repository"""
|
|
28
|
+
branches = _exec("git", "branch", "-l", "--format", "%(refname:short)", root=root)
|
|
29
|
+
return branches or []
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def get_branch(root: str | os.PathLike | None = None) -> str | None:
|
|
33
|
+
"""Return branch name pointing to HEAD, or None"""
|
|
34
|
+
branches = _exec("git", "rev-parse", "--abbrev-ref", "HEAD", root=root)
|
|
35
|
+
return branches[0] if branches else None
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def get_all_tags(sort_by: str = DEFAULT_SORT_BY, root: str | os.PathLike | None = None) -> list[str]:
|
|
39
|
+
"""Return list of tags in the git repository"""
|
|
40
|
+
tags = _exec("git", "tag", f"--sort=-{sort_by}", root=root)
|
|
41
|
+
return tags or []
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def get_tags(
|
|
45
|
+
sort_by: str = DEFAULT_SORT_BY,
|
|
46
|
+
filter_callback: Callable[[str], str | None] | None = None,
|
|
47
|
+
root: str | os.PathLike | None = None,
|
|
48
|
+
) -> list[str]:
|
|
49
|
+
"""Return list of tags merged into HEAD history tree"""
|
|
50
|
+
tags = _exec("git", "tag", f"--sort=-{sort_by}", "--merged", root=root)
|
|
51
|
+
if filter_callback:
|
|
52
|
+
# pull the tags that don't start with tag_prefix out of the list
|
|
53
|
+
return list(filter(filter_callback, tags))
|
|
54
|
+
return tags or []
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def get_tag(*args, **kwargs) -> str | None:
|
|
58
|
+
"""Return latest tag merged into HEAD history tree"""
|
|
59
|
+
tags = get_tags(*args, **kwargs)
|
|
60
|
+
return tags[0] if tags else None
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def get_sha(name: str = "HEAD", root: str | os.PathLike | None = None) -> str | None:
|
|
64
|
+
"""Get commit SHA-1 hash"""
|
|
65
|
+
sha = _exec("git", "rev-list", "-n", "1", name, root=root)
|
|
66
|
+
return sha[0] if sha else None
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def get_latest_file_commit(path: str | os.PathLike, root: str | os.PathLike | None = None) -> str | None:
|
|
70
|
+
"""Get SHA-1 hash of latest commit of the file in the repository"""
|
|
71
|
+
sha = _exec("git", "log", "-n", "1", "--pretty=format:%H", "--", os.fspath(path), root=root)
|
|
72
|
+
return sha[0] if sha else None
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def is_dirty(root: str | os.PathLike | None = None) -> bool:
|
|
76
|
+
"""Check index status, and return True if there are some uncommitted changes"""
|
|
77
|
+
res = _exec("git", "status", "--short", root=root)
|
|
78
|
+
return bool(res)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def count_since(name: str, root: str | os.PathLike | None = None) -> int | None:
|
|
82
|
+
"""Get number of commits between HEAD and the commit, or None if they are not related"""
|
|
83
|
+
res = _exec("git", "rev-list", "--count", "HEAD", f"^{name}", root=root)
|
|
84
|
+
if res:
|
|
85
|
+
with suppress(ValueError, TypeError):
|
|
86
|
+
return int(res[0])
|
|
87
|
+
return None
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
# setuptools v60.2.0 changed default logging level to DEBUG: https://github.com/pypa/setuptools/pull/2974
|
|
6
|
+
# to avoid printing information messages to the same output as version number,
|
|
7
|
+
# use a custom levels below built-in DEBUG level (10)
|
|
8
|
+
INFO = 9
|
|
9
|
+
DEBUG = 8
|
|
10
|
+
logging.addLevelName(INFO, "INF0")
|
|
11
|
+
logging.addLevelName(DEBUG, "DE8UG")
|
|
12
|
+
VERBOSITY_LEVELS = {
|
|
13
|
+
0: logging.WARNING,
|
|
14
|
+
1: INFO,
|
|
15
|
+
2: DEBUG,
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
LOG_FORMAT = "[%(asctime)s] %(levelname)+8s: %(message)s"
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
import textwrap
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from pprint import pformat
|
|
9
|
+
from typing import TYPE_CHECKING
|
|
10
|
+
|
|
11
|
+
from setuptools_git_versioning.defaults import set_default_options
|
|
12
|
+
from setuptools_git_versioning.factories import add_to_sys_path
|
|
13
|
+
from setuptools_git_versioning.log import DEBUG, INFO
|
|
14
|
+
from setuptools_git_versioning.version import version_from_git
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
# avoid importing 'packaging' because setuptools-git-versioning can be installed using sdist
|
|
18
|
+
# where 'packaging' is not installed yet
|
|
19
|
+
from packaging.version import Version
|
|
20
|
+
from setuptools.dist import Distribution
|
|
21
|
+
|
|
22
|
+
log = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def read_toml(name_or_path: str | os.PathLike = "pyproject.toml", root: str | os.PathLike | None = None) -> dict:
|
|
26
|
+
project_root = Path(root) if root else Path.cwd()
|
|
27
|
+
file_path = project_root.joinpath(name_or_path)
|
|
28
|
+
if not file_path.exists():
|
|
29
|
+
log.log(INFO, "'%s' does not exist", file_path)
|
|
30
|
+
return {}
|
|
31
|
+
|
|
32
|
+
if not file_path.is_file():
|
|
33
|
+
msg = f"'{file_path}' is not a file"
|
|
34
|
+
raise OSError(msg)
|
|
35
|
+
|
|
36
|
+
log.log(INFO, "Trying 'pyproject.toml' ...")
|
|
37
|
+
try:
|
|
38
|
+
# for Python 3.11+
|
|
39
|
+
import tomllib
|
|
40
|
+
|
|
41
|
+
with file_path.open("rb") as file:
|
|
42
|
+
parsed_file = tomllib.load(file)
|
|
43
|
+
except (ImportError, NameError):
|
|
44
|
+
import tomli
|
|
45
|
+
|
|
46
|
+
with file_path.open("rb") as file:
|
|
47
|
+
parsed_file = tomli.load(file)
|
|
48
|
+
|
|
49
|
+
result = parsed_file.get("tool", {}).get("setuptools-git-versioning", None)
|
|
50
|
+
if result:
|
|
51
|
+
log.log(DEBUG, "'tool.setuptools-git-versioning' section content:\n%s", pformat(result))
|
|
52
|
+
return result
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def infer_version(dist: Distribution, root: str | os.PathLike | None = None) -> Version | None:
|
|
56
|
+
log.log(INFO, "Trying 'setup.py' ...")
|
|
57
|
+
|
|
58
|
+
# see above
|
|
59
|
+
import setuptools # isort: skip # noqa: F401
|
|
60
|
+
|
|
61
|
+
from distutils.errors import DistutilsOptionError, DistutilsSetupError
|
|
62
|
+
|
|
63
|
+
config = getattr(dist, "setuptools_git_versioning", None)
|
|
64
|
+
toml_config = read_toml(root=root)
|
|
65
|
+
|
|
66
|
+
if config is None:
|
|
67
|
+
config = toml_config
|
|
68
|
+
elif toml_config:
|
|
69
|
+
msg = (
|
|
70
|
+
"Both 'setup.py' and 'pyproject.toml' have 'setuptools_git_versioning' config section. "
|
|
71
|
+
"Please remove one of them"
|
|
72
|
+
)
|
|
73
|
+
raise DistutilsSetupError(msg)
|
|
74
|
+
|
|
75
|
+
if config is None:
|
|
76
|
+
# Nothing to do here
|
|
77
|
+
return None
|
|
78
|
+
|
|
79
|
+
if not isinstance(config, dict):
|
|
80
|
+
msg = f"Wrong config format. Expected dict, got: {config}"
|
|
81
|
+
raise DistutilsOptionError(msg)
|
|
82
|
+
|
|
83
|
+
if not config or not config.pop("enabled", True):
|
|
84
|
+
# Nothing to do here
|
|
85
|
+
return None
|
|
86
|
+
|
|
87
|
+
set_default_options(config)
|
|
88
|
+
|
|
89
|
+
version = version_from_git(dist.metadata.name, **config, root=root)
|
|
90
|
+
dist.metadata.version = str(version)
|
|
91
|
+
return version
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def infer_setup_py(name_or_path: str = "setup.py", root: str | os.PathLike | None = None) -> Version | None:
|
|
95
|
+
project_root = Path(root) if root else Path.cwd()
|
|
96
|
+
setup_py_path = project_root.joinpath(name_or_path)
|
|
97
|
+
if not setup_py_path.exists():
|
|
98
|
+
log.log(INFO, "'%s' does not exist", setup_py_path)
|
|
99
|
+
return None
|
|
100
|
+
|
|
101
|
+
# because we use distutils in this file, we need to ensure that setuptools is
|
|
102
|
+
# imported first so that it can do monkey patching. this is not always already
|
|
103
|
+
# done for us, for example, when running this in a test or as a module
|
|
104
|
+
import setuptools # isort: skip # noqa: F401
|
|
105
|
+
|
|
106
|
+
from distutils.core import run_setup
|
|
107
|
+
|
|
108
|
+
# distutils does not change current directory, causing version of 'setuptools_git_versioning'
|
|
109
|
+
# is being get instead of target package.
|
|
110
|
+
# also some setup.py files can contain imports of other files from the package,
|
|
111
|
+
# and if they will be missing from sys.path, import will fail.
|
|
112
|
+
# emulating `python setup.py` call by modifying current dir and sys.path, but restore everything back after import
|
|
113
|
+
original_cwd = os.getcwd() # noqa: PTH109
|
|
114
|
+
original_sys_path = sys.path.copy()
|
|
115
|
+
original_sys_modules = sys.modules.copy()
|
|
116
|
+
try:
|
|
117
|
+
add_to_sys_path(project_root)
|
|
118
|
+
os.chdir(project_root)
|
|
119
|
+
dist = run_setup(os.fspath(setup_py_path), stop_after="init")
|
|
120
|
+
return infer_version(dist, root=root)
|
|
121
|
+
finally:
|
|
122
|
+
sys.path[:] = original_sys_path
|
|
123
|
+
sys.modules = original_sys_modules
|
|
124
|
+
os.chdir(original_cwd)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def get_version(config: dict | None = None, root: str | os.PathLike | None = None) -> Version:
|
|
128
|
+
if not config:
|
|
129
|
+
log.log(INFO, "No explicit config passed")
|
|
130
|
+
log.log(INFO, "Searching for config files in '%s' folder", root or Path.cwd())
|
|
131
|
+
result = infer_setup_py(root=root)
|
|
132
|
+
if result is not None:
|
|
133
|
+
return result
|
|
134
|
+
|
|
135
|
+
config = read_toml(root=root)
|
|
136
|
+
|
|
137
|
+
if not config or not config.pop("enabled", True):
|
|
138
|
+
raise RuntimeError(
|
|
139
|
+
textwrap.dedent(
|
|
140
|
+
f"""
|
|
141
|
+
'setuptools-git-versioning' command can be used only
|
|
142
|
+
with 'pyproject.toml' or 'setup.py' file present in folder '{root or Path.cwd()}',
|
|
143
|
+
containing the 'enabled: True' setting
|
|
144
|
+
(see https://setuptools-git-versioning.readthedocs.io/en/stable/install.html)
|
|
145
|
+
""",
|
|
146
|
+
),
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
set_default_options(config)
|
|
150
|
+
return version_from_git(**config, root=root)
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import os
|
|
5
|
+
import re
|
|
6
|
+
from datetime import datetime, timezone
|
|
7
|
+
from pprint import pformat
|
|
8
|
+
|
|
9
|
+
from setuptools_git_versioning.log import DEBUG
|
|
10
|
+
|
|
11
|
+
ENV_VARS_REGEXP = re.compile(r"\{env:(?P<name>[^:}]+):?(?P<default>[^}]+\}*)?\}", re.IGNORECASE | re.UNICODE)
|
|
12
|
+
TIMESTAMP_REGEXP = re.compile(r"\{timestamp:?(?P<fmt>[^:}]+)?\}", re.IGNORECASE | re.UNICODE)
|
|
13
|
+
|
|
14
|
+
log = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def substitute_env_variables(template: str) -> str:
|
|
18
|
+
log.log(DEBUG, "Substitute environment variables in template %r", template)
|
|
19
|
+
for var, default_value in ENV_VARS_REGEXP.findall(template):
|
|
20
|
+
log.log(DEBUG, "Variable: %r", var)
|
|
21
|
+
|
|
22
|
+
default = default_value
|
|
23
|
+
if default_value.upper() == "IGNORE":
|
|
24
|
+
default = ""
|
|
25
|
+
elif not default_value:
|
|
26
|
+
default = "UNKNOWN"
|
|
27
|
+
log.log(DEBUG, "Default: %r", default)
|
|
28
|
+
|
|
29
|
+
value = os.environ.get(var, default)
|
|
30
|
+
log.log(DEBUG, "Value: %r", value)
|
|
31
|
+
|
|
32
|
+
template, _ = ENV_VARS_REGEXP.subn(value, template, count=1)
|
|
33
|
+
|
|
34
|
+
log.log(DEBUG, "Result: %r", template)
|
|
35
|
+
return template
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def substitute_timestamp(template: str) -> str:
|
|
39
|
+
log.log(DEBUG, "Substitute timestamps in template %r", template)
|
|
40
|
+
|
|
41
|
+
now = datetime.now(tz=timezone.utc).astimezone()
|
|
42
|
+
for fmt in TIMESTAMP_REGEXP.findall(template):
|
|
43
|
+
format_string = fmt or "%s"
|
|
44
|
+
log.log(DEBUG, "Format: %r", format_string)
|
|
45
|
+
|
|
46
|
+
result = now.strftime(format_string)
|
|
47
|
+
log.log(DEBUG, "Value: %r", result)
|
|
48
|
+
|
|
49
|
+
template, _ = TIMESTAMP_REGEXP.subn(result, template, count=1)
|
|
50
|
+
|
|
51
|
+
log.log(DEBUG, "Result: %r", template)
|
|
52
|
+
return template
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def resolve_substitutions(template: str, *args, **kwargs) -> str:
|
|
56
|
+
log.log(DEBUG, "Template: %r", template)
|
|
57
|
+
log.log(DEBUG, "Args:%s", pformat(args))
|
|
58
|
+
|
|
59
|
+
while True:
|
|
60
|
+
if "{env" in template:
|
|
61
|
+
new_template = substitute_env_variables(template)
|
|
62
|
+
if new_template == template:
|
|
63
|
+
break
|
|
64
|
+
template = new_template
|
|
65
|
+
else:
|
|
66
|
+
break
|
|
67
|
+
|
|
68
|
+
if "{timestamp" in template:
|
|
69
|
+
template = substitute_timestamp(template)
|
|
70
|
+
|
|
71
|
+
return template.format(*args, **kwargs)
|