Skip to content

Commit 6795cbd

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 6182988 commit 6795cbd

File tree

4 files changed

+202
-5
lines changed

4 files changed

+202
-5
lines changed

cylc/flow/parsec/OrderedDict.py

Lines changed: 13 additions & 0 deletions
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

Lines changed: 40 additions & 3 deletions
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,6 +35,7 @@
3335

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

3740

3841
class DefaultList(list):
@@ -41,6 +44,7 @@ class DefaultList(list):
4144

4245
class ParsecConfig:
4346
"""Object wrapper for parsec functions."""
47+
META: "Literal['meta']" = 'meta'
4448

4549
def __init__(
4650
self,
@@ -166,7 +170,7 @@ def get(self, keys: Optional[Iterable[str]] = None, sparse: bool = False):
166170
return cfg
167171

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

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

cylc/flow/scripts/config.py

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

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

116125
platform_listing_options_group = parser.add_option_group(
@@ -140,6 +149,28 @@ def get_option_parser() -> COP:
140149
return parser
141150

142151

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

168200
if options.print_platform_names and options.print_platforms:
169201
options.print_platform_names = False
@@ -189,7 +221,8 @@ async def _main(
189221
options.item,
190222
not options.defaults,
191223
oneline=options.oneline,
192-
none_str=options.none_str
224+
none_str=options.none_str,
225+
json=options.json,
193226
)
194227
return
195228

@@ -219,5 +252,6 @@ async def _main(
219252
options.item,
220253
not options.defaults,
221254
oneline=options.oneline,
222-
none_str=options.none_str
255+
none_str=options.none_str,
256+
json=options.json
223257
)
Lines changed: 113 additions & 0 deletions
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)