Skip to content

Commit f7f6d79

Browse files
committed
Jinja2 templating in configurations.
Should allow more power and eventually get rid of the eval()
1 parent 29105be commit f7f6d79

File tree

4 files changed

+99
-50
lines changed

4 files changed

+99
-50
lines changed

bin/lib/config_expand.py

+61
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
from typing import Any, Mapping
2+
3+
import jinja2
4+
5+
MAX_ITERS = 5
6+
7+
JINJA_ENV = jinja2.Environment()
8+
9+
10+
def is_list_of_strings(value: Any) -> bool:
11+
return isinstance(value, list) and all(isinstance(x, str) for x in value)
12+
13+
14+
# dear god it's late and this can't really be sensible, right?
15+
def is_list_of_strings_or_lists(value: Any) -> bool:
16+
return isinstance(value, list) and all(isinstance(x, str) or is_list_of_strings_or_lists(x) for x in value)
17+
18+
19+
def is_value_type(value: Any) -> bool:
20+
return isinstance(value, str) \
21+
or isinstance(value, bool) \
22+
or isinstance(value, float) \
23+
or isinstance(value, int) \
24+
or is_list_of_strings(value) \
25+
or is_list_of_strings_or_lists(value)
26+
27+
28+
def needs_expansion(target):
29+
for value in target.values():
30+
if is_list_of_strings(value):
31+
for v in value:
32+
if '{' in v:
33+
return True
34+
elif isinstance(value, str):
35+
if '{' in value:
36+
return True
37+
return False
38+
39+
40+
def expand_one(template_string, configuration):
41+
jinjad = JINJA_ENV.from_string(template_string).render(**configuration)
42+
return jinjad.format(**configuration)
43+
44+
45+
def expand_target(target: Mapping[str, Any], context):
46+
iterations = 0
47+
while needs_expansion(target):
48+
iterations += 1
49+
if iterations > MAX_ITERS:
50+
raise RuntimeError(f"Too many mutual references (in {'/'.join(context)})")
51+
for key, value in target.items():
52+
try:
53+
if is_list_of_strings(value):
54+
target[key] = [expand_one(x, target) for x in value]
55+
elif isinstance(value, str):
56+
target[key] = expand_one(value, target)
57+
elif isinstance(value, float):
58+
target[key] = str(value)
59+
except KeyError as ke:
60+
raise RuntimeError(f"Unable to find key {ke} in {target[key]} (in {'/'.join(context)})") from ke
61+
return target

bin/lib/installation.py

+5-49
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,13 @@
2121
from cachecontrol.caches import FileCache
2222

2323
from lib.amazon import list_compilers, list_s3_artifacts
24+
from lib.config_expand import is_value_type, expand_target
2425
from lib.config_safe_loader import ConfigSafeLoader
2526
from lib.library_build_config import LibraryBuildConfig
2627
from lib.library_builder import LibraryBuilder
2728

2829
VERSIONED_RE = re.compile(r'^(.*)-([0-9.]+)$')
2930

30-
MAX_ITERS = 5
31-
3231
NO_DEFAULT = "__no_default__"
3332

3433
logger = logging.getLogger(__name__)
@@ -685,6 +684,9 @@ def __init__(self, install_context: InstallationContext, config: Dict[str, Any])
685684
strip_components = self.config_get("strip_components", 0)
686685
if strip_components:
687686
self.tar_cmd += ['--strip-components', str(strip_components)]
687+
extract_only = self.config_get("extract_only", "")
688+
if extract_only:
689+
self.tar_cmd += [extract_only]
688690
self.strip = self.config_get('strip', False)
689691
self._setup_check_exe(self.install_path)
690692
if self.install_path_symlink:
@@ -862,36 +864,6 @@ def targets_from(node, enabled, base_config=None):
862864
return _targets_from(node, enabled, [], "", base_config)
863865

864866

865-
def is_list_of_strings(value: Any) -> bool:
866-
return isinstance(value, list) and all(isinstance(x, str) for x in value)
867-
868-
869-
# dear god it's late and this can't really be sensible, right?
870-
def is_list_of_strings_or_lists(value: Any) -> bool:
871-
return isinstance(value, list) and all(isinstance(x, str) or is_list_of_strings_or_lists(x) for x in value)
872-
873-
874-
def is_value_type(value: Any) -> bool:
875-
return isinstance(value, str) \
876-
or isinstance(value, bool) \
877-
or isinstance(value, float) \
878-
or isinstance(value, int) \
879-
or is_list_of_strings(value) \
880-
or is_list_of_strings_or_lists(value)
881-
882-
883-
def needs_expansion(target):
884-
for value in target.values():
885-
if is_list_of_strings(value):
886-
for v in value:
887-
if '{' in v:
888-
return True
889-
elif isinstance(value, str):
890-
if '{' in value:
891-
return True
892-
return False
893-
894-
895867
def _targets_from(node, enabled, context, name, base_config):
896868
if not node:
897869
return
@@ -932,23 +904,7 @@ def _targets_from(node, enabled, context, name, base_config):
932904
raise RuntimeError(f"Target {target} was parsed as a float. Enclose in quotes")
933905
if isinstance(target, str):
934906
target = {'name': target}
935-
target = ChainMap(target, base_config)
936-
iterations = 0
937-
while needs_expansion(target):
938-
iterations += 1
939-
if iterations > MAX_ITERS:
940-
raise RuntimeError(f"Too many mutual references (in {'/'.join(context)})")
941-
for key, value in target.items():
942-
try:
943-
if is_list_of_strings(value):
944-
target[key] = [x.format(**target) for x in value]
945-
elif isinstance(value, str):
946-
target[key] = value.format(**target)
947-
elif isinstance(value, float):
948-
target[key] = str(value)
949-
except KeyError as ke:
950-
raise RuntimeError(f"Unable to find key {ke} in {target[key]} (in {'/'.join(context)})") from ke
951-
yield target
907+
yield expand_target(ChainMap(target, base_config), context)
952908

953909

954910
INSTALLER_TYPES = {

bin/test/installation_test.py

+31
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,37 @@ def test_nested_expansion():
110110
assert second['architectures'] == [['AAA'], 'DDD']
111111

112112

113+
def test_jinja_expansion():
114+
[target] = parse_targets("""
115+
compilers:
116+
targets:
117+
- name: 5.4.0
118+
spleen: '{{ name }}'
119+
""")
120+
assert target['spleen'] == '5.4.0'
121+
122+
123+
def test_jinja_expansion_with_filters():
124+
[target] = parse_targets("""
125+
compilers:
126+
targets:
127+
- name: 5.4.0
128+
spleen: "{{ name | replace('.', '_') }}"
129+
""")
130+
assert target['spleen'] == '5_4_0'
131+
132+
133+
def test_jinja_expansion_with_filters_refering_forward():
134+
[target] = parse_targets("""
135+
boost:
136+
underscore_name: "{{ name | replace('.', '_') }}"
137+
url: https://dl.bintray.com/boostorg/release/{name}/source/boost_{underscore_name}.tar.bz2
138+
targets:
139+
- 1.64.0
140+
""")
141+
assert target['url'] == 'https://dl.bintray.com/boostorg/release/1.64.0/source/boost_1_64_0.tar.bz2'
142+
143+
113144
@pytest.fixture(name='fake_context')
114145
def fake_context_fixture():
115146
return MagicMock(spec=InstallationContext)

requirements.txt

+2-1
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,5 @@ cachecontrol[filecache]
55
pyyaml
66
pytest
77
mypy
8-
pylint
8+
pylint
9+
jinja2

0 commit comments

Comments
 (0)