From 3b8a7df4a9af0fb45d1bb893839918de8f6f5e9c Mon Sep 17 00:00:00 2001 From: TomekTrzeciak Date: Fri, 6 Dec 2024 17:08:41 +0000 Subject: [PATCH] UserList to tell apart user set empty list from undefined value --- cylc/flow/parsec/util.py | 34 +++++++++++++----------------- cylc/flow/parsec/validate.py | 7 +++++- cylc/flow/workflow_events.py | 3 ++- tests/unit/test_workflow_events.py | 9 ++++---- 4 files changed, 28 insertions(+), 25 deletions(-) diff --git a/cylc/flow/parsec/util.py b/cylc/flow/parsec/util.py index 785612639a3..e26134c62c0 100644 --- a/cylc/flow/parsec/util.py +++ b/cylc/flow/parsec/util.py @@ -212,7 +212,7 @@ def replicate(target, source): target[key].defaults_ = pdeepcopy(val.defaults_) replicate(target[key], val) elif isinstance(val, list): - target[key] = val[:] + target[key] = type(val)(val) else: target[key] = val @@ -247,7 +247,7 @@ def poverride(target, sparse, prepend=False): # Override in-place in the target ordered dict. setitem = target.__setitem__ if isinstance(val, list): - setitem(key, val[:]) + setitem(key, type(val)(val)) else: setitem(key, val) @@ -290,25 +290,21 @@ def m_override(target, sparse): stack.append( (val, dest[key], keylist + [key], child_many_defaults)) else: - if key not in dest: - if not ( - '__MANY__' in dest - or key in many_defaults - or '__MANY__' in many_defaults - ): - # TODO - validation prevents this, but handle properly - # for completeness. - raise Exception( - "parsec dict override: no __MANY__ placeholder" + - "%s" % (keylist + [key]) - ) - if isinstance(val, list): - dest[key] = val[:] - else: - dest[key] = val + if not ( + key in dest + or '__MANY__' in dest + or key in many_defaults + or '__MANY__' in many_defaults + ): + # TODO - validation prevents this, but handle properly + # for completeness. + raise Exception( + "parsec dict override: no __MANY__ placeholder" + + "%s" % (keylist + [key]) + ) if isinstance(val, list): - dest[key] = val[:] + dest[key] = type(val)(val) else: dest[key] = val for dest_dict, defaults in defaults_list: diff --git a/cylc/flow/parsec/validate.py b/cylc/flow/parsec/validate.py index 18c19596a63..eb2447d9f2b 100644 --- a/cylc/flow/parsec/validate.py +++ b/cylc/flow/parsec/validate.py @@ -597,7 +597,7 @@ def strip_and_unquote_list(cls, keys, value): # allow trailing commas if values[-1] == '': values = values[0:-1] - return values + return UserList(values) @classmethod def _unquoted_list_parse(cls, keys, value): @@ -647,6 +647,11 @@ def __str__(self): return f'{self[0]} .. {self[1]}' +class UserList(list): + # distinguish user defined, possibly empty list from automatic defaults + pass + + class CylcConfigValidator(ParsecValidator): """Type validator and coercer for Cylc configurations. diff --git a/cylc/flow/workflow_events.py b/cylc/flow/workflow_events.py index 02d2d4166d3..719b7d02f6f 100644 --- a/cylc/flow/workflow_events.py +++ b/cylc/flow/workflow_events.py @@ -24,6 +24,7 @@ from cylc.flow.cfgspec.glbl_cfg import glbl_cfg from cylc.flow.hostuserutil import get_host, get_user from cylc.flow.log_diagnosis import run_reftest +from cylc.flow.parsec.validate import UserList from cylc.flow.subprocctx import SubProcContext if TYPE_CHECKING: @@ -235,7 +236,7 @@ def get_events_conf( glbl_cfg().get(['scheduler', 'mail']) ): value = getter.get(key) - if value is not None: + if value not in (None, []) or isinstance(value, UserList): return value return default diff --git a/tests/unit/test_workflow_events.py b/tests/unit/test_workflow_events.py index cea2c3f2b41..47e366b38ba 100644 --- a/tests/unit/test_workflow_events.py +++ b/tests/unit/test_workflow_events.py @@ -21,6 +21,7 @@ from types import SimpleNamespace +from cylc.flow.parsec.validate import UserList from cylc.flow.workflow_events import ( WorkflowEventHandler, get_template_variables, @@ -32,11 +33,11 @@ 'key, workflow_cfg, glbl_cfg, expected', [ ('handlers', True, True, ['stall']), - ('handlers', False, True, []), - ('handlers', False, False, []), + ('handlers', False, True, None), + ('handlers', False, False, None), ('mail events', True, True, []), ('mail events', False, True, ['abort']), - ('mail events', False, False, []), + ('mail events', False, False, None), ('from', True, True, 'docklands@railway'), ('from', False, True, 'highway@mixture'), ('from', False, False, None), @@ -65,7 +66,7 @@ def test_get_events_handler( config = SimpleNamespace() config.cfg = { 'scheduler': { - 'events': {'handlers': ['stall'], 'mail events': []}, + 'events': {'handlers': ['stall'], 'mail events': UserList()}, 'mail': {'from': 'docklands@railway'}, } if workflow_cfg else {'events': {}} }