Skip to content

Commit

Permalink
utils: split the loading and processing of the config
Browse files Browse the repository at this point in the history
* A future rose stem interface will need to inject itself
  between config loading and processing.
* So colocate the plugin processing logic into one function.
  • Loading branch information
oliver-sanders committed Nov 1, 2023
1 parent aa79c43 commit d1ff77d
Show file tree
Hide file tree
Showing 8 changed files with 154 additions and 92 deletions.
18 changes: 15 additions & 3 deletions cylc/rose/entry_points.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@
from cylc.rose.utilities import (
copy_config_file,
dump_rose_log,
get_rose_vars,
export_environment,
load_rose_config,
process_config,
record_cylc_install_options,
rose_config_exists,
)
Expand All @@ -32,12 +34,22 @@ def pre_configure(srcdir=None, opts=None, rundir=None) -> dict:
if not srcdir:
# not sure how this could happen
return {
# default return value
'env': {},
'template_variables': {},
'templating_detected': None
}
srcdir: Path = Path(srcdir)
return get_rose_vars(srcdir=srcdir, opts=opts)

# load the Rose config
config_tree = load_rose_config(Path(srcdir), opts=opts)

# extract plugin return information from the Rose config
plugin_result = process_config(config_tree)

# set environment variables
export_environment(plugin_result['env'])

return plugin_result


def post_install(srcdir=None, opts=None, rundir=None) -> Union[dict, bool]:
Expand Down
1 change: 1 addition & 0 deletions cylc/rose/fileinstall.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ def rose_fileinstall(rundir: 'Path', opts: 'Values'):
return False

# Load the config tree
# TODO: private
config_tree = rose_config_tree_loader(rundir, opts)

if any(i.startswith('file') for i in config_tree.node.value):
Expand Down
18 changes: 14 additions & 4 deletions cylc/rose/stem.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,14 @@
from metomi.rose.reporter import Event, Reporter
from metomi.rose.resource import ResourceLocator

from cylc.rose.entry_points import get_rose_vars
from cylc.rose.utilities import id_templating_section
from cylc.rose.entry_points import (
export_environment,
load_rose_config,
)
from cylc.rose.utilities import (
id_templating_section,
process_config,
)

EXC_EXIT = cparse('<red><bold>{name}: </bold>{exc}</red>')
DEFAULT_TEST_DIR = 'rose-stem'
Expand Down Expand Up @@ -456,8 +462,12 @@ def process(self):
self.opts.project.append(project)

if i == 0:
template_type = get_rose_vars(
Path(url) / "rose-stem")["templating_detected"]
config_tree = load_rose_config(Path(url) / "rose-stem")
plugin_result = process_config(config_tree)
# set environment variables
export_environment(plugin_result['env'])
template_type = plugin_result['templating_detected']

self.template_section = id_templating_section(
template_type, with_brackets=True)

Expand Down
103 changes: 60 additions & 43 deletions cylc/rose/utilities.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
import re
import shlex
import shutil
from typing import Any, List, Optional, Tuple, Union
from typing import Any, Dict, List, Optional, Tuple, Union

from cylc.flow import LOG
from cylc.flow.exceptions import CylcError
Expand All @@ -39,6 +39,7 @@
ConfigNodeDiff,
)
from metomi.rose.config_processor import ConfigProcessError
from metomi.rose.config_tree import ConfigTree
from metomi.rose.env import UnboundEnvironmentVariableError, env_var_process

from cylc.rose.jinja2_parser import Parser, patch_jinja2_leading_zeros
Expand Down Expand Up @@ -73,28 +74,41 @@ class InvalidDefineError(CylcError):
...


