Skip to content

Commit 02616bc

Browse files
committed
Allow Cylc Config to output a metadata JSON.
Response to review - Ensure --json help message is useful. - Make the null value reporting only happen to actual null values. Prevent --json being combined with print.. options Tidy up
1 parent 33788b3 commit 02616bc

File tree

4 files changed

+202
-5
lines changed

4 files changed

+202
-5
lines changed

cylc/flow/parsec/OrderedDict.py

+13
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,19 @@ def prepend(self, key, value):
100100
self[key] = value
101101
self.move_to_end(key, last=False)
102102

103+
@staticmethod
104+
def repl_val(target, replace, replacement):
105+
"""Replace dictionary values with a string.
106+
107+
Designed to be used recursively.
108+
"""
109+
for key, val in target.items():
110+
if isinstance(val, dict):
111+
OrderedDictWithDefaults.repl_val(
112+
val, replace, replacement)
113+
elif val == replace:
114+
target[key] = replacement
115+
103116

104117
class DictTree:
105118
"""An object providing a single point of access to a tree of dicts.

cylc/flow/parsec/config.py

+40-3
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,11 @@
1515
# along with this program. If not, see <http://www.gnu.org/licenses/>.
1616

1717
from copy import deepcopy
18+
import json
1819
import re
20+
import sys
1921
from textwrap import dedent
20-
from typing import TYPE_CHECKING, Callable, Iterable, List, Optional
22+
from typing import TYPE_CHECKING, Callable, Iterable, List, Optional, TextIO
2123

2224
from cylc.flow.context_node import ContextNode
2325
from cylc.flow.parsec.exceptions import (
@@ -33,10 +35,12 @@
3335

3436
if TYPE_CHECKING:
3537
from optparse import Values
38+
from typing_extensions import Literal
3639

3740

3841
class ParsecConfig:
3942
"""Object wrapper for parsec functions."""
43+
META: "Literal['meta']" = 'meta'
4044

4145
def __init__(
4246
self,
@@ -162,7 +166,7 @@ def get(self, keys: Optional[Iterable[str]] = None, sparse: bool = False):
162166
return cfg
163167

164168
def idump(self, items=None, sparse=False, prefix='',
165-
oneline=False, none_str='', handle=None):
169+
oneline=False, none_str='', handle=None, json=False):
166170
"""
167171
items is a list of --item style inputs:
168172
'[runtime][foo]script'.
@@ -178,7 +182,40 @@ def idump(self, items=None, sparse=False, prefix='',
178182
mkeys.append(j)
179183
if null:
180184
mkeys = [[]]
181-
self.mdump(mkeys, sparse, prefix, oneline, none_str, handle=handle)
185+
if json:
186+
self.jdump(mkeys, sparse, oneline, none_str, handle=handle)
187+
else:
188+
self.mdump(mkeys, sparse, prefix, oneline, none_str, handle=handle)
189+
190+
def jdump(
191+
self,
192+
mkeys: Optional[Iterable] = None,
193+
sparse: bool = False,
194+
oneline: bool = False,
195+
none_str: Optional[str] = None,
196+
handle: Optional[TextIO] = None
197+
) -> None:
198+
"""Dump a config to JSON format.
199+
200+
Args:
201+
mkeys: Items to display.
202+
sparse: Only display user set items, not defaults.
203+
oneline: Output on a single line.
204+
none_str: Value to give instead of null.
205+
handle: Where to write the output.
206+
"""
207+
# Use json indent to control online output:
208+
indent = None if oneline else 4
209+
210+
for keys in mkeys or []:
211+
if not keys:
212+
keys = []
213+
cfg = self.get(keys, sparse)
214+
if none_str:
215+
cfg.repl_val(cfg, None, none_str)
216+
data = json.dumps(cfg, indent=indent)
217+
218+
print(data, file=handle or sys.stdout)
182219

183220
def mdump(self, mkeys=None, sparse=False, prefix='',
184221
oneline=False, none_str='', handle=None):

cylc/flow/scripts/config.py

+36-2
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,15 @@ def get_option_parser() -> COP:
110110
"overrides any settings it shares with those higher up."),
111111
action="store_true", default=False, dest="print_hierarchy")
112112

113+
parser.add_option(
114+
'--json',
115+
help=(
116+
'Returns config as JSON rather than Cylc Config format.'),
117+
default=False,
118+
action='store_true',
119+
dest='json'
120+
)
121+
113122
parser.add_option(icp_option)
114123

