Skip to content

Commit 5d0715e

Browse files
authored
Add CLI option to render construct.yaml files (#1263)
* Add recipe render CLI * Add unit tests * Add news
1 parent 71a39b1 commit 5d0715e

4 files changed

Lines changed: 161 additions & 15 deletions

File tree

constructor/construct.py

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from functools import partial
1717
from os.path import dirname
1818
from pathlib import Path
19+
from typing import TYPE_CHECKING
1920

2021
from jsonschema import Draft202012Validator, validators
2122
from jsonschema.exceptions import ValidationError
@@ -24,6 +25,9 @@
2425
from constructor.exceptions import UnableToParse, UnableToParseMissingJinja2, YamlParsingError
2526
from constructor.utils import yaml
2627

28+
if TYPE_CHECKING:
29+
import os
30+
2731
logger = logging.getLogger(__name__)
2832

2933
HERE = Path(__file__).parent
@@ -92,32 +96,31 @@ def select_lines(data, namespace):
9296
return "\n".join(lines) + "\n"
9397

9498

95-
# adapted from conda-build
96-
def yamlize(data, directory, content_filter):
99+
def render(path: os.PathLike, platform: str) -> str:
100+
try:
101+
with open(path) as fi:
102+
data = fi.read()
103+
except OSError:
104+
sys.exit("Error: could not open '%s' for reading" % path)
105+
directory = dirname(path)
106+
content_filter = partial(select_lines, namespace=ns_platform(platform))
97107
data = content_filter(data)
98108
try:
99-
return yaml.load(data)
109+
yaml.load(data)
110+
return data
100111
except YAMLError as e:
101112
if ("{{" not in data) and ("{%" not in data):
102113
raise UnableToParse(original=e)
103114
try:
104115
from constructor.jinja import render_jinja_for_input_file
105116
except ImportError as ex:
106117
raise UnableToParseMissingJinja2(original=ex)
107-
data = render_jinja_for_input_file(data, directory, content_filter)
108-
return yaml.load(data)
118+
return render_jinja_for_input_file(data, directory, content_filter)
109119

110120

111-
def parse(path, platform):
112-
try:
113-
with open(path) as fi:
114-
data = fi.read()
115-
except OSError:
116-
sys.exit("Error: could not open '%s' for reading" % path)
117-
directory = dirname(path)
118-
content_filter = partial(select_lines, namespace=ns_platform(platform))
121+
def parse(path: os.PathLike, platform: str):
119122
try:
120-
res = yamlize(data, directory, content_filter)
123+
res = yaml.load(render(path, platform))
121124
except YamlParsingError as e:
122125
sys.exit(e.error_msg())
123126

constructor/main.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
from .conda_interface import VersionOrder as Version
2929
from .construct import SCHEMA_PATH, ns_platform
3030
from .construct import parse as construct_parse
31+
from .construct import render as construct_render
3132
from .construct import verify as construct_verify
3233
from .fcp import main as fcp_main
3334
from .utils import StandaloneExe, check_version, identify_conda_exe, normalize_path, yield_lines
@@ -544,6 +545,11 @@ def main(argv=None):
544545
default=False,
545546
action="store_true",
546547
)
548+
p.add_argument(
549+
"--render",
550+
help="Parse and render the construct.yaml file",
551+
action="store_true",
552+
)
547553

548554
p.add_argument("-v", "--verbose", action="store_true")
549555

@@ -581,7 +587,8 @@ def main(argv=None):
581587
)
582588

583589
args = p.parse_args(argv)
584-
logger.info("Got the following cli arguments: '%s'", args)
590+
if not args.render:
591+
logger.info("Got the following cli arguments: '%s'", args)
585592

586593
if args.verbose or args.debug:
587594
logging.getLogger("constructor").setLevel(logging.DEBUG)
@@ -604,6 +611,11 @@ def main(argv=None):
604611
if not os.path.isfile(full_config_path):
605612
p.error("no such file: %s" % full_config_path)
606613

614+
if args.render:
615+
platform = args.platform or cc_platform
616+
print(construct_render(full_config_path, platform=platform))
617+
return
618+
607619
conda_exe = args.conda_exe
608620
conda_exe_default_path = os.path.join(sys.prefix, "standalone_conda", "conda.exe")
609621
conda_exe_default_path = normalize_path(conda_exe_default_path)