def get_rose_vars_from_config_node(config, config_node, environ=os.environ):
"""Load template variables from a Rose config node.
def process_config(
config_tree: 'ConfigTree',
environ=os.environ,
) -> Dict[str, Any]:
"""Process template and environment variables.
This uses only the provided config node and environment variables
- there is no system interaction.
Note:
This uses only the provided config node and environment variables,
there is no system interaction.
Args:
config (dict):
Object which will be populated with the results.
config_node (metomi.rose.config.ConfigNode):
config_tree:
Configuration node representing the Rose suite configuration.
environ (dict):
environ:
Dictionary of environment variables (for testing).
"""
plugin_result: Dict[str, Any] = {
# default return value
'env': {},
'template_variables': {},
'templating_detected': None
}
config_node = config_tree.node

# Don't allow multiple templating sections.
templating = identify_templating_section(config_node)

if templating != 'template variables':
config['templating_detected'] = templating.replace(':suite.rc', '')
plugin_result['templating_detected'] = templating.replace(
':suite.rc',
'',
)
else:
config['templating_detected'] = templating
plugin_result['templating_detected'] = templating

# Create env section if it doesn't already exist.
if 'env' not in config_node.value:
Expand Down Expand Up @@ -151,29 +165,29 @@ def get_rose_vars_from_config_node(config, config_node, environ=os.environ):

# For each of the template language sections extract items to a simple
# dict to be returned.
config['env'] = {
plugin_result['env'] = {
item[0][1]: item[1].value for item in
config_node.value['env'].walk()
if item[1].state == ConfigNode.STATE_NORMAL
}
config['template_variables'] = {
plugin_result['template_variables'] = {
item[0][1]: item[1].value for item in
config_node.value[templating].walk()
if item[1].state == ConfigNode.STATE_NORMAL
}

# Add the entire config to ROSE_SUITE_VARIABLES to allow for programatic
# access.
# Add the entire plugin_result to ROSE_SUITE_VARIABLES to allow for
# programatic access.
with patch_jinja2_leading_zeros():
# BACK COMPAT: patch_jinja2_leading_zeros
# back support zero-padded integers for a limited time to help
# users migrate before upgrading cylc-flow to Jinja2>=3.1
parser = Parser()
for key, value in config['template_variables'].items():
for key, value in plugin_result['template_variables'].items():
# The special variables are already Python variables.
if key not in ['ROSE_ORIG_HOST', 'ROSE_VERSION', 'ROSE_SITE']:
try:
config['template_variables'][key] = (
plugin_result['template_variables'][key] = (
parser.literal_eval(value)
)
except Exception:
Expand All @@ -185,9 +199,11 @@ def get_rose_vars_from_config_node(config, config_node, environ=os.environ):
' (note strings "must be quoted").'
) from None

# Add ROSE_SUITE_VARIABLES to config of templating engines in use.
config['template_variables'][
'ROSE_SUITE_VARIABLES'] = config['template_variables']
# Add ROSE_SUITE_VARIABLES to plugin_result of templating engines in use.
plugin_result['template_variables'][
'ROSE_SUITE_VARIABLES'] = plugin_result['template_variables']

return plugin_result


def identify_templating_section(config_node):
Expand Down Expand Up @@ -789,19 +805,27 @@ def deprecation_warnings(config_tree):
LOG.warning(info[MESSAGE])


def get_rose_vars(srcdir=None, opts=None):
"""Load template variables from Rose suite configuration.
def load_rose_config(
srcdir: Path,
opts=None,
warn: bool = True,
) -> 'ConfigTree':
"""Load rose configuration from srcdir.
Load template variables from Rose suite configuration.
Loads the Rose suite configuration tree from the filesystem
using the shell environment.
Args:
srcdir(pathlib.Path):
srcdir:
Path to the Rose suite configuration
(the directory containing the ``rose-suite.conf`` file).
opts:
Options object containing specification of optional
configuarations set by the CLI.
warn:
Log deprecation warnings if True.
Returns:
dict - A dictionary of sections of rose-suite.conf.
Expand All @@ -815,43 +839,36 @@ def get_rose_vars(srcdir=None, opts=None):
}
}
"""
# Set up blank page for returns.
config = {
'env': {},
'template_variables': {},
'templating_detected': None
}

# Return a blank config dict if srcdir does not exist
if not rose_config_exists(srcdir):
if (
getattr(opts, "opt_conf_keys", None)
or getattr(opts, "defines", None)
or getattr(opts, "rose_template_vars", None)
opts
and (
getattr(opts, "opt_conf_keys", None)
or getattr(opts, "defines", None)
or getattr(opts, "rose_template_vars", None)
)
):
raise NotARoseSuiteException()
return config
return ConfigTree()

# Check for definitely invalid defines
if opts and hasattr(opts, 'defines'):
invalid_defines_check(opts.defines)

# Load the raw config tree
config_tree = rose_config_tree_loader(srcdir, opts)
deprecation_warnings(config_tree)
if warn:
deprecation_warnings(config_tree)

# Extract templatevars from the configuration
get_rose_vars_from_config_node(
config,
config_tree.node,
)
return config_tree


def export_environment(environment):
# Export environment vars
for key, val in config['env'].items():
for key, val in environment.items():
os.environ[key] = val

return config


def record_cylc_install_options(
rundir=None,
Expand Down
8 changes: 4 additions & 4 deletions tests/functional/test_pre_configure.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
import pytest
from pytest import param

from cylc.rose.utilities import NotARoseSuiteException, get_rose_vars
from cylc.rose.utilities import NotARoseSuiteException, load_rose_config


@pytest.mark.parametrize(
Expand Down Expand Up @@ -121,7 +121,7 @@ def test_process(srcdir, envvars):
def test_warn_if_root_dir_set(root_dir_config, tmp_path, caplog):
"""Test using unsupported root-dir config raises error."""
(tmp_path / 'rose-suite.conf').write_text(root_dir_config)
get_rose_vars(srcdir=tmp_path)
load_rose_config(tmp_path)
msg = 'rose-suite.conf[root-dir]'
assert msg in caplog.records[0].msg

Expand Down Expand Up @@ -150,7 +150,7 @@ def test_warn_if_old_templating_set(
'cylc.rose.utilities.cylc7_back_compat', compat_mode
)
(tmp_path / 'rose-suite.conf').write_text(f'[{rose_config}]')
get_rose_vars(srcdir=tmp_path)
load_rose_config(tmp_path)
msg = "Use [template variables]"
if compat_mode:
assert not caplog.records
Expand All @@ -174,7 +174,7 @@ def test_fail_if_options_but_no_rose_suite_conf(opts, tmp_path):
NotARoseSuiteException,
match='^Cylc-Rose CLI'
):
get_rose_vars(tmp_path, opts)
load_rose_config(tmp_path, opts)


def generate_params():
Expand Down
Loading

0 comments on commit d1ff77d

Please sign in to comment.