Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cylc Config json output mode #6275

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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:
OrderedDictWithDefaults.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__
Loading