news/1263-render-cli

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
### Enhancements
2+
3+
* Add `constructor --render` CLI option to render `construct.yaml` files. (#1263)
4+
5+
### Bug fixes
6+
7+
* <news item>
8+
9+
### Deprecations
10+
11+
* <news item>
12+
13+
### Docs
14+
15+
* <news item>
16+
17+
### Other
18+
19+
* <news item>

tests/test_construct.py

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
from __future__ import annotations
2+
3+
from typing import TYPE_CHECKING
4+
5+
import pytest
6+
7+
from constructor.conda_interface import cc_platform
8+
from constructor.construct import parse as construct_parse
9+
from constructor.construct import render as construct_render
10+
11+
if TYPE_CHECKING:
12+
from pathlib import Path
13+
14+
15+
CONSTRUCT_YAML = """\
16+
name: Installer
17+
version: 1.0.0
18+
specs:
19+
- python
20+
- miniforge_console_shortcut # [win]
21+
"""
22+
23+
CONSTRUCT_YAML_JINJA = """\
24+
name: Installer
25+
version: 1.0.0
26+
specs:
27+
- python
28+
{%- if os.environ.get("__CONSTRUCTOR_INCLUDE_CONDA__") %}
29+
- conda
30+
{%- endif %}
31+
"""
32+
33+
CONSTRUCY_YAML_BROKEN = """\
34+
name: Installer
35+
version: 1.0.0
36+
specs
37+
"""
38+
39+
40+
@pytest.fixture
41+
def construct_yaml_file(tmp_path: Path) -> str:
42+
file_path = tmp_path / "construct.yaml"
43+
file_path.write_text(CONSTRUCT_YAML)
44+
return str(file_path)
45+
46+
47+
@pytest.fixture
48+
def construct_yaml_file_jinja(tmp_path: Path) -> str:
49+
file_path = tmp_path / "construct.yaml"
50+
file_path.write_text(CONSTRUCT_YAML_JINJA)
51+
return str(file_path)
52+
53+
54+
@pytest.mark.parametrize("platform", ("linux-64", "win-64"))
55+
def test_render(platform: str, construct_yaml_file: Path):
56+
rendered = construct_render(construct_yaml_file, platform)
57+
rendered_lines = rendered.splitlines()
58+
expected = CONSTRUCT_YAML.splitlines()[:-1]
59+
if platform == "win-64":
60+
expected.append(" - miniforge_console_shortcut")
61+
assert rendered_lines == expected
62+
63+
64+
@pytest.mark.parametrize("platform", ("linux-64", "win-64"))
65+
def test_parse(platform: str, construct_yaml_file: Path):
66+
parsed = construct_parse(construct_yaml_file, platform)
67+
assert parsed["name"] == "Installer"
68+
assert parsed["version"] == "1.0.0"
69+
expected_specs = [
70+
"python",
71+
*(("miniforge_console_shortcut",) if platform == "win-64" else ()),
72+
]
73+
assert parsed["specs"] == expected_specs
74+
75+
76+
@pytest.mark.parametrize("include_conda", (True, False))
77+
def test_render_jinja(
78+
include_conda: bool, construct_yaml_file_jinja: Path, monkeypatch: pytest.MonkeyPatch
79+
):
80+
if include_conda:
81+
monkeypatch.setenv("__CONSTRUCTOR_INCLUDE_CONDA__", "1")
82+
rendered = construct_render(construct_yaml_file_jinja, cc_platform)
83+
rendered_lines = rendered.splitlines()
84+
expected = CONSTRUCT_YAML_JINJA.splitlines()[:-3]
85+
if include_conda:
86+
expected.append(" - conda")
87+
assert rendered_lines == expected
88+
89+
90+
@pytest.mark.parametrize("include_conda", (True, False))
91+
def test_parse_jinja(
92+
include_conda: bool, construct_yaml_file_jinja: Path, monkeypatch: pytest.MonkeyPatch
93+
):
94+
if include_conda:
95+
monkeypatch.setenv("__CONSTRUCTOR_INCLUDE_CONDA__", "1")
96+
parsed = construct_parse(construct_yaml_file_jinja, cc_platform)
97+
assert parsed["name"] == "Installer"
98+
assert parsed["version"] == "1.0.0"
99+
expected_specs = [
100+
"python",
101+
*(("conda",) if include_conda else ()),
102+
]
103+
assert parsed["specs"] == expected_specs
104+
105+
106+
def test_parse_error(tmp_path):
107+
construct_yaml_file = tmp_path / "construct.yaml"
108+
construct_yaml_file.write_text(CONSTRUCY_YAML_BROKEN)
109+
with pytest.raises(SystemExit) as exc:
110+
construct_parse(construct_yaml_file, cc_platform)
111+
assert exc.value.code != 0
112+
assert "Unable to parse" in str(exc.getrepr())

0 commit comments

Comments
 (0)