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

Rejig #265

Merged
merged 12 commits into from
Nov 8, 2023
331 changes: 38 additions & 293 deletions cylc/rose/entry_points.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,320 +13,65 @@
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""Cylc support for reading and interpreting ``rose-suite.conf`` workflow
configuration files.

Top level module providing entry point functions.
"""

import os
import shutil
"""Top level module providing entry point functions."""

from pathlib import Path
from typing import TYPE_CHECKING

from metomi.rose.config import ConfigLoader, ConfigDumper
from cylc.rose.utilities import (
ROSE_ORIG_HOST_INSTALLED_OVERRIDE_STRING,
deprecation_warnings,
copy_config_file,
dump_rose_log,
get_rose_vars_from_config_node,
identify_templating_section,
invalid_defines_check,
export_environment,
load_rose_config,
process_config,
record_cylc_install_options,
rose_config_exists,
rose_config_tree_loader,
merge_rose_cylc_suite_install_conf,
paths_to_pathlib,
get_cli_opts_node,
add_cylc_install_to_rose_conf_node_opts,
)
from cylc.flow.hostuserutil import get_host


class NotARoseSuiteException(Exception):
def __str__(self):
msg = (
'Cylc-Rose CLI arguments only used '
'if a rose-suite.conf file is present:'
'\n * "--opt-conf-key" or "-O"'
'\n * "--define" or "-D"'
'\n * "--rose-template-variable" or "-S"'
)
return msg


def pre_configure(srcdir=None, opts=None, rundir=None):
srcdir, rundir = paths_to_pathlib([srcdir, rundir])
return get_rose_vars(srcdir=srcdir, opts=opts)


def post_install(srcdir=None, opts=None, rundir=None):
if not rose_config_exists(srcdir, opts):
return False
srcdir, rundir = paths_to_pathlib([srcdir, rundir])
results = {}
copy_config_file(srcdir=srcdir, rundir=rundir)
results['record_install'] = record_cylc_install_options(
srcdir=srcdir, opts=opts, rundir=rundir
)
results['fileinstall'] = rose_fileinstall(
srcdir=srcdir, opts=opts, rundir=rundir
)
# Finally dump a log of the rose-conf in its final state.
if results['fileinstall']:
dump_rose_log(rundir=rundir, node=results['fileinstall'])

return results


def get_rose_vars(srcdir=None, opts=None):
"""Load template variables from Rose suite configuration.

Loads the Rose suite configuration tree from the filesystem
using the shell environment.

Args:
srcdir(pathlib.Path):
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.

Returns:
dict - A dictionary of sections of rose-suite.conf.
For each section either a dictionary or None is returned.
E.g.
{
'env': {'MYVAR': 42},
'empy:suite.rc': None,
'jinja2:suite.rc': {
'myJinja2Var': {'yes': 'it is a dictionary!'}
}
}
"""
# 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, opts):
if (
getattr(opts, "opt_conf_keys", None)
or getattr(opts, "defines", None)
or getattr(opts, "rose_template_vars", None)
):
raise NotARoseSuiteException()
return config

# 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)

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

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

return config

if TYPE_CHECKING:
from cylc.flow.option_parsers import Values

def record_cylc_install_options(
rundir=None,
opts=None,
srcdir=None,
):
"""Create/modify files recording Cylc install config options.

Creates a new config based on CLI options and writes it to the workflow
install location as ``rose-suite-cylc-install.conf``.
def pre_configure(srcdir: Path, opts: 'Values') -> dict:
"""Run before the Cylc configuration is read."""
# load the Rose config
config_tree = load_rose_config(Path(srcdir), opts=opts)

If ``rose-suite-cylc-install.conf`` already exists over-writes changed
items, except for ``!opts=`` which is merged and simplified.
# extract plugin return information from the Rose config
plugin_result = process_config(config_tree)

If ``!opts=`` have been changed these are appended to those that have
been written in the installed ``rose-suite.conf``.
# set environment variables
export_environment(plugin_result['env'])

Args:
srcdir (pathlib.Path):
Used to check whether the source directory contains a rose config.
opts:
Cylc option parser object - we want to extract the following
values:
- opt_conf_keys (list or str):
Equivalent of ``rose suite-run --option KEY``
- defines (list of str):
Equivalent of ``rose suite-run --define KEY=VAL``
- rose_template_vars (list of str):
Equivalent of ``rose suite-run --define-suite KEY=VAL``
rundir (pathlib.Path):
Path to dump the rose-suite-cylc-conf
return plugin_result

