16
16
17
17
"""Cylc command argument validation logic."""
18
18
19
-
20
19
from typing import (
20
+ TYPE_CHECKING ,
21
+ Dict ,
21
22
Iterable ,
22
23
List ,
23
24
Optional ,
25
+ Set ,
26
+ cast ,
24
27
)
25
28
26
- from cylc .flow .exceptions import InputError
29
+ from cylc .flow .cycling .loader import standardise_point_string
30
+ from cylc .flow .exceptions import InputError , PointParsingError
27
31
from cylc .flow .flow_mgr import (
28
32
FLOW_NEW ,
29
33
FLOW_NONE ,
32
36
IDTokens ,
33
37
Tokens ,
34
38
)
35
- from cylc .flow .task_outputs import TASK_OUTPUT_SUCCEEDED
39
+ from cylc .flow .id_cli import contains_fnmatch
36
40
from cylc .flow .scripts .set import XTRIGGER_PREREQ_PREFIX
41
+ from cylc .flow .task_outputs import TASK_OUTPUT_SUCCEEDED
42
+
43
+
44
+ if TYPE_CHECKING :
45
+ from cylc .flow .id import TaskTokens
37
46
38
47
39
48
ERR_OPT_FLOW_VAL_INT_NEW_NONE = ( # for set and trigger commands
49
58
def flow_opts (
50
59
flows : List [str ],
51
60
flow_wait : bool ,
52
- allow_new_or_none : bool = True
61
+ allow_new_or_none : bool = True ,
53
62
) -> None :
54
63
"""Check validity of flow-related CLI options.
55
64
@@ -235,9 +244,7 @@ def prereq(prereq: str) -> Optional[str]:
235
244
return None
236
245
237
246
if tokens ["cycle" ] == XTRIGGER_PREREQ_PREFIX :
238
- if (
239
- tokens ["task_sel" ] not in {None , TASK_OUTPUT_SUCCEEDED }
240
- ):
247
+ if tokens ["task_sel" ] not in {None , TASK_OUTPUT_SUCCEEDED }:
241
248
# Error: xtrigger status must be default or succeeded.
242
249
return None
243
250
if tokens ["task_sel" ] is None :
@@ -291,7 +298,7 @@ def outputs(outputs: Optional[List[str]]):
291
298
292
299
def consistency (
293
300
outputs : Optional [List [str ]],
294
- prereqs : Optional [List [str ]]
301
+ prereqs : Optional [List [str ]],
295
302
) -> None :
296
303
"""Check global option consistency
297
304
@@ -309,40 +316,73 @@ def consistency(
309
316
raise InputError ("Use --prerequisite or --output, not both." )
310
317
311
318
312
- def is_tasks (tasks : Iterable [str ]):
313
- """All tasks in a list of tasks are task ID's without trailing job ID .
319
+ def is_tasks (ids : Iterable [str ]) -> 'Set[TaskTokens]' :
320
+ """Ensure all IDs are task IDs and standardise them .
314
321
315
- Examples:
316
- # All legal
317
- >>> is_tasks(['1/foo', '1/bar', '*/baz', '*/*'])
322
+ * Parses IDs.
323
+ * Filters out job ids and ensures at least the cycle point is provided.
324
+ * Standardises the cycle point format.
325
+ * Defaults the namespace to "root" unless provided.
318
326
319
- # Some legal
320
- >>> is_tasks(['1/foo/NN', '1/bar', '*/baz', '*/*/42'])
321
- Traceback (most recent call last):
322
- ...
323
- cylc.flow.exceptions.InputError: This command does not take job ids:
324
- * 1/foo/NN
325
- * */*/42
327
+ Args:
328
+ ids: The strings to parse.
326
329
327
- # None legal
328
- >>> is_tasks(['*/baz/12'])
329
- Traceback (most recent call last):
330
- ...
331
- cylc.flow.exceptions.InputError: This command does not take job ids:
332
- * */baz/12
330
+ Returns:
331
+ The parsed IDs as TaskTokens objects.
332
+
333
+ Raises:
334
+ InputError: If any of the IDs cannot be parsed or formatted.
333
335
334
- >>> is_tasks([])
335
- Traceback (most recent call last):
336
- ...
337
- cylc.flow.exceptions.InputError: No tasks specified
338
336
"""
339
- if not tasks :
337
+ if not ids :
340
338
raise InputError ("No tasks specified" )
341
- bad_tasks : List [str ] = []
342
- for task in tasks :
343
- tokens = Tokens ('//' + task )
339
+ ret : 'Set[TaskTokens]' = set ()
340
+ errors : Dict [str , List [str ]] = {}
341
+
342
+ for id_ in ids :
343
+ # parse id
344
+ try :
345
+ tokens = Tokens (id_ , relative = True )
346
+ except ValueError :
347
+ errors .setdefault ('Invalid ID' , []).append (id_ )
348
+ continue
349
+
350
+ # filter out job IDs
344
351
if tokens .lowest_token == IDTokens .Job .value :
345
- bad_tasks .append (task )
346
- if bad_tasks :
347
- msg = 'This command does not take job ids:\n * '
348
- raise InputError (msg + '\n * ' .join (bad_tasks ))
352
+ errors .setdefault ('This command does not take job IDs' , []).append (
353
+ id_
354
+ )
355
+ continue
356
+
357
+ # if the task is not specified, default to "root"
358
+ if tokens ['task' ] is None :
359
+ tokens = tokens .duplicate (task = 'root' )
360
+
361
+ # if the cycle is not a glob or reference, standardise it
362
+ if (
363
+ # cycle point is a glob
364
+ not contains_fnmatch (cast ('str' , tokens ['cycle' ]))
365
+ # cycle point is a reference to the ICP/FCP
366
+ and tokens ['cycle' ] not in {'^' , '$' }
367
+ ):
368
+ try :
369
+ cycle = standardise_point_string (tokens ['cycle' ])
370
+ except PointParsingError :
371
+ errors .setdefault ('Invalid cycle point' , []).append (id_ )
372
+ continue
373
+ else :
374
+ if cycle != tokens ['cycle' ]:
375
+ tokens = tokens .duplicate (cycle = cycle )
376
+
377
+ # we have confirmed that both cycle and task have been provided
378
+ ret .add (cast ('TaskTokens' , tokens ))
379
+
380
+ if errors :
381
+ raise InputError (
382
+ '\n ' .join (
383
+ f'{ message } : { ", " .join (sorted (_ids ))} '
384
+ for message , _ids in sorted (errors .items ())
385
+ )
386
+ )
387
+
388
+ return ret
0 commit comments