Skip to content

Commit ec2ff38

Browse files
authored
Merge pull request #7213 from cylc/8.6.x-sync
🤖 Merge 8.6.x-sync into master
2 parents 5ef72ee + 113a724 commit ec2ff38

7 files changed

Lines changed: 127 additions & 33 deletions

File tree

changes.d/7121.fix.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fixed `cylc broadcast` traceback when broadcasting an auto-upgraded deprecated setting.

cylc/flow/cfgspec/workflow.py

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
from typing import (
2222
Any,
2323
Dict,
24+
Literal,
2425
Optional,
2526
Set,
2627
)
@@ -2102,7 +2103,9 @@ def get_script_common_text(this: str, example: Optional[str] = None):
21022103
''')
21032104

21042105

2105-
def upg(cfg, descr, for_cancel_broadcast=False):
2106+
def upg(
2107+
cfg: dict, descr: str, broadcast: bool | Literal["cancel"] = False
2108+
) -> upgrader:
21062109
"""Upgrade old workflow configuration.
21072110
21082111
NOTE: We are silencing deprecation (and only deprecation) warnings
@@ -2111,14 +2114,15 @@ def upg(cfg, descr, for_cancel_broadcast=False):
21112114
warnings and upgrade the syntax).
21122115
21132116
Args:
2114-
for_cancel_broadcast:
2115-
If True, extra validation steps which inspect configuration values
2116-
will be skipped. This is used for "cylc broadcast --cancel" where
2117-
the values are not known.
2117+
broadcast:
2118+
If truthy, tailors warning messages for the context of broadcasts.
2119+
If "cancel", extra validation steps which inspect configuration
2120+
values will be skipped. This is used for
2121+
"cylc broadcast --cancel" where the values are not known.
21182122
See https://github.com/cylc/cylc-flow/issues/6950.
21192123
21202124
"""
2121-
u = upgrader(cfg, descr)
2125+
u = upgrader(cfg, descr, broadcast=bool(broadcast))
21222126

21232127
u.obsolete(
21242128
'7.8.0', ['runtime', '__MANY__', 'suite state polling', 'template']
@@ -2321,7 +2325,7 @@ def upg(cfg, descr, for_cancel_broadcast=False):
23212325
)
23222326
u.upgrade()
23232327

2324-
if not for_cancel_broadcast:
2328+
if broadcast != "cancel":
23252329
upgrade_graph_section(cfg, descr)
23262330
upgrade_param_env_templates(cfg, descr)
23272331
warn_about_depr_platform(cfg)
@@ -2393,7 +2397,7 @@ def upgrade_param_env_templates(cfg, descr):
23932397
continue
23942398
if not cylc.flow.flags.cylc7_back_compat:
23952399
if first_warn:
2396-
LOG.warning(upgrader.DEPR_MSG)
2400+
LOG.warning(upgrader.depr_msg)
23972401
first_warn = False
23982402
LOG.warning(
23992403
f' * (8.0.0) {dep % task_name} contents prepended to '

cylc/flow/config.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2848,7 +2848,7 @@ def _upg_wflow_event_names(self) -> None:
28482848
)
28492849
if upgraded and not cylc.flow.flags.cylc7_back_compat:
28502850
LOG.warning(
2851-
f"{upgrader.DEPR_MSG}\n"
2851+
f"{upgrader.depr_msg}\n"
28522852
f" * (8.0.0) [scheduler][events][{setting}] "
28532853
+ ', '.join(f'{k} -> {v}' for k, v in upgraded.items())
28542854
)

cylc/flow/parsec/config.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,11 +45,11 @@ class ParsecConfig:
4545
def __init__(
4646
self,
4747
spec: 'ConfigNode',
48-
upgrader: Optional[Callable[[dict, str], None]] = None,
49-
output_fname: Optional[str] = None,
50-
tvars: Optional[dict] = None,
51-
validator: Optional[Callable] = None,
52-
options: Optional['Values'] = None
48+
upgrader: Callable | None = None,
49+
output_fname: str | None = None,
50+
tvars: dict | None = None,
51+
validator: Callable | None = None,
52+
options: 'Values | None' = None
5353
):
5454
"""Instatiate a parsec config object.
5555

cylc/flow/parsec/upgrade.py

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -43,17 +43,28 @@ class upgrader:
4343
SITE_CONFIG = 'site config'
4444
USER_CONFIG = 'user config'
4545

46-
DEPR_MSG = (
46+
DEPR_TEMPLATE = (
4747
"Deprecated config items were automatically upgraded. "
48-
"Please alter your workflow to use the new syntax."
48+
"Please alter your {} to use the new syntax."
4949
)
50+
depr_msg = DEPR_TEMPLATE.format('workflow')
5051

51-
def __init__(self, cfg, descr):
52+
obslt_msg = (
53+
"Obsolete config items were automatically deleted. "
54+
"Please check your workflow and remove them permanently."
55+
)
56+
57+
def __init__(self, cfg: dict, descr: str, broadcast: bool = False):
5258
"""Store the config dict to be upgraded if necessary."""
5359
self.cfg = cfg
5460
self.descr = descr
5561
# upgrades must be ordered in case several act on the same item
56-
self.upgrades = OrderedDict()
62+
self.upgrades: OrderedDict[str, list[dict]] = OrderedDict()
63+
if broadcast:
64+
self.depr_msg = self.DEPR_TEMPLATE.format('broadcast')
65+
self.obslt_msg = (
66+
"Obsolete config items were rejected by the broadcast."
67+
)
5768

5869
def deprecate(
5970
self, vn, oldkeys, newkeys=None,
@@ -255,13 +266,9 @@ def upgrade(self):
255266
# Log at warning level.
256267
level = WARNING
257268
if obsoletions:
258-
LOG.log(
259-
level,
260-
"Obsolete config items were automatically deleted."
261-
" Please check your workflow and remove them permanently."
262-
)
269+
LOG.log(level, self.obslt_msg)
263270
if deprecations:
264-
LOG.log(level, self.DEPR_MSG)
271+
LOG.log(level, self.depr_msg)
265272

266273
for vn, msgs in warnings.items():
267274
for msg in msgs:

cylc/flow/scripts/broadcast.py

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -196,13 +196,36 @@ def get_rdict(left, right=None, for_cancel_broadcast=False):
196196
upg(
197197
{'runtime': {'__MANY__': rdict}},
198198
'test',
199-
for_cancel_broadcast=for_cancel_broadcast,
199+
broadcast='cancel' if for_cancel_broadcast else True,
200200
)
201+
rdict = strip_empty_sections(rdict)
201202
# Perform validation, but don't coerce the original (deepcopy).
202203
cylc_config_validate(deepcopy(rdict), SPEC['runtime']['__MANY__'])
203204
return rdict
204205

205206

207+
def strip_empty_sections(data):
208+
"""Recursively strip data structure of empty dicts.
209+
210+
This is needed post-upgrade as the upgrader can leave empty sections
211+
behind.
212+
213+
>>> strip_empty_sections(
214+
... {'job': {}, 'thing limit': 'PT2S', 'whatever': ''})
215+
{'thing limit': 'PT2S', 'whatever': ''}
216+
217+
>>> strip_empty_sections({'section': {'subsection': {}}})
218+
{}
219+
"""
220+
if not isinstance(data, dict):
221+
return data
222+
return {
223+
key: stripped
224+
for key, val in data.items()
225+
if (stripped := strip_empty_sections(val)) != {}
226+
}
227+
228+
206229
def files_to_settings(settings, setting_files, cancel_mode=False):
207230
"""Parse setting files, and append to settings."""
208231
cfg = ParsecConfig(
@@ -437,9 +460,11 @@ async def run(options: 'Values', workflow_id):
437460
raise InputError(
438461
"--cancel=[SEC]ITEM does not take a value")
439462
option_item = option_item.strip()
440-
setting = get_rdict(option_item, for_cancel_broadcast=True)
441-
settings.append(setting)
463+
if setting := get_rdict(option_item, for_cancel_broadcast=True):
464+
settings.append(setting)
442465
files_to_settings(settings, options.cancel_files, options.cancel)
466+
if not settings:
467+
raise InputError("No valid settings to broadcast.")
443468
mutation_kwargs['variables'].update(
444469
{
445470
'bMode': 'Clear',
@@ -456,9 +481,11 @@ async def run(options: 'Values', workflow_id):
456481
raise InputError(
457482
"--set=[SEC]ITEM=VALUE requires a value")
458483
lhs, rhs = [s.strip() for s in option_item.split("=", 1)]
459-
setting = get_rdict(lhs, rhs)
460-
settings.append(setting)
484+
if setting := get_rdict(lhs, rhs):
485+
settings.append(setting)
461486
files_to_settings(settings, options.setting_files)
487+
if not settings:
488+
raise InputError("No valid settings to broadcast.")
462489
mutation_kwargs['variables'].update(
463490
{
464491
'bMode': 'Set',
@@ -472,9 +499,9 @@ async def run(options: 'Values', workflow_id):
472499

473500
results = await pclient.async_request('graphql', mutation_kwargs)
474501
try:
502+
bad_options = {}
475503
for result in results['broadcast']['result']:
476-
modified_settings = result['response'][0]
477-
bad_options = result['response'][1]
504+
modified_settings, bad_options = result['response']
478505
if modified_settings:
479506
ret['stdout'].append(
480507
get_broadcast_change_report(
@@ -483,7 +510,7 @@ async def run(options: 'Values', workflow_id):
483510
)
484511
)
485512
bad_result = report_bad_options(bad_options, is_set=report_set)
486-
except TypeError:
513+
except (TypeError, ValueError):
487514
# Catch internal API server errors
488515
bad_result = cparse(f'<red>{results}</red>')
489516

tests/integration/scripts/test_broadcast.py

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,19 @@
1414
# You should have received a copy of the GNU General Public License
1515
# along with this program. If not, see <http://www.gnu.org/licenses/>.
1616

17+
import logging
18+
1719
from ansimarkup import strip as cstrip
20+
import pytest
1821

1922
from cylc.flow.network.client import WorkflowRuntimeClient
2023
from cylc.flow.option_parsers import Options
2124
from cylc.flow.rundb import CylcWorkflowDAO
22-
from cylc.flow.scripts.broadcast import _main, get_option_parser
25+
from cylc.flow.scheduler import Scheduler
26+
from cylc.flow.scripts.broadcast import (
27+
_main,
28+
get_option_parser,
29+
)
2330
from cylc.flow.util import sstrip
2431

2532

@@ -314,3 +321,51 @@ def put_broadcast(*args, **kwargs):
314321

315322
# the error should be logged server-side too
316323
assert log_filter(contains='FooBar')
324+
325+
326+
async def test_broadcast_auto_upgraded_settings(
327+
one: Scheduler,
328+
start,
329+
log_filter,
330+
caplog: pytest.LogCaptureFixture,
331+
capsys: pytest.CaptureFixture,
332+
):
333+
"""Test broadcast of deprecated/obsolete settings."""
334+
opts = {
335+
'point_strings': ['1'],
336+
'namespaces': ['root'],
337+
}
338+
async with start(one):
339+
# Test deprecated setting that is moved to a different section by
340+
# the auto-upgrader should still work.
341+
rets = await _main(
342+
BroadcastOptions(
343+
settings=['[job]execution time limit=PT1H'], **opts
344+
),
345+
one.workflow,
346+
)
347+
assert set(rets.values()) == {True}
348+
assert log_filter(
349+
logging.WARNING, regex=r"Deprecated config.* upgraded.* broadcast"
350+
)
351+
352+
stdout, stderr = capsys.readouterr()
353+
assert "Traceback" not in stdout + stderr
354+
caplog.clear()
355+
356+
# Test obsolete setting that is removed by the auto-upgrader should
357+
# be rejected.
358+
rets = await _main(
359+
BroadcastOptions(
360+
settings=['extra log files=whatever'], **opts
361+
),
362+
one.workflow,
363+
)
364+
assert set(rets.values()) == {False}
365+
assert log_filter(
366+
logging.WARNING,
367+
regex=r"Obsolete config.* rejected by the broadcast",
368+
)
369+
stdout, stderr = capsys.readouterr()
370+
assert "InputError: No valid settings to broadcast" in stderr
371+
assert "Traceback" not in stdout + stderr

0 commit comments

Comments
 (0)