Skip to content

Commit 545fb44

Browse files
committed
Allow passing nested values to env substitution
1 parent 889438d commit 545fb44

File tree

5 files changed

+193
-50
lines changed

5 files changed

+193
-50
lines changed

CHANGELOG.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,11 @@ Changelog
4747

4848
Add ``tag_formatter`` option
4949

50+
.. change::
51+
:tags: core, feature
52+
53+
Allow nested default values to be passed to ``env`` substitution
54+
5055
.. change::
5156
:tags: tests, feature
5257

docs/substitutions/env.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,5 @@ You can pass 2 positional options to this substitution:
2222
- no default value meaus that variable will be replaced with literal ``"UNKNOWN"``
2323
- ``some value`` - just plain text
2424
- ``{ccount}`` - any other substitution is supported (**without nesting**)
25+
- ``{env:MISSINGVAR:{ccount}}`` - nested too
2526
- ``IGNORE`` - just substitute empty string

setuptools_git_versioning.py

Lines changed: 41 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,12 @@
1515
from setuptools.dist import Distribution
1616
from six.moves import collections_abc
1717

18-
DEFAULT_TEMPLATE = "{tag}" # type: str
19-
DEFAULT_DEV_TEMPLATE = "{tag}.post{ccount}+git.{sha}" # type: str
20-
DEFAULT_DIRTY_TEMPLATE = "{tag}.post{ccount}+git.{sha}.dirty" # type: str
18+
DEFAULT_TEMPLATE = "{tag}"
19+
DEFAULT_DEV_TEMPLATE = "{tag}.post{ccount}+git.{sha}"
20+
DEFAULT_DIRTY_TEMPLATE = "{tag}.post{ccount}+git.{sha}.dirty"
2121
DEFAULT_STARTING_VERSION = "0.0.1"
22-
ENV_VARS_REGEXP = re.compile(r"\{env:([^:}]+):?([^}]+}?)?\}", re.IGNORECASE | re.UNICODE) # type: re.Pattern
23-
TIMESTAMP_REGEXP = re.compile(r"\{timestamp:?([^:}]+)?\}", re.IGNORECASE | re.UNICODE) # type: re.Pattern
22+
ENV_VARS_REGEXP = re.compile(r"\{env:(?P<name>[^:}]+):?(?P<default>[^}]+\}*)?\}", re.IGNORECASE | re.UNICODE)
23+
TIMESTAMP_REGEXP = re.compile(r"\{timestamp:?(?P<fmt>[^:}]+)?\}", re.IGNORECASE | re.UNICODE)
2424

