Skip to content

Commit ec8eec8

Browse files
authored
Do not allow plugins to act on global.cylc (#5839)
1 parent 994b774 commit ec8eec8

File tree

3 files changed

+74
-39
lines changed

3 files changed

+74
-39
lines changed

cylc/flow/parsec/fileparse.py

Lines changed: 36 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
value type is known).
3131
"""
3232

33+
from copy import deepcopy
3334
import os
3435
from pathlib import Path
3536
import re
@@ -51,6 +52,7 @@
5152

5253
if t.TYPE_CHECKING:
5354
from optparse import Values
55+
from typing import Union
5456

5557

5658
# heading/sections can contain commas (namespace name lists) and any
@@ -105,6 +107,13 @@
105107
_UNCLOSED_MULTILINE = re.compile(
106108
r'(?<![\w>])\[.*\]'
107109
)
110+
TEMPLATING_DETECTED = 'templating_detected'
111+
TEMPLATE_VARIABLES = 'template_variables'
112+
EXTRA_VARS_TEMPLATE: t.Dict[str, t.Any] = {
113+
'env': {},
114+
TEMPLATE_VARIABLES: {},
115+
TEMPLATING_DETECTED: None
116+
}
108117

109118

110119
def get_cylc_env_vars() -> t.Dict[str, str]:
@@ -242,7 +251,7 @@ def multiline(flines, value, index, maxline):
242251
return quot + newvalue + line, index
243252

244253

245-
def process_plugins(fpath, opts):
254+
def process_plugins(fpath: 'Union[str, Path]', opts: 'Values'):
246255
"""Run a Cylc pre-configuration plugin.
247256
248257
Plugins should return a dictionary containing:
@@ -253,22 +262,25 @@ def process_plugins(fpath, opts):
253262
or ``empy``.
254263
255264
args:
256-
fpath: Directory where the plugin will look for a config.
265+
fpath: Path to a config file. Plugin will look at the parent
266+
directory of this file.
257267
opts: Command line options to be passed to the plugin.
258268
259269
Returns: Dictionary in the form:
260270
extra_vars = {
261271
'env': {},
262272
'template_variables': {},
263-
'templating_detected': None
273+
TEMPLATING_DETECTED: None
264274
}
265275
"""
276+
fpath = Path(fpath)
277+
266278
# Set out blank dictionary for return:
267-
extra_vars = {
268-
'env': {},
269-
'template_variables': {},
270-
'templating_detected': None
271-
}
279+
extra_vars = deepcopy(EXTRA_VARS_TEMPLATE)
280+
281+
# Don't run this on global configs:
282+
if fpath.name == 'global.cylc':
283+
return extra_vars
272284

273285
# Run entry point pre_configure items, trying to merge values with each.:
274286
for entry_point in iter_entry_points(
@@ -278,7 +290,7 @@ def process_plugins(fpath, opts):
278290
# If you want it to work on sourcedirs you need to get the options
279291
# to here.
280292
plugin_result = entry_point.load()(
281-
srcdir=fpath, opts=opts
293+
srcdir=fpath.parent, opts=opts
282294
)
283295
except Exception as exc:
284296
# NOTE: except Exception (purposefully vague)
@@ -288,7 +300,7 @@ def process_plugins(fpath, opts):
288300
entry_point.name,
289301
exc
290302
) from None
291-
for section in ['env', 'template_variables']:
303+
for section in ['env', TEMPLATE_VARIABLES]:
292304
if section in plugin_result and plugin_result[section] is not None:
293305
# Raise error if multiple plugins try to update the same keys.
294306
section_update = plugin_result.get(section, {})
@@ -302,26 +314,20 @@ def process_plugins(fpath, opts):
302314
)
303315
extra_vars[section].update(section_update)
304316

317+
templating_detected = plugin_result.get(TEMPLATING_DETECTED, None)
305318
if (
306-
'templating_detected' in plugin_result and
307-
plugin_result['templating_detected'] is not None and
308-
extra_vars['templating_detected'] is not None and
309-
extra_vars['templating_detected'] !=
310-
plugin_result['templating_detected']
319+
templating_detected is not None
320+
and extra_vars[TEMPLATING_DETECTED] is not None
321+
and extra_vars[TEMPLATING_DETECTED] != templating_detected
311322
):
312323
# Don't allow subsequent plugins with different templating_detected
313324
raise ParsecError(
314325
"Can't merge templating languages "
315-
f"{extra_vars['templating_detected']} and "
316-
f"{plugin_result['templating_detected']}"
326+
f"{extra_vars[TEMPLATING_DETECTED]} and "
327+
f"{templating_detected}"
317328
)
318-
elif (
319-
'templating_detected' in plugin_result and
320-
plugin_result['templating_detected'] is not None
321-
):
322-
extra_vars['templating_detected'] = plugin_result[
323-
'templating_detected'
324-
]
329+
else:
330+
extra_vars[TEMPLATING_DETECTED] = templating_detected
325331

326332
return extra_vars
327333

@@ -352,8 +358,8 @@ def merge_template_vars(
352358
>>> merge_template_vars(a, b)
353359
{'FOO': 42, 'BAZ': 3.14159, 'BAR': 'Hello World'}
354360
"""
355-
if plugin_result['templating_detected'] is not None:
356-
plugin_tvars = plugin_result['template_variables']
361+
if plugin_result[TEMPLATING_DETECTED] is not None:
362+
plugin_tvars = plugin_result[TEMPLATE_VARIABLES]
357363
will_be_overwritten = (
358364
native_tvars.keys() &
359365
plugin_tvars.keys()
@@ -456,7 +462,7 @@ def read_and_proc(
456462
do_jinja2 = True
457463
do_contin = True
458464

459-
extra_vars = process_plugins(Path(fpath).parent, opts)
465+
extra_vars = process_plugins(fpath, opts)
460466

461467
if not template_vars:
462468
template_vars = {}
@@ -483,12 +489,12 @@ def read_and_proc(
483489

484490
# Fail if templating_detected ≠ hashbang
485491
process_with = hashbang_and_plugin_templating_clash(
486-
extra_vars['templating_detected'], flines
492+
extra_vars[TEMPLATING_DETECTED], flines
487493
)
488494
# process with EmPy
489495
if do_empy:
490496
if (
491-
extra_vars['templating_detected'] == 'empy' and
497+
extra_vars[TEMPLATING_DETECTED] == 'empy' and
492498
not process_with and
493499
process_with != 'empy'
494500
):
@@ -508,7 +514,7 @@ def read_and_proc(
508514
# process with Jinja2
509515
if do_jinja2:
510516
if (
511-
extra_vars['templating_detected'] == 'jinja2' and
517+
extra_vars[TEMPLATING_DETECTED] == 'jinja2' and
512518
not process_with and
513519
process_with != 'jinja2'
514520
):

tests/unit/parsec/test_fileparse.py

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,15 @@
3232
)
3333
from cylc.flow.parsec.OrderedDict import OrderedDictWithDefaults
3434
from cylc.flow.parsec.fileparse import (
35+
EXTRA_VARS_TEMPLATE,
3536
_prepend_old_templatevars,
3637
_get_fpath_for_source,
3738
get_cylc_env_vars,
3839
addict,
3940
addsect,
4041
multiline,
4142
parse,
43+
process_plugins,
4244
read_and_proc,
4345
merge_template_vars
4446
)
@@ -741,7 +743,7 @@ def test_get_fpath_for_source(tmp_path):
741743

742744
def test_user_has_no_cwd(tmp_path):
743745
"""Test we can parse a config file even if cwd does not exist."""
744-
cwd = tmp_path/"cwd"
746+
cwd = tmp_path / "cwd"
745747
os.mkdir(cwd)
746748
os.chdir(cwd)
747749
os.rmdir(cwd)
@@ -765,15 +767,42 @@ def test_get_cylc_env_vars(monkeypatch):
765767
{
766768
"CYLC_VERSION": "betwixt",
767769
"CYLC_ENV_NAME": "between",
768-
"CYLC_QUESTION": "que?",
769-
"CYLC_ANSWER": "42",
770+
"CYLC_QUESTION": "que?",
771+
"CYLC_ANSWER": "42",
770772
"FOO": "foo"
771773
}
772774
)
773775
assert (
774776
get_cylc_env_vars() == {
775-
"CYLC_QUESTION": "que?",
776-
"CYLC_ANSWER": "42",
777+
"CYLC_QUESTION": "que?",
778+
"CYLC_ANSWER": "42",
777779
}
778780
)
779781

782+
783+
class EntryPointWrapper:
784+
"""Wraps a method to make it look like an entry point."""
785+
786+
def __init__(self, fcn):
787+
self.name = fcn.__name__
788+
self.fcn = fcn
789+
790+
def load(self):
791+
return self.fcn
792+
793+
794+
@EntryPointWrapper
795+
def pre_configure_basic(*_, **__):
796+
"""Simple plugin that returns one env var and one template var."""
797+
return {'env': {'foo': 44}, 'template_variables': {}}
798+
799+
800+
def test_plugins_not_called_on_global_config(monkeypatch):
801+
monkeypatch.setattr(
802+
'cylc.flow.parsec.fileparse.iter_entry_points',
803+
lambda x: [pre_configure_basic]
804+
)
805+
result = process_plugins('/pennine/way/flow.cylc', {})
806+
assert result != EXTRA_VARS_TEMPLATE
807+
result = process_plugins('/appalachian/trail/global.cylc', {})
808+
assert result == EXTRA_VARS_TEMPLATE

tests/unit/plugins/test_pre_configure.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ def test_pre_configure(monkeypatch):
6868
'cylc.flow.parsec.fileparse.iter_entry_points',
6969
lambda x: [pre_configure_basic]
7070
)
71-
extra_vars = process_plugins(None, None)
71+
extra_vars = process_plugins('/', None)
7272
assert extra_vars == {
7373
'env': {
7474
'ANSWER': '42'
@@ -90,7 +90,7 @@ def test_pre_configure_duplicate(monkeypatch):
9090
]
9191
)
9292
with pytest.raises(ParsecError):
93-
process_plugins(None, None)
93+
process_plugins('/', None)
9494

9595

9696
def test_pre_configure_templating_detected(monkeypatch):
@@ -103,7 +103,7 @@ def test_pre_configure_templating_detected(monkeypatch):
103103
]
104104
)
105105
with pytest.raises(ParsecError):
106-
process_plugins(None, None)
106+
process_plugins('/', None)
107107

108108

109109
def test_pre_configure_exception(monkeypatch):
@@ -113,7 +113,7 @@ def test_pre_configure_exception(monkeypatch):
113113
lambda x: [pre_configure_error]
114114
)
115115
with pytest.raises(PluginError) as exc_ctx:
116-
process_plugins(None, None)
116+
process_plugins('/', None)
117117
# the context of the original error should be preserved in the raised
118118
# exception
119119
assert exc_ctx.value.entry_point == 'cylc.pre_configure'

0 commit comments

Comments
 (0)