Skip to content

Commit 3d86396

Browse files
authored
Improve and simplify logic related to labels (#11)
1 parent 9fd4f92 commit 3d86396

File tree

2 files changed

+122
-52
lines changed

2 files changed

+122
-52
lines changed

_version.py

Lines changed: 77 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
"""
2-
_version.py v1.5
2+
_version.py v1.6
33
44
Simple version string management, using a hard-coded version string
55
for simplicity and compatibility, while adding git info at runtime.
@@ -38,51 +38,70 @@
3838

3939
def get_version() -> str:
4040
"""Get the version string."""
41-
if repo_dir:
42-
return get_extended_version()
43-
return __version__
41+
try:
42+
if repo_dir:
43+
release, post, tag, dirty = get_version_info_from_git()
44+
result = get_extended_version(release, post, tag, dirty)
4445

46+
# Warn if release does not match base_version.
47+
# Can happen between bumping and tagging. And also when merging a
48+
# version bump into a working branch, because we use --first-parent.
49+
if release and release != base_version:
50+
release2, _post, _tag, _dirty = get_version_info_from_git(
51+
first_parent=False
52+
)
53+
if release2 != base_version:
54+
warning(
55+
f"{project_name} version from git ({release})"
56+
f" and __version__ ({base_version}) don't match."
57+
)
4558

46-
def get_extended_version() -> str:
47-
"""Get an extended version string with information from git."""
48-
release, post, labels = get_version_info_from_git()
59+
return result
60+
61+
except Exception as err:
62+
# Failsafe.
63+
warning(f"Error getting refined version: {err}")
4964

50-
# Start version string (__version__ string is leading)
65+
return base_version
66+
67+
68+
def get_extended_version(release: str, post: str, tag: str, dirty: str) -> str:
69+
"""Get an extended version string with information from git."""
70+
# Start version string (__version__ string is leading).
5171
version = base_version
52-
tag_prefix = "#"
72+
labels = []
5373

5474
if release and release != base_version:
55-
# Can happen between bumping and tagging. And also when merging a
56-
# version bump into a working branch, because we use --first-parent.
57-
release2, _post, _labels = get_version_info_from_git(first_parent=False)
58-
if release2 != base_version:
59-
warning(
60-
f"{project_name} version from git ({release})"
61-
f" and __version__ ({base_version}) don't match."
62-
)
63-
version += "+from_tag_" + release.replace(".", "_")
64-
tag_prefix = "."
65-
66-
# Add git info
67-
if post and post != "0":
75+
pre_label = "from_tag_" + release.replace(".", "_")
76+
labels = [pre_label, f"post{post}", tag, dirty]
77+
elif post and post != "0":
6878
version += f".post{post}"
69-
if labels:
70-
version += tag_prefix + ".".join(labels)
71-
elif labels and labels[-1] == "dirty":
72-
version += tag_prefix + ".".join(labels)
73-
79+
labels = [tag, dirty]
80+
elif dirty:
81+
labels = [tag, dirty]
82+
else:
83+
# If not post and not dirty, show 'clean' version without git tag.
84+
pass
85+
86+
# Compose final version (remove empty labels, e.g. when not dirty).
87+
# Everything after the '+' is not sortable (does not get in version_info).
88+
label_str = ".".join(label for label in labels if label)
89+
if label_str:
90+
version += "+" + label_str
7491
return version
7592

7693

77-
def get_version_info_from_git(*, first_parent: bool = True) -> str:
94+
def get_version_info_from_git(
95+
*, first_parent: bool = True
96+
) -> tuple[str, str, str, str]:
7897
"""
79-
Get (release, post, labels) from Git.
98+
Get (release, post, tag, dirty) from Git.
8099
81100
With `release` the version number from the latest tag, `post` the
82-
number of commits since that tag, and `labels` a tuple with the
83-
git-hash and optionally a dirty flag.
101+
number of commits since that tag, `tag` the git hash, and `dirty` a string
102+
that is either empty or says 'dirty'.
84103
"""
85-
# Call out to Git
104+
# Call out to Git.
86105
command = ["git", "describe", "--long", "--always", "--tags", "--dirty"]
87106
if first_parent:
88107
command.append("--first-parent")
@@ -92,9 +111,9 @@ def get_version_info_from_git(*, first_parent: bool = True) -> str:
92111
warning(f"Could not get {project_name} version: {e}")
93112
p = None
94113

95-
# Parse the result into parts
114+
# Parse the result into parts.
96115
if p is None:
97-
parts = (None, None, "unknown")
116+
parts = ("", "", "unknown")
98117
else:
99118
output = p.stdout.decode(errors="ignore")
100119
if p.returncode:
@@ -105,40 +124,50 @@ def get_version_info_from_git(*, first_parent: bool = True) -> str:
105124
+ "\n\nstderr: "
106125
+ stderr
107126
)
108-
parts = (None, None, "unknown")
127+
parts = ("", "", "unknown")
109128
else:
110129
parts = output.strip().lstrip("v").split("-")
111130
if len(parts) <= 2:
112131
# No tags (and thus no post). Only git hash and maybe 'dirty'.
113-
parts = (None, None, *parts)
132+
parts = ("", "", *parts)
114133

115-
# Return unpacked parts
116-
release, post, *labels = parts
117-
return release, post, labels
134+
# Return unpacked parts.
135+
release = parts[0]
136+
post = parts[1]
137+
tag = parts[2]
138+
dirty = "dirty" if len(parts) > 3 else ""
139+
return release, post, tag, dirty
118140

119141

120142
def version_to_tuple(v: str) -> tuple:
121143
parts = []
122-
for part in v.split("."):
123-
p, _, h = part.partition("#")
124-
if p:
125-
parts.append(p)
126-
if h:
127-
parts.append("#" + h)
128-
return tuple(int(i) if i.isnumeric() else i for i in parts)
144+
for part in v.split("+", maxsplit=1)[0].split("."):
145+
if not part:
146+
pass
147+
elif part.startswith("post"):
148+
try:
149+
parts.extend(["post", int(part[4:])])
150+
except ValueError:
151+
parts.append(part)
152+
else:
153+
try:
154+
parts.append(int(part))
155+
except ValueError:
156+
parts.append(part)
157+
return tuple(parts)
129158

130159

131160
def warning(m: str) -> None:
132161
logger.warning(m)
133162

134163

135-
# Apply the versioning
164+
# Apply the versioning.
136165
base_version = __version__
137166
__version__ = get_version()
138167
version_info = version_to_tuple(__version__)
139168

140169

141-
# The CLI part
170+
# The CLI part.
142171

143172
CLI_USAGE = """
144173
_version.py

test_version.py

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,56 @@
77

88
import _version
99

10+
example_git_output = [
11+
(("", "", "", ""), "0.0.0"),
12+
(("", "", "unknown", ""), "0.0.0"),
13+
(("", "", "unknown", "dirty"), "0.0.0+unknown.dirty"),
14+
(("0.0.0", "", "abcd", ""), "0.0.0"),
15+
(("0.0.0", "", "abcd", "dirty"), "0.0.0+abcd.dirty"),
16+
(("0.0.0", "1", "abcd", ""), "0.0.0.post1+abcd"),
17+
(("0.0.0", "1", "abcd", "dirty"), "0.0.0.post1+abcd.dirty"),
18+
]
19+
20+
21+
def test_get_extended_version() -> None:
22+
"""Test get_extended_version function."""
23+
for args, ref in example_git_output:
24+
v = _version.get_extended_version(*args)
25+
assert v == ref, f"{args} -> {v} != {ref}"
26+
27+
28+
def test_failsafe() -> None:
29+
"""Test that in case anything errors, we don't crash."""
30+
ori_repo_dir = _version.repo_dir
31+
del _version.repo_dir
32+
33+
try:
34+
v = _version.get_version()
35+
assert v == "0.0.0"
36+
37+
finally:
38+
_version.repo_dir = ori_repo_dir
39+
40+
1041
example_versions = [
1142
("1", (1,)),
1243
("1.2", (1, 2)),
1344
("1.2.3", (1, 2, 3)),
1445
("1.2.3", (1, 2, 3)),
15-
("0.29.0.post4#g3175010", (0, 29, 0, "post4", "#g3175010")),
16-
("2.6.0#gcd877db.dirty", (2, 6, 0, "#gcd877db", "dirty")),
17-
("0.15.0.post16#g63b1a427.dirty", (0, 15, 0, "post16", "#g63b1a427", "dirty")),
18-
("1.#foo", (1, "#foo")),
46+
("1.2.3.post9", (1, 2, 3, "post", 9)),
47+
("1.2.3.post10", (1, 2, 3, "post", 10)),
48+
("0.29.0.post4+g3175010", (0, 29, 0, "post", 4)),
49+
(
50+
"2.6.0+gcd877db.dirty",
51+
(
52+
2,
53+
6,
54+
0,
55+
),
56+
),
57+
("0.15.0.post16+g63b1a427.dirty", (0, 15, 0, "post", 16)),
58+
("2.6.1+from_tag_2_6_0.post3.g72c1d22", (2, 6, 1)),
59+
("1.+foo", (1,)),
1960
]
2061

2162

0 commit comments

Comments
 (0)