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

Fix bugs in cylc broadcast #5933

Merged
merged 4 commits into from
Jan 24, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changes.d/5933.fix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fixed bug in `cylc broadcast` (and the GUI Edit Runtime command) where everything after a `#` character in a setting would be stripped out.
63 changes: 39 additions & 24 deletions cylc/flow/parsec/validate.py
Original file line number Diff line number Diff line change
Expand Up @@ -373,7 +373,7 @@ def coerce_range(cls, value, keys):
return Range((int(min_), int(max_)))

@classmethod
def coerce_str(cls, value, keys):
def coerce_str(cls, value, keys) -> str:
"""Coerce value to a string.

Examples:
Expand All @@ -385,7 +385,7 @@ def coerce_str(cls, value, keys):
"""
if isinstance(value, list):
# handle graph string merging
vraw = []
vraw: List[str] = []
vals = [value]
while vals:
val = vals.pop()
Expand Down Expand Up @@ -512,35 +512,36 @@ def parse_int_range(cls, value):
return None

@classmethod
def strip_and_unquote(cls, keys, value):
def strip_and_unquote(cls, keys: List[str], value: str) -> str:
"""Remove leading and trailing spaces and unquote value.

Args:
keys (list):
keys:
Keys in nested dict that represents the raw configuration.
value (str):
value:
String value in raw configuration.

Return (str):
Return:
Processed value.

Examples:
>>> ParsecValidator.strip_and_unquote(None, '" foo "')
'foo'

"""
for substr, rec in [
["'''", cls._REC_MULTI_LINE_SINGLE],
['"""', cls._REC_MULTI_LINE_DOUBLE],
['"', cls._REC_DQ_VALUE],
["'", cls._REC_SQ_VALUE]]:
for substr, rec in (
("'''", cls._REC_MULTI_LINE_SINGLE),
('"""', cls._REC_MULTI_LINE_DOUBLE),
('"', cls._REC_DQ_VALUE),
("'", cls._REC_SQ_VALUE)
):
if value.startswith(substr):
match = rec.match(value)
if not match:
raise IllegalValueError("string", keys, value)
value = match.groups()[0]
break
else:
else: # no break
# unquoted
value = value.split(r'#', 1)[0]

Expand Down Expand Up @@ -1136,23 +1137,25 @@ def _coerce_type(cls, value):
return val


# BACK COMPAT: BroadcastConfigValidator
# The DB at 8.0.x stores Interval values as neither ISO8601 duration
# string or DurationFloat. This has been fixed at 8.1.0, and
# the following class acts as a bridge between fixed and broken.
# url:
# https://github.com/cylc/cylc-flow/pull/5138
# from:
# 8.0.x
# to:
# 8.1.x
# remove at:
# 8.x
class BroadcastConfigValidator(CylcConfigValidator):
"""Validate and Coerce DB loaded broadcast config to internal objects."""
def __init__(self):
CylcConfigValidator.__init__(self)

@classmethod
def coerce_str(cls, value, keys) -> str:
Comment on lines +1149 to +1150
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
@classmethod
def coerce_str(cls, value, keys) -> str:
@staticmethod
def coerce_str(value, keys) -> str:

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is overriding a class method from ParsecValidator

"""Coerce value to a string.

Examples:
>>> BroadcastConfigValidator.coerce_str('abc#def', None)
MetRonnie marked this conversation as resolved.
Show resolved Hide resolved
'abc#def'
"""
# Prevent ParsecValidator from assuming '#' means comments;
# '#' has valid uses in shell script such as parameter substitution
if isinstance(value, str) and '#' in value:
value = f'"{value}"'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't work if the string is already quoted e.g:

$ cylc broadcast one -n root -s 'script="sleep 30 # ja!"'
ERROR: [{'error': {'message': '(type=string) script = ""sleep 30 # ja!""', 'traceback': ['graphql.error.located_error.GraphQLLocatedError: (type=string) script = ""sleep 30 # ja!""\n']}}]

Maybe use shlex.quote which has logic to avoid surplus quoting.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shlex.quote messes with the quotes inside which we don't want. I've come up with a less hacky approach

return ParsecValidator.coerce_str(value, keys)

@classmethod
def strip_and_unquote_list(cls, keys, value):
"""Remove leading and trailing spaces and unquote list value.
Expand All @@ -1177,6 +1180,18 @@ def strip_and_unquote_list(cls, keys, value):
value = value.lstrip('[').rstrip(']')
return ParsecValidator.strip_and_unquote_list(keys, value)

# BACK COMPAT: BroadcastConfigValidator.coerce_interval
# The DB at 8.0.x stores Interval values as neither ISO8601 duration
# string or DurationFloat. This has been fixed at 8.1.0, and
# the following class acts as a bridge between fixed and broken.
# url:
# https://github.com/cylc/cylc-flow/pull/5138
# from:
# 8.0.x
# to:
# 8.1.x
# remove at:
# 8.x
@classmethod
def coerce_interval(cls, value, keys):
"""Coerce an ISO 8601 interval into seconds.
Expand Down
3 changes: 2 additions & 1 deletion cylc/flow/scripts/broadcast.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@
from ansimarkup import parse as cparse
from copy import deepcopy
from functools import partial
import os.path
import re
import sys
from tempfile import NamedTemporaryFile
Expand Down Expand Up @@ -203,7 +204,7 @@ def files_to_settings(settings, setting_files, cancel_mode=False):
handle.seek(0, 0)
cfg.loadcfg(handle.name)
else:
cfg.loadcfg(setting_file)
cfg.loadcfg(os.path.abspath(setting_file))
stack = [([], cfg.get(sparse=True))]
while stack:
keys, item = stack.pop()
Expand Down
2 changes: 2 additions & 0 deletions tests/functional/broadcast/10-file-1/broadcast.cylc
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
script="""
printenv CYLC_FOOBAR

# (This hash char should not cause the rest of the script to be stripped out)
MetRonnie marked this conversation as resolved.
Show resolved Hide resolved

if (($CYLC_TASK_TRY_NUMBER < 2 )); then
false
fi
Expand Down
20 changes: 12 additions & 8 deletions tests/unit/parsec/test_validate.py
Original file line number Diff line number Diff line change
Expand Up @@ -456,11 +456,9 @@ def test_coerce_int_list():
validator.coerce_int_list(value, ['whatever'])


def test_coerce_str():
"""Test coerce_str."""
validator = ParsecValidator()
# The good
for value, result in [
@pytest.mark.parametrize(
'value, expected',
[
('', ''),
('Hello World!', 'Hello World!'),
('"Hello World!"', 'Hello World!'),
Expand All @@ -474,9 +472,15 @@ def test_coerce_str():
'Hello:\n foo\nGreet\n baz'),
('False', 'False'),
('None', 'None'),
(['a', 'b'], 'a\nb')
]:
assert validator.coerce_str(value, ['whatever']) == result
(['a', 'b'], 'a\nb'),
('abc#def', 'abc'),
]
)
def test_coerce_str(value: str, expected: str):
"""Test coerce_str."""
validator = ParsecValidator()
# The good
assert validator.coerce_str(value, ['whatever']) == expected


def test_coerce_str_list():
Expand Down
Loading