115124
platform_listing_options_group = parser.add_option_group(
@@ -139,6 +148,28 @@ def get_option_parser() -> COP:
139148
return parser
140149

141150

151+
def json_opt_check(parser, options):
152+
"""Return an error if --json and incompatible options used.
153+
"""
154+
not_with_json = {
155+
'--print-hierarchy': 'print_hierarchy',
156+
'--platform-names': 'print_platform_names',
157+
'--platforms': 'print_platforms'
158+
}
159+
160+
if not options.json:
161+
return
162+
163+
not_with_json = [
164+
name for name, dest
165+
in not_with_json.items()
166+
if options.__dict__[dest]]
167+
168+
if not_with_json:
169+
parser.error(
170+
f'--json incompatible with {" or ".join(not_with_json)}')
171+
172+
142173
def get_config_file_hierarchy(workflow_id: Optional[str] = None) -> List[str]:
143174
filepaths = [os.path.join(path, glbl_cfg().CONF_BASENAME)
144175
for _, path in glbl_cfg().conf_dir_hierarchy]
@@ -163,6 +194,7 @@ async def _main(
163194
options: 'Values',
164195
*ids,
165196
) -> None:
197+
json_opt_check(parser, options)
166198

167199
if options.print_platform_names and options.print_platforms:
168200
options.print_platform_names = False
@@ -188,7 +220,8 @@ async def _main(
188220
options.item,
189221
not options.defaults,
190222
oneline=options.oneline,
191-
none_str=options.none_str
223+
none_str=options.none_str,
224+
json=options.json,
192225
)
193226
return
194227

@@ -213,5 +246,6 @@ async def _main(
213246
options.item,
214247
not options.defaults,
215248
oneline=options.oneline,
216-
none_str=options.none_str
249+
none_str=options.none_str,
250+
json=options.json
217251
)
+113
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
#!/usr/bin/env bash
2+
# THIS FILE IS PART OF THE CYLC WORKFLOW ENGINE.
3+
# Copyright (C) NIWA & British Crown (Met Office) & Contributors.
4+
#
5+
# This program is free software: you can redistribute it and/or modify
6+
# it under the terms of the GNU General Public License as published by
7+
# the Free Software Foundation, either version 3 of the License, or
8+
# (at your option) any later version.
9+
#
10+
# This program is distributed in the hope that it will be useful,
11+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
# GNU General Public License for more details.
14+
#
15+
# You should have received a copy of the GNU General Public License
16+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
17+
#-------------------------------------------------------------------------------
18+
# Test cylc config can dump json files.
19+
# n.b. not heavily tested because most of this functionality
20+
# is from Standard library json.
21+
. "$(dirname "$0")/test_header"
22+
#-------------------------------------------------------------------------------
23+
set_test_number 9
24+
#-------------------------------------------------------------------------------
25+
26+
# Test that option parser errors if incompat options given:
27+
cylc config --json --platforms 2> err
28+
named_grep_ok "${TEST_NAME_BASE}.CLI-one-incompat-item" \
29+
"--json incompatible with --platforms" \
30+
err
31+
32+
cylc config --json --platforms --platform-names 2> err
33+
named_grep_ok "${TEST_NAME_BASE}.CLI-two-incompat-items" \
34+
"--json incompatible with --platform-names or --platforms" \
35+
err
36+
37+
cylc config --json --platforms --platform-names --print-hierarchy 2> err
38+
named_grep_ok "${TEST_NAME_BASE}.CLI-three-incompat-items" \
39+
"--json incompatible with --print-hierarchy or --platform-names or --platforms" \
40+
err
41+
42+
43+
# Test the global.cylc
44+
TEST_NAME="${TEST_NAME_BASE}-global"
45+
46+
cat > "global.cylc" <<__HEREDOC__
47+
[platforms]
48+
[[golders_green]]
49+
[[[meta]]]
50+
can = "Test lots of things"
51+
because = metadata, is, not, fussy
52+
number = 99
53+
__HEREDOC__
54+
55+
export CYLC_CONF_PATH="${PWD}"
56+
run_ok "${TEST_NAME}" cylc config --json --one-line
57+
cmp_ok "${TEST_NAME}.stdout" <<__HERE__
58+
{"platforms": {"golders_green": {"meta": {"can": "Test lots of things", "because": "metadata, is, not, fussy", "number": "99"}}}}
59+
__HERE__
60+
61+
# Test a flow.cylc
62+
TEST_NAME="${TEST_NAME_BASE}-workflow"
63+
64+
cat > "flow.cylc" <<__HERE__
65+
[scheduling]
66+
[[graph]]
67+
P1D = foo
68+
69+
[runtime]
70+
[[foo]]
71+
__HERE__
72+
73+
run_ok "${TEST_NAME}" cylc config . --json --icp 1000
74+
cmp_ok "${TEST_NAME}.stdout" <<__HERE__
75+
{
76+
"scheduling": {
77+
"graph": {
78+
"P1D": "foo"
79+
},
80+
"initial cycle point": "1000"
81+
},
82+
"runtime": {
83+
"root": {},
84+
"foo": {
85+
"completion": "succeeded"
86+
}
87+
}
88+
}
89+
__HERE__
90+
91+
# Test an empty global.cylc to check:
92+
# * item selection
93+
# * null value setting
94+
# * showing defaults
95+
TEST_NAME="${TEST_NAME_BASE}-defaults-item-null-value"
96+
echo "" > global.cylc
97+
export CYLC_CONF_PATH="${PWD}"
98+
99+
run_ok "${TEST_NAME}" cylc config \
100+
-i '[scheduler][mail]' \
101+
--json \
102+
--defaults \
103+
--null-value='zilch'
104+
105+
cmp_ok "${TEST_NAME}.stdout" <<__HERE__
106+
{
107+
"from": "zilch",
108+
"smtp": "zilch",
109+
"to": "zilch",
110+
"footer": "zilch",
111+
"task event batch interval": 300.0
112+
}
113+
__HERE__

0 commit comments

Comments
 (0)