2525
DEFAULT_CONFIG = {
2626
"template": DEFAULT_TEMPLATE,
@@ -195,30 +195,50 @@ def read_version_from_file(path): # type: (Union[str, os.PathLike]) -> str
195195
return file.read().strip()
196196

197197

198-
def subst_env_variables(template): # type: (str) -> str
199-
if "{env" in template:
200-
for var, default in ENV_VARS_REGEXP.findall(template):
201-
if default.upper() == "IGNORE":
202-
default = ""
203-
elif not default:
204-
default = "UNKNOWN"
198+
def substitute_env_variables(template): # type: (str) -> str
199+
for var, default in ENV_VARS_REGEXP.findall(template):
200+
if default.upper() == "IGNORE":
201+
default = ""
202+
elif not default:
203+
default = "UNKNOWN"
205204

206-
value = os.environ.get(var, default)
207-
template, _ = ENV_VARS_REGEXP.subn(value, template, count=1)
205+
log.warning(var)
206+
log.warning(default)
207+
value = os.environ.get(var, default)
208+
log.warning(os.environ)
209+
log.warning(value)
210+
template, _ = ENV_VARS_REGEXP.subn(value, template, count=1)
211+
log.warning(template)
208212

209213
return template
210214

211215

212-
def subst_timestamp(template): # type: (str) -> str
213-
if "{timestamp" in template:
214-
now = datetime.now()
215-
for fmt in TIMESTAMP_REGEXP.findall(template):
216-
result = now.strftime(fmt or "%s")
217-
template, _ = TIMESTAMP_REGEXP.subn(result, template, count=1)
216+
def substitute_timestamp(template): # type: (str) -> str
217+
now = datetime.now()
218+
for fmt in TIMESTAMP_REGEXP.findall(template):
219+
result = now.strftime(fmt or "%s")
220+
template, _ = TIMESTAMP_REGEXP.subn(result, template, count=1)
218221

219222
return template
220223

221224

225+
def resolve_substitutions(template, *args, **kwargs): # type: (str, *Any, **Any) -> str
226+
while True:
227+
if "{env" in template:
228+
new_template = substitute_env_variables(template)
229+
if new_template == template:
230+
break
231+
else:
232+
template = new_template
233+
else:
234+
break
235+
236+
if "{timestamp" in template:
237+
template = substitute_timestamp(template)
238+
239+
return template.format(*args, **kwargs)
240+
241+
222242
def import_reference(
223243
ref, # type: str
224244
package_name=None, # Optional[str]
@@ -433,10 +453,7 @@ def version_from_git(
433453
else:
434454
t = template
435455

436-
t = subst_env_variables(t)
437-
t = subst_timestamp(t)
438-
439-
version = t.format(sha=full_sha[:8], tag=tag, ccount=ccount, branch=branch, full_sha=full_sha)
456+
version = resolve_substitutions(t, sha=full_sha[:8], tag=tag, ccount=ccount, branch=branch, full_sha=full_sha)
440457

441458
# Ensure local version label only contains permitted characters
442459
public, sep, local = version.partition("+")

tests/conftest.py

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import toml
1010
import uuid
1111

12-
from typing import Any, Dict, Optional
12+
from typing import Any, Callable, Dict, Optional
1313

1414
log = logging.getLogger(__name__)
1515
root = os.path.dirname(os.path.dirname(__file__))
@@ -163,16 +163,24 @@ def create_config(request):
163163
return request.param
164164

165165

166-
def typed_config(repo, config_creator, config_type, template=None, config=None):
166+
def typed_config(
167+
repo, # type: str
168+
config_creator, # type: Callable
169+
config_type, # type: str
170+
template=None, # type: Optional[str]
171+
template_name=None, # type: Optional[str]
172+
config=None, # type: Optional[dict]
173+
):
167174
if config_type == "tag":
168175
cfg = {}
169176
else:
170177
cfg = {"version_file": "VERSION.txt", "count_commits_from_version_file": True}
171178

172-
if config_type == "tag":
173-
template_name = "template"
174-
else:
175-
template_name = "dev_template"
179+
if template_name is None:
180+
if config_type == "tag":
181+
template_name = "template"
182+
else:
183+
template_name = "dev_template"
176184

177185
if template:
178186
cfg[template_name] = template

tests/test_substitution.py

Lines changed: 132 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,42 @@
11
from datetime import datetime
22
import pytest
33
import re
4+
import subprocess
45

5-
from tests.conftest import execute, get_version
6+
from tests.conftest import execute, get_version_setup_py, create_file, create_setup_py
67

78
pytestmark = pytest.mark.all
89

910

11+
@pytest.mark.parametrize(
12+
"template",
13+
[
14+
"{tag}",
15+
"{tag}+a{tag}",
16+
],
17+
)
18+
def test_substitution_tag(repo, template):
19+
create_setup_py(repo, {"template": template})
20+
execute(repo, "git tag 1.2.3")
21+
22+
assert get_version_setup_py(repo) == template.format(tag="1.2.3")
23+
24+
25+
@pytest.mark.parametrize(
26+
"dev_template",
27+
[
28+
"{tag}.{ccount}",
29+
"{tag}.{ccount}+a{ccount}",
30+
],
31+
)
32+
def test_substitution_ccount(repo, dev_template):
33+
create_setup_py(repo, {"dev_template": dev_template})
34+
execute(repo, "git tag 1.2.3")
35+
create_file(repo)
36+
37+
assert get_version_setup_py(repo) == dev_template.format(tag="1.2.3", ccount=1)
38+
39+
1040
@pytest.mark.parametrize(
1141
"branch, suffix",
1242
[
@@ -18,45 +48,103 @@
1848
("post", ".post"),
1949
],
2050
)
21-
def test_substitution_branch(repo, template_config, create_config, branch, suffix):
51+
@pytest.mark.parametrize(
52+
"template, real_template",
53+
[
54+
("{tag}{branch}", "{tag}{suffix}0"),
55+
("{tag}{branch}+a{branch}", "{tag}{suffix}0+a{branch}"),
56+
],
57+
)
58+
def test_substitution_branch(repo, template, real_template, branch, suffix):
2259
execute(repo, "git checkout -b {branch}".format(branch=branch))
23-
template_config(repo, create_config, template="{tag}{branch}0")
60+
create_setup_py(repo, {"template": template})
61+
execute(repo, "git tag 1.2.3")
2462

25-
assert get_version(repo) == "1.2.3{suffix}0".format(suffix=suffix)
63+
assert get_version_setup_py(repo) == real_template.format(tag="1.2.3", branch=branch, suffix=suffix)
2664

2765

2866
@pytest.mark.parametrize(
29-
"template, pipeline_id, suffix",
67+
"dev_template, pipeline_id, suffix",
3068
[
3169
# leading zeros are removed by setuptools
32-
("{tag}.post{env:PIPELINE_ID:123}", "0234", "234"),
70+
("{tag}.post{env:PIPELINE_ID}", "234", "234"),
71+
("{tag}.post{env:PIPELINE_ID}", "0234", "234"),
72+
("{tag}.post{env:PIPELINE_ID}", None, "UNKNOWN"),
73+
("{tag}.post{env:PIPELINE_ID}+abc{env:ANOTHER_ENV}", "234", "234+abc3.4.5"),
74+
("{tag}.post{env:PIPELINE_ID}+abc{env:ANOTHER_ENV}", None, "UNKNOWN+abc3.4.5"),
75+
("{tag}.post{env:PIPELINE_ID}+abc{env:MISSING_ENV}", "234", "234+abcunknown"),
76+
("{tag}.post{env:PIPELINE_ID}+abc{env:MISSING_ENV}", None, "UNKNOWN+abcUNKNOWN"),
3377
("{tag}.post{env:PIPELINE_ID:123}", "234", "234"),
3478
("{tag}.post{env:PIPELINE_ID:123}", None, "123"),
35-
("{tag}.post{env:PIPELINE_ID:IGNORE}", "0234", "234"),
79+
("{tag}.post{env:PIPELINE_ID:123}+abc{env:ANOTHER_ENV}", "234", "234+abc3.4.5"),
80+
("{tag}.post{env:PIPELINE_ID:123}+abc{env:ANOTHER_ENV}", None, "123+abc3.4.5"),
81+
("{tag}.post{env:PIPELINE_ID:123}+abc{env:MISSING_ENV}", "234", "234+abcunknown"),
82+
("{tag}.post{env:PIPELINE_ID:123}+abc{env:MISSING_ENV}", None, "123+abcunknown"),
83+
("{tag}.post{env:PIPELINE_ID:123}+abc{env:MISSING_ENV:5.6.7}", "234", "234+abc5.6.7"),
84+
("{tag}.post{env:PIPELINE_ID:123}+abc{env:MISSING_ENV:5.6.7}", None, "123+abc5.6.7"),
85+
("{tag}.post{env:PIPELINE_ID:123}+abc{env:MISSING_ENV:B.C:D}", "234", "234+abcb.c.d"), # allowed
86+
("{tag}.post{env:PIPELINE_ID:123}+abc{env:MISSING_ENV:B-C%D}", None, "123+abcb.c.d"),
3687
("{tag}.post{env:PIPELINE_ID:IGNORE}", "234", "234"),
3788
("{tag}.post{env:PIPELINE_ID:IGNORE}", None, "0"),
38-
("{tag}.post{env:PIPELINE_ID:{ccount}}", "0234", "234"),
89+
("{tag}.post{env:PIPELINE_ID:IGNORE}+abc{env:ANOTHER_ENV}", "234", "234+abc3.4.5"),
90+
("{tag}.post{env:PIPELINE_ID:IGNORE}+abc{env:ANOTHER_ENV}", None, "0+abc3.4.5"),
91+
("{tag}.post{env:PIPELINE_ID:IGNORE}+abc{env:MISSING_ENV:5.6.7}", "234", "234+abc5.6.7"),
92+
("{tag}.post{env:PIPELINE_ID:IGNORE}+abc{env:MISSING_ENV:5.6.7}", None, "0+abc5.6.7"),
93+
("{tag}.post{env:PIPELINE_ID:IGNORE}+abc{env:MISSING_ENV:IGNORE}", "234", "234+abc"),
94+
("{tag}.post{env:PIPELINE_ID:IGNORE}+abc{env:MISSING_ENV:IGNORE}", None, "0+abc"),
3995
("{tag}.post{env:PIPELINE_ID:{ccount}}", "234", "234"),
40-
("{tag}.post{env:PIPELINE_ID:{ccount}}", None, "0"),
41-
("{tag}.post{env:PIPELINE_ID}", "0234", "234"),
42-
("{tag}.post{env:PIPELINE_ID}", "234", "234"),
43-
("{tag}.post{env:PIPELINE_ID}", None, "UNKNOWN"),
96+
("{tag}.post{env:PIPELINE_ID:{ccount}}", None, "1"),
97+
("{tag}.post{env:PIPELINE_ID:{timestamp:%Y}}", "234", "234"),
98+
("{tag}.post{env:PIPELINE_ID:{timestamp:%Y}}", None, datetime.now().year),
99+
("{tag}.post{env:PIPELINE_ID:{ccount}}+abc{env:ANOTHER_ENV}", "234", "234+abc3.4.5"),
100+
("{tag}.post{env:PIPELINE_ID:{ccount}}+abc{env:ANOTHER_ENV}", None, "1+abc3.4.5"),
101+
("{tag}.post{env:PIPELINE_ID:{ccount}}+abc{env:MISSING_ENV:5.6.7}", "234", "234+abc5.6.7"),
102+
("{tag}.post{env:PIPELINE_ID:{ccount}}+abc{env:MISSING_ENV:5.6.7}", None, "1+abc5.6.7"),
103+
("{tag}.post{env:PIPELINE_ID:{ccount}}+abc{env:MISSING_ENV:IGNORE}", "234", "234+abc"),
104+
("{tag}.post{env:PIPELINE_ID:{ccount}}+abc{env:MISSING_ENV:IGNORE}", None, "1+abc"),
105+
("{tag}.post{env:PIPELINE_ID:{ccount}}+abc{env:MISSING_ENV:{ccount}}", "234", "234+abc1"),
106+
("{tag}.post{env:PIPELINE_ID:{ccount}}+abc{env:MISSING_ENV:{ccount}}", None, "1+abc1"),
107+
(
108+
"{tag}.post{env:PIPELINE_ID:{ccount}}+abc{env:MISSING_ENV:{timestamp:%Y}}",
109+
"234",
110+
"234+abc" + str(datetime.now().year),
111+
),
112+
(
113+
"{tag}.post{env:PIPELINE_ID:{ccount}}+abc{env:MISSING_ENV:{timestamp:%Y}}",
114+
None,
115+
"1+abc" + str(datetime.now().year),
116+
),
117+
("{tag}.post{env:PIPELINE_ID:{env:ANOTHER_ENV}}", "234", "234"),
118+
("{tag}.post{env:PIPELINE_ID:{env:ANOTHER_ENV}}", None, "3.4.5"),
119+
("{tag}.post{env:PIPELINE_ID:{env:MISSING_ENV}}", "234", "234"),
120+
("{tag}.post{env:PIPELINE_ID:{env:MISSING_ENV}}", None, "UNKNOWN"),
121+
("{tag}.post{env:PIPELINE_ID:{env:MISSING_ENV:5.6.7}}", "234", "234"),
122+
("{tag}.post{env:PIPELINE_ID:{env:MISSING_ENV:5.6.7}}", None, "5.6.7"),
123+
("{tag}.post{env:PIPELINE_ID:{env:MISSING_ENV:{ccount}}}", "234", "234"),
124+
("{tag}.post{env:PIPELINE_ID:{env:MISSING_ENV:{ccount}}}", None, "1"),
125+
("{tag}.post{env:PIPELINE_ID:{env:MISSING_ENV:{timestamp:%Y}}}", "234", "234"),
126+
("{tag}.post{env:PIPELINE_ID:{env:MISSING_ENV:{timestamp:%Y}}}", None, datetime.now().year),
127+
# empty env variable name
128+
("{tag}.post{env: }", "234", "UNKNOWN"),
44129
],
45130
)
46-
def test_substitution_env(repo, template_config, create_config, template, pipeline_id, suffix):
47-
template_config(repo, create_config, template=template)
131+
def test_substitution_env(repo, dev_template, pipeline_id, suffix):
132+
create_setup_py(repo, {"dev_template": dev_template})
133+
execute(repo, "git tag 1.2.3")
134+
create_file(repo)
48135

49-
env = {}
136+
env = {"ANOTHER_ENV": "3.4.5"}
50137
if pipeline_id is not None:
51-
env = {"PIPELINE_ID": pipeline_id}
138+
env["PIPELINE_ID"] = pipeline_id
52139

53-
assert get_version(repo, env=env) == "1.2.3.post{suffix}".format(suffix=suffix)
140+
assert get_version_setup_py(repo, env=env) == "1.2.3.post{suffix}".format(suffix=suffix)
54141

55142

56143
@pytest.mark.parametrize(
57144
"template, fmt, callback",
58145
[
59146
("{tag}.post{timestamp}", "{tag}.post{}", lambda dt: (int(dt.strftime("%s")) // 100,)),
147+
("{tag}.post{timestamp:}", "{tag}.post{}", lambda dt: (int(dt.strftime("%s")) // 100,)),
60148
("{tag}.post{timestamp:%s}", "{tag}.post{}", lambda dt: (int(dt.strftime("%s")) // 100,)),
61149
(
62150
"{timestamp:%Y}.{timestamp:%m}.{timestamp:%d}+{timestamp:%H%M%S}",
@@ -68,10 +156,13 @@ def test_substitution_env(repo, template_config, create_config, template, pipeli
68156
"{tag}.post{ccount}+{}",
69157
lambda dt: (dt.strftime("%Y.%m.%dt%H.%M"),),
70158
),
159+
# unknown format
160+
("{tag}+git{timestamp:%i}", "{tag}+git.i", lambda x: []),
71161
],
72162
)
73-
def test_substitution_timestamp(repo, template_config, create_config, template, fmt, callback):
74-
template_config(repo, create_config, template=template)
163+
def test_substitution_timestamp(repo, template, fmt, callback):
164+
create_setup_py(repo, {"template": template})
165+
execute(repo, "git tag 1.2.3")
75166

76167
value = fmt.format(tag="1.2.3", ccount=0, *callback(datetime.now()))
77168
pattern = re.compile(r"([^\d\w])0+(\d+[^\d\w]|\d+$)")
@@ -81,4 +172,25 @@ def test_substitution_timestamp(repo, template_config, create_config, template,
81172
if new_value == value:
82173
break
83174
value = new_value
84-
assert new_value in get_version(repo)
175+
assert new_value in get_version_setup_py(repo)
176+
177+
178+
@pytest.mark.parametrize(
179+
"template",
180+
[
181+
"{tag}+a{env}",
182+
"{tag}+a{env:}",
183+
"{tag}+a{env:MISSING_ENV:{}",
184+
"{tag}+a{env:MISSING_ENV:{{}}",
185+
"{tag}+a{env:MISSING_ENV:}}",
186+
"{tag}+a{env:MISSING_ENV:{}}}",
187+
"{tag}+a{timestamp:A:B}",
188+
"{tag}+a{timestamp:{%Y}",
189+
],
190+
)
191+
def test_substitution_wrong_format(repo, template):
192+
create_setup_py(repo, {"template": template})
193+
execute(repo, "git tag 1.2.3")
194+
195+
with pytest.raises(subprocess.CalledProcessError):
196+
get_version_setup_py(repo)

0 commit comments

Comments
 (0)