Returns:
cli_config - Config Node which has been dumped to
``rose-suite-cylc-install.conf``.
rose_suite_conf['opts'] - Opts section of the config node dumped to
installed ``rose-suite.conf``.
"""
# Create a config based on command line options:
cli_config = get_cli_opts_node(opts, srcdir)

# raise error if CLI config has multiple templating sections
identify_templating_section(cli_config)
def post_install(srcdir: Path, rundir: str, opts: 'Values') -> bool:
"""Run after Cylc file installation has completed."""
from cylc.rose.fileinstall import rose_fileinstall

# Construct path objects representing our target files.
(Path(rundir) / 'opt').mkdir(exist_ok=True)
conf_filepath = Path(rundir) / 'opt/rose-suite-cylc-install.conf'
rose_conf_filepath = Path(rundir) / 'rose-suite.conf'
dumper = ConfigDumper()
loader = ConfigLoader()

# If file exists we need to merge with our new config, over-writing with
# new items where there are duplicates.
if conf_filepath.is_file():
if opts.clear_rose_install_opts:
conf_filepath.unlink()
else:
oldconfig = loader.load(str(conf_filepath))
# Check old config for clashing template variables sections.
identify_templating_section(oldconfig)
cli_config = merge_rose_cylc_suite_install_conf(
oldconfig, cli_config
)

# Get Values for standard ROSE variable ROSE_ORIG_HOST.
rose_orig_host = get_host()
for section in [
'env', 'jinja2:suite.rc', 'empy:suite.rc', 'template variables'
]:
if section in cli_config:
cli_config[section].set(['ROSE_ORIG_HOST'], rose_orig_host)
cli_config[section]['ROSE_ORIG_HOST'].comments = [
ROSE_ORIG_HOST_INSTALLED_OVERRIDE_STRING
]

cli_config.comments = [' This file records CLI Options.']
dumper.dump(cli_config, str(conf_filepath))

# Merge the opts section of the rose-suite.conf with those set by CLI:
rose_conf_filepath.touch()
rose_suite_conf = loader.load(str(rose_conf_filepath))
rose_suite_conf = add_cylc_install_to_rose_conf_node_opts(
rose_suite_conf, cli_config
)
identify_templating_section(rose_suite_conf)

dumper(rose_suite_conf, rose_conf_filepath)

return cli_config, rose_suite_conf


def rose_fileinstall(srcdir=None, opts=None, rundir=None):
"""Call Rose Fileinstall.

Args:
srcdir(pathlib.Path):
Search for a ``rose-suite.conf`` file in this location.
rundir (pathlib.Path)

"""
if not rose_config_exists(rundir, opts):
if not rose_config_exists(srcdir):
# nothing to do here
return False
_rundir: Path = Path(rundir)

# Load the config tree
config_tree = rose_config_tree_loader(rundir, opts)
# transfer the rose-suite.conf file
copy_config_file(srcdir=srcdir, rundir=_rundir)

if any(i.startswith('file') for i in config_tree.node.value):
try:
startpoint = os.getcwd()
os.chdir(rundir)
except FileNotFoundError as exc:
raise exc
else:
# Carry out imports.
import asyncio
from metomi.rose.config_processor import ConfigProcessorsManager
from metomi.rose.popen import RosePopener
from metomi.rose.reporter import Reporter
from metomi.rose.fs_util import FileSystemUtil
# write cylc-install CLI options to an optional config
record_cylc_install_options(srcdir, _rundir, opts)

# Update config tree with install location
# NOTE-TO-SELF: value=os.environ["CYLC_WORKFLOW_RUN_DIR"]
config_tree.node = config_tree.node.set(
keys=["file-install-root"], value=str(rundir)
)
# perform file installation
config_node = rose_fileinstall(_rundir, opts)
if config_node:
dump_rose_log(rundir=_rundir, node=config_node)

# Artificially set rose to verbose.
event_handler = Reporter(3)
fs_util = FileSystemUtil(event_handler)
popen = RosePopener(event_handler)

# Get an Asyncio loop if one doesn't exist:
# Rose may need an event loop to invoke async interfaces,
# doing this here incase we want to go async in cylc-rose.
# See https://github.com/cylc/cylc-rose/pull/130/files
try:
asyncio.get_event_loop()
except RuntimeError:
asyncio.set_event_loop(asyncio.new_event_loop())

# Process fileinstall.
config_pm = ConfigProcessorsManager(event_handler, popen, fs_util)
config_pm(config_tree, "file")
finally:
os.chdir(startpoint)

return config_tree.node


def copy_config_file(
srcdir=None,
opts=None,
rundir=None,
):
"""Copy the ``rose-suite.conf`` from a workflow source to run directory.

Args:
srcdir (pathlib.Path | or str):
Source Path of Cylc install.
opts:
Not used in this function, but requried for consistent entry point.
rundir (pathlib.Path | or str):
Destination path of Cylc install - the workflow rundir.
return True

Return:
True if ``rose-suite.conf`` has been installed.
False if insufficiant information to install file given.
"""
if (
rundir is None or
srcdir is None
):
raise FileNotFoundError(
"This plugin requires both source and rundir to exist."
)

rundir = Path(rundir)
srcdir = Path(srcdir)
srcdir_rose_conf = srcdir / 'rose-suite.conf'
rundir_rose_conf = rundir / 'rose-suite.conf'
def rose_stem():
"""Implements the "rose stem" command."""
from cylc.rose.stem import get_rose_stem_opts

Check warning on line 74 in cylc/rose/entry_points.py

View check run for this annotation

Codecov / codecov/patch

cylc/rose/entry_points.py#L74

Added line #L74 was not covered by tests

if not srcdir_rose_conf.is_file():
return False
elif rundir_rose_conf.is_file():
rundir_rose_conf.unlink()
shutil.copy2(srcdir_rose_conf, rundir_rose_conf)

return True
parser, opts = get_rose_stem_opts()
rose_stem(parser, opts)

Check warning on line 77 in cylc/rose/entry_points.py

View check run for this annotation

Codecov / codecov/patch

cylc/rose/entry_points.py#L76-L77

Added lines #L76 - L77 were not covered by tests
Loading