diff --git a/cylc/flow/parsec/validate.py b/cylc/flow/parsec/validate.py index 4ef66e19862..0f69e5d8ef1 100644 --- a/cylc/flow/parsec/validate.py +++ b/cylc/flow/parsec/validate.py @@ -26,7 +26,7 @@ import shlex from collections import deque from textwrap import dedent -from typing import List, Dict, Any, Tuple +from typing import List, Dict, Any, Tuple, Union from metomi.isodatetime.data import Duration, TimePoint from metomi.isodatetime.dumpers import TimePointDumper @@ -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: Union[str, List[str]], keys: List[str]) -> str: """Coerce value to a string. Examples: @@ -512,16 +512,16 @@ 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: @@ -1136,23 +1136,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: Union[str, List[str]], keys: List[str]) -> str: + """Coerce value to a string. + + Examples: + >>> BroadcastConfigValidator.coerce_str('abc#def', None) + '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}"' + return ParsecValidator.coerce_str(value, keys) + @classmethod def strip_and_unquote_list(cls, keys, value): """Remove leading and trailing spaces and unquote list value. @@ -1177,6 +1179,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. diff --git a/tests/functional/broadcast/10-file-1/broadcast.cylc b/tests/functional/broadcast/10-file-1/broadcast.cylc index f429f8a2740..eaadb6b4579 100644 --- a/tests/functional/broadcast/10-file-1/broadcast.cylc +++ b/tests/functional/broadcast/10-file-1/broadcast.cylc @@ -1,6 +1,8 @@ script=""" printenv CYLC_FOOBAR +# (This hash char should not cause the rest of the script to be stripped out) + if (($CYLC_TASK_TRY_NUMBER < 2 )); then false fi diff --git a/tests/unit/parsec/test_validate.py b/tests/unit/parsec/test_validate.py index 8c73b840eb3..ff68e9e78d6 100644 --- a/tests/unit/parsec/test_validate.py +++ b/tests/unit/parsec/test_validate.py @@ -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!'), @@ -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():