Skip to content

Commit

Permalink
xtriggers: improve parser
Browse files Browse the repository at this point in the history
Tolerate comas within strings and equals signs withing kwarg strings.

* Closes #5575
* Closes #5576
  • Loading branch information
oliver-sanders committed Jun 15, 2023
1 parent 36a14ab commit 88bc9aa
Showing 1 changed file with 83 additions and 13 deletions.
96 changes: 83 additions & 13 deletions cylc/flow/parsec/validate.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -994,6 +995,64 @@ 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 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.
Expand All @@ -1002,33 +1061,44 @@ 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
>>> xtrig('a(1, 1.1, True, abc)', [None])
a(1, 1.1, True, 'abc')
>>> xtrig('a(w=1, x=1.1, y=True, z=abc)', [None])
a(w=1, x=1.1, y=True, z='abc')
"""
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)
fname, fargs, intvl = match.groups()
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)

Expand Down

0 comments on commit 88bc9aa

Please sign in to comment.