Skip to content

Commit

Permalink
Merge pull request #6123 from MetRonnie/tokens
Browse files Browse the repository at this point in the history
Tokens: handle ISO 8601 long format cycle points
  • Loading branch information
hjoliver committed Jun 11, 2024
2 parents 8690b03 + e1f7205 commit 1a9c681
Show file tree
Hide file tree
Showing 5 changed files with 74 additions and 4 deletions.
1 change: 1 addition & 0 deletions changes.d/6123.fix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Allow long-format datetime cycle points in IDs used on the command line.
2 changes: 1 addition & 1 deletion cylc/flow/id.py
Original file line number Diff line number Diff line change
Expand Up @@ -403,7 +403,7 @@ def duplicate(
# //cycle[:sel][/task[:sel][/job[:sel]]]
RELATIVE_PATTERN = rf'''
//
(?P<{IDTokens.Cycle.value}>[^~\/:\n]+)
(?P<{IDTokens.Cycle.value}>[^~\/:\n][^~\/\n]*?)
(?:
:
(?P<{IDTokens.Cycle.value}_sel>[^\/:\n]+)
Expand Down
38 changes: 36 additions & 2 deletions cylc/flow/id_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@
import re
from typing import Optional, Dict, List, Tuple, Any

from metomi.isodatetime.parsers import TimePointParser
from metomi.isodatetime.exceptions import ISO8601SyntaxError

from cylc.flow import LOG
from cylc.flow.exceptions import (
InputError,
Expand All @@ -28,6 +31,7 @@
from cylc.flow.id import (
Tokens,
contains_multiple_workflows,
tokenise,
upgrade_legacy_ids,
)
from cylc.flow.pathutil import EXPLICIT_RELATIVE_PATH_REGEX
Expand All @@ -43,6 +47,36 @@


FN_CHARS = re.compile(r'[\*\?\[\]\!]')
TP_PARSER = TimePointParser()


def cli_tokenise(id_: str) -> Tokens:
"""Tokenise with support for long-format datetimes.
If a cycle selector is present, it could be part of a long-format
ISO 8601 datetime that was erroneously split. Re-attach it if it
results in a valid datetime.
Examples:
>>> f = lambda t: {k: v for k, v in t.items() if v is not None}
>>> f(cli_tokenise('foo//2021-01-01T00:00Z'))
{'workflow': 'foo', 'cycle': '2021-01-01T00:00Z'}
>>> f(cli_tokenise('foo//2021-01-01T00:horse'))
{'workflow': 'foo', 'cycle': '2021-01-01T00', 'cycle_sel': 'horse'}
"""
tokens = tokenise(id_)
cycle = tokens['cycle']
cycle_sel = tokens['cycle_sel']
if not (cycle and cycle_sel) or '-' not in cycle:
return tokens
cycle = f'{cycle}:{cycle_sel}'
try:
TP_PARSER.parse(cycle)
except ISO8601SyntaxError:
return tokens
dict.__setitem__(tokens, 'cycle', cycle)
del tokens['cycle_sel']
return tokens


def _parse_cli(*ids: str) -> List[Tokens]:
Expand Down Expand Up @@ -124,14 +158,14 @@ def _parse_cli(*ids: str) -> List[Tokens]:
tokens_list: List[Tokens] = []
for id_ in ids:
try:
tokens = Tokens(id_)
tokens = cli_tokenise(id_)
except ValueError:
if id_.endswith('/') and not id_.endswith('//'): # noqa: SIM106
# tolerate IDs that end in a single slash on the CLI
# (e.g. CLI auto completion)
try:
# this ID is invalid with or without the trailing slash
tokens = Tokens(id_[:-1])
tokens = cli_tokenise(id_[:-1])
except ValueError:
raise InputError(f'Invalid ID: {id_}')
else:
Expand Down
2 changes: 1 addition & 1 deletion tests/unit/test_id.py
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ def test_universal_id_matches_hierarchical(identifier):
'//~',
'//:',
'//workflow//cycle',
'//task:task_sel:task_sel'
'//cycle/task:task_sel:task_sel'
]
)
def test_relative_id_illegal(identifier):
Expand Down
35 changes: 35 additions & 0 deletions tests/unit/test_id_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
_validate_constraint,
_validate_workflow_ids,
_validate_number,
cli_tokenise,
parse_ids_async,
)
from cylc.flow.pathutil import get_cylc_run_dir
Expand Down Expand Up @@ -607,3 +608,37 @@ async def test_expand_workflow_tokens_impl_selector(no_scan):
tokens = tokens.duplicate(workflow_sel='stopped')
with pytest.raises(InputError):
await _expand_workflow_tokens([tokens])


@pytest.mark.parametrize('identifier, expected', [
(
'//2024-01-01T00:fail/a',
{'cycle': '2024-01-01T00', 'cycle_sel': 'fail', 'task': 'a'}
),
(
'//2024-01-01T00:00Z/a',
{'cycle': '2024-01-01T00:00Z', 'task': 'a'}
),
(
'//2024-01-01T00:00Z:fail/a',
{'cycle': '2024-01-01T00:00Z', 'cycle_sel': 'fail', 'task': 'a'}
),
(
'//2024-01-01T00:00:00+05:30/a',
{'cycle': '2024-01-01T00:00:00+05:30', 'task': 'a'}
),
(
'//2024-01-01T00:00:00+05:30:f/a',
{'cycle': '2024-01-01T00:00:00+05:30', 'cycle_sel': 'f', 'task': 'a'}
),
(
# Nonsensical example, but whatever...
'//2024-01-01T00:00Z:00Z/a',
{'cycle': '2024-01-01T00:00Z', 'cycle_sel': '00Z', 'task': 'a'}
)
])
def test_iso_long_fmt(identifier, expected):
assert {
k: v for k, v in cli_tokenise(identifier).items()
if v is not None
} == expected

0 comments on commit 1a9c681

Please sign in to comment.