Skip to content

Commit

Permalink
Allow Cylc Config to output a metadata JSON.
Browse files Browse the repository at this point in the history
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
  • Loading branch information
wxtim committed Aug 29, 2024
1 parent 33788b3 commit 02616bc
Show file tree
Hide file tree
Showing 4 changed files with 202 additions and 5 deletions.
13 changes: 13 additions & 0 deletions cylc/flow/parsec/OrderedDict.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,19 @@ def prepend(self, key, value):
self[key] = value
self.move_to_end(key, last=False)

@staticmethod
def repl_val(target, replace, replacement):
"""Replace dictionary values with a string.
Designed to be used recursively.
"""
for key, val in target.items():
if isinstance(val, dict):
OrderedDictWithDefaults.repl_val(
val, replace, replacement)
elif val == replace:
target[key] = replacement


class DictTree:
"""An object providing a single point of access to a tree of dicts.
Expand Down
43 changes: 40 additions & 3 deletions cylc/flow/parsec/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,11 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.

from copy import deepcopy
import json
import re
import sys
from textwrap import dedent
from typing import TYPE_CHECKING, Callable, Iterable, List, Optional
from typing import TYPE_CHECKING, Callable, Iterable, List, Optional, TextIO

from cylc.flow.context_node import ContextNode
from cylc.flow.parsec.exceptions import (
Expand All @@ -33,10 +35,12 @@

if TYPE_CHECKING:
from optparse import Values
from typing_extensions import Literal


class ParsecConfig:
"""Object wrapper for parsec functions."""
META: "Literal['meta']" = 'meta'

def __init__(
self,
Expand Down Expand Up @@ -162,7 +166,7 @@ def get(self, keys: Optional[Iterable[str]] = None, sparse: bool = False):
return cfg

def idump(self, items=None, sparse=False, prefix='',
oneline=False, none_str='', handle=None):
oneline=False, none_str='', handle=None, json=False):
"""
items is a list of --item style inputs:
'[runtime][foo]script'.
Expand All @@ -178,7 +182,40 @@ def idump(self, items=None, sparse=False, prefix='',
mkeys.append(j)
if null:
mkeys = [[]]
self.mdump(mkeys, sparse, prefix, oneline, none_str, handle=handle)
if json:
self.jdump(mkeys, sparse, oneline, none_str, handle=handle)
else:
self.mdump(mkeys, sparse, prefix, oneline, none_str, handle=handle)

def jdump(
self,
mkeys: Optional[Iterable] = None,
sparse: bool = False,
oneline: bool = False,
none_str: Optional[str] = None,
handle: Optional[TextIO] = None
) -> None:
"""Dump a config to JSON format.
Args:
mkeys: Items to display.
sparse: Only display user set items, not defaults.
oneline: Output on a single line.
none_str: Value to give instead of null.
handle: Where to write the output.
"""
# Use json indent to control online output:
indent = None if oneline else 4

for keys in mkeys or []:
if not keys:
keys = []
cfg = self.get(keys, sparse)
if none_str:
cfg.repl_val(cfg, None, none_str)
data = json.dumps(cfg, indent=indent)

print(data, file=handle or sys.stdout)

def mdump(self, mkeys=None, sparse=False, prefix='',
oneline=False, none_str='', handle=None):
Expand Down
38 changes: 36 additions & 2 deletions cylc/flow/scripts/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,15 @@ def get_option_parser() -> COP:
"overrides any settings it shares with those higher up."),
action="store_true", default=False, dest="print_hierarchy")

parser.add_option(
'--json',
help=(
'Returns config as JSON rather than Cylc Config format.'),
default=False,
action='store_true',
dest='json'
)

parser.add_option(icp_option)

platform_listing_options_group = parser.add_option_group(
Expand Down Expand Up @@ -139,6 +148,28 @@ def get_option_parser() -> COP:
return parser


def json_opt_check(parser, options):
"""Return an error if --json and incompatible options used.
"""
not_with_json = {
'--print-hierarchy': 'print_hierarchy',
'--platform-names': 'print_platform_names',
'--platforms': 'print_platforms'
}

if not options.json:
return

not_with_json = [
name for name, dest
in not_with_json.items()
if options.__dict__[dest]]

if not_with_json:
parser.error(
f'--json incompatible with {" or ".join(not_with_json)}')


def get_config_file_hierarchy(workflow_id: Optional[str] = None) -> List[str]:
filepaths = [os.path.join(path, glbl_cfg().CONF_BASENAME)
for _, path in glbl_cfg().conf_dir_hierarchy]
Expand All @@ -163,6 +194,7 @@ async def _main(
options: 'Values',
*ids,
) -> None:
json_opt_check(parser, options)

if options.print_platform_names and options.print_platforms:
options.print_platform_names = False
Expand All @@ -188,7 +220,8 @@ async def _main(
options.item,
not options.defaults,
oneline=options.oneline,
none_str=options.none_str
none_str=options.none_str,
json=options.json,
)
return

Expand All @@ -213,5 +246,6 @@ async def _main(
options.item,
not options.defaults,
oneline=options.oneline,
none_str=options.none_str
none_str=options.none_str,
json=options.json
)
113 changes: 113 additions & 0 deletions tests/functional/cylc-config/11-json-dump.t
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
#!/usr/bin/env bash
# THIS FILE IS PART OF THE CYLC WORKFLOW ENGINE.
# Copyright (C) NIWA & British Crown (Met Office) & Contributors.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#-------------------------------------------------------------------------------
# Test cylc config can dump json files.
# n.b. not heavily tested because most of this functionality
# is from Standard library json.
. "$(dirname "$0")/test_header"
#-------------------------------------------------------------------------------
set_test_number 9
#-------------------------------------------------------------------------------

# Test that option parser errors if incompat options given:
cylc config --json --platforms 2> err
named_grep_ok "${TEST_NAME_BASE}.CLI-one-incompat-item" \
"--json incompatible with --platforms" \
err

cylc config --json --platforms --platform-names 2> err
named_grep_ok "${TEST_NAME_BASE}.CLI-two-incompat-items" \
"--json incompatible with --platform-names or --platforms" \
err

cylc config --json --platforms --platform-names --print-hierarchy 2> err
named_grep_ok "${TEST_NAME_BASE}.CLI-three-incompat-items" \
"--json incompatible with --print-hierarchy or --platform-names or --platforms" \
err


# Test the global.cylc
TEST_NAME="${TEST_NAME_BASE}-global"

cat > "global.cylc" <<__HEREDOC__
[platforms]
[[golders_green]]
[[[meta]]]
can = "Test lots of things"
because = metadata, is, not, fussy
number = 99
__HEREDOC__

export CYLC_CONF_PATH="${PWD}"
run_ok "${TEST_NAME}" cylc config --json --one-line
cmp_ok "${TEST_NAME}.stdout" <<__HERE__
{"platforms": {"golders_green": {"meta": {"can": "Test lots of things", "because": "metadata, is, not, fussy", "number": "99"}}}}
__HERE__

# Test a flow.cylc
TEST_NAME="${TEST_NAME_BASE}-workflow"

cat > "flow.cylc" <<__HERE__
[scheduling]
[[graph]]
P1D = foo
[runtime]
[[foo]]
__HERE__

run_ok "${TEST_NAME}" cylc config . --json --icp 1000
cmp_ok "${TEST_NAME}.stdout" <<__HERE__
{
"scheduling": {
"graph": {
"P1D": "foo"
},
"initial cycle point": "1000"
},
"runtime": {
"root": {},
"foo": {
"completion": "succeeded"
}
}
}
__HERE__

# Test an empty global.cylc to check:
# * item selection
# * null value setting
# * showing defaults
TEST_NAME="${TEST_NAME_BASE}-defaults-item-null-value"
echo "" > global.cylc
export CYLC_CONF_PATH="${PWD}"

run_ok "${TEST_NAME}" cylc config \
-i '[scheduler][mail]' \
--json \
--defaults \
--null-value='zilch'

cmp_ok "${TEST_NAME}.stdout" <<__HERE__
{
"from": "zilch",
"smtp": "zilch",
"to": "zilch",
"footer": "zilch",
"task event batch interval": 300.0
}
__HERE__

0 comments on commit 02616bc

Please sign in to comment.