diff --git a/CHANGES.md b/CHANGES.md index 7370fff01c7..905528ea71b 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -12,6 +12,13 @@ ones in. --> ## __cylc-8.2.0 (Upcoming)__ +### Breaking Changes + +[#5600](https://github.com/cylc/cylc-flow/pull/5600) - +The `CYLC_TASK_DEPENDENCIES` environment variable will no longer be exported +in job environments if there are more than 50 dependencies. This avoids an +issue which could cause jobs to fail if this variable became too long. + ### Enhancements [#5405](https://github.com/cylc/cylc-flow/pull/5405) - Improve scan command @@ -34,17 +41,6 @@ in `share/bin` and Python modules in `share/lib/python`. [#5328](https://github.com/cylc/cylc-flow/pull/5328) - Efficiency improvements to reduce task management overheads on the Scheduler. -## __cylc-8.1.5 (Upcoming)__ - -### Breaking Changes - -[#5600](https://github.com/cylc/cylc-flow/pull/5600) - -The `CYLC_TASK_DEPENDENCIES` environment variable will no longer be exported -in job environments if there are more than 50 dependencies. This avoids an -issue which could cause jobs to fail if this variable became too long. - -### Enhancements - [#5546](https://github.com/cylc/cylc-flow/pull/5546) - `cylc lint` will provide a non-zero return code if any issues are identified. This can be overridden using the new `--exit-zero` flag. @@ -73,6 +69,10 @@ the UI whilst the workflow is in the process of shutting down. [#5582](https://github.com/cylc/cylc-flow/pull/5582) - Set Cylc 7 compatibility mode before running pre-configure plugins. +[#5587](https://github.com/cylc/cylc-flow/pull/5587) - +Permit commas in xtrigger arguments and fix minor issues with the parsing of +xtrigger function signatures. + ## __cylc-8.1.4 (Released 2023-05-04)__ ### Fixes diff --git a/cylc/flow/parsec/validate.py b/cylc/flow/parsec/validate.py index f6e40e6a912..5055d01ab79 100644 --- a/cylc/flow/parsec/validate.py +++ b/cylc/flow/parsec/validate.py @@ -26,6 +26,7 @@ import shlex from collections import deque from textwrap import dedent +from typing import List, Dict, Any, Tuple from metomi.isodatetime.data import Duration, TimePoint from metomi.isodatetime.dumpers import TimePointDumper @@ -994,6 +995,66 @@ def coerce_parameter_list(cls, value, keys): except ValueError: return items + @staticmethod + def parse_xtrig_arglist(value: str) -> Tuple[List[Any], Dict[str, Any]]: + """Parse Pythonic-like arg/kwarg signatures. + + A stateful parser treats all args/kwargs as strings with + implicit quoting. + + Examples: + >>> parse = CylcConfigValidator.parse_xtrig_arglist + + # Parse pythonic syntax + >>> parse('a, b, c, d=1, e=2,') + (['a', 'b', 'c'], {'d': '1', 'e': '2'}) + >>> parse('a, "1,2,3", b=",=",') + (['a', '"1,2,3"'], {'b': '",="'}) + >>> parse('a, "b c", '"'d=e '") + (['a', '"b c"', "'d=e '"], {}) + + # Parse implicit (i.e. unquoted) strings + >>> parse('%(cycle)s, %(task)s, output=succeeded') + (['%(cycle)s', '%(task)s'], {'output': 'succeeded'}) + + """ + # results + args = [] + kwargs = {} + # state + in_str = False # are we inside a quoted string + in_kwarg = False # are we after the = sign of a kwarg + buffer = '' # the current argument being parsed + kwarg_buffer = '' # the key of a kwarg if in_kwarg == True + # parser + for char in value: + if char in {'"', "'"}: + in_str = not in_str + buffer += char + elif not in_str and char == ',': + if in_kwarg: + kwargs[kwarg_buffer.strip()] = buffer.strip() + in_kwarg = False + kwarg_buffer = '' + else: + args.append(buffer.strip()) + buffer = '' + elif char == '=' and not in_str and not in_kwarg: + in_kwarg = True + kwarg_buffer = buffer + buffer = '' + else: + buffer += char + + # reached the end of the string + if buffer: + if in_kwarg: + kwargs[kwarg_buffer.strip()] = buffer.strip() + else: + args.append(buffer.strip()) + + return args, kwargs + @classmethod def coerce_xtrigger(cls, value, keys): """Coerce a string into an xtrigger function context object. @@ -1002,17 +1063,26 @@ def coerce_xtrigger(cls, value, keys): Checks for legal string templates in arg values too. Examples: - >>> CylcConfigValidator.coerce_xtrigger('a(b, c):PT1M', [None]) + >>> xtrig = CylcConfigValidator.coerce_xtrigger + + # parse xtrig function signatures + >>> xtrig('a(b, c):PT1M', [None]) a(b, c):60.0 + >>> xtrig('a(x, "1,2,3", y):PT1S', [None]) + a(x, 1,2,3, y):1.0 - """ + # cast types + >>> x = xtrig('a(1, 1.1, True, abc, x=True, y=1.1)', [None]) + >>> x.func_args + [1, 1.1, True, 'abc'] + >>> x.func_kwargs + {'x': True, 'y': 1.1} + """ label = keys[-1] value = cls.strip_and_unquote(keys, value) if not value: raise IllegalValueError("xtrigger", keys, value) - args = [] - kwargs = {} match = cls._REC_TRIG_FUNC.match(value) if match is None: raise IllegalValueError("xtrigger", keys, value) @@ -1020,15 +1090,18 @@ def coerce_xtrigger(cls, value, keys): if intvl: intvl = cls.coerce_interval(intvl, keys) - if fargs: - # Extract function args and kwargs. - for farg in fargs.split(r','): - try: - key, val = farg.strip().split(r'=', 1) - except ValueError: - args.append(cls._coerce_type(farg.strip())) - else: - kwargs[key.strip()] = cls._coerce_type(val.strip()) + # parse args + args, kwargs = CylcConfigValidator.parse_xtrig_arglist(fargs or '') + + # cast types + args = [ + CylcConfigValidator._coerce_type(arg) + for arg in args + ] + kwargs = { + key: CylcConfigValidator._coerce_type(value) + for key, value in kwargs.items() + } return SubFuncContext(label, fname, args, kwargs, intvl) diff --git a/cylc/flow/subprocctx.py b/cylc/flow/subprocctx.py index 030bedaced6..85bbc284503 100644 --- a/cylc/flow/subprocctx.py +++ b/cylc/flow/subprocctx.py @@ -162,7 +162,7 @@ def get_signature(self): def __str__(self): return ( f'{self.func_name}(' - f'{", ".join(self.func_args + list(self.func_kwargs))}' + f'{", ".join(map(str, self.func_args + list(self.func_kwargs)))}' f'):{self.intvl}' )