From 02493720da64f5c26fc36410133c5cd9f9201e82 Mon Sep 17 00:00:00 2001 From: Ronnie Dutta <61982285+MetRonnie@users.noreply.github.com> Date: Wed, 1 Nov 2023 13:51:40 +0000 Subject: [PATCH 1/8] Fix parentheses on RHS of trigger expression bug --- cylc/flow/graph_parser.py | 25 ++++++---- tests/unit/test_graph_parser.py | 87 ++++++++++++++++++++++++++------- 2 files changed, 85 insertions(+), 27 deletions(-) diff --git a/cylc/flow/graph_parser.py b/cylc/flow/graph_parser.py index f37954712c0..0656e420091 100644 --- a/cylc/flow/graph_parser.py +++ b/cylc/flow/graph_parser.py @@ -85,10 +85,10 @@ class GraphParser: store dependencies for the whole workflow (call parse_graph multiple times and key results by graph section). - The general form of a dependency is "EXPRESSION => NODE", where: - * On the right, NODE is a task or family name + The general form of a dependency is "LHS => RHS", where: * On the left, an EXPRESSION of nodes involving parentheses, and logical operators '&' (AND), and '|' (OR). + * On the right, an EXPRESSION of nodes NOT involving '|' * Node names may be parameterized (any number of parameters): NODE NODE # specific parameter value @@ -517,16 +517,18 @@ def _proc_dep_pair( "Suicide markers must be" f" on the right of a trigger: {left}") + # Check that parentheses match. + mismatch_msg = 'Mismatched parentheses in: "{}"' + if left and left.count("(") != left.count(")"): + raise GraphParseError(mismatch_msg.format(left)) + if right.count("(") != right.count(")"): + raise GraphParseError(mismatch_msg.format(right)) + # Ignore cycle point offsets on the right side. # (Note we can't ban this; all nodes get process as left and right.) if '[' in right: return - # Check that parentheses match. - if left and left.count("(") != left.count(")"): - raise GraphParseError( - "Mismatched parentheses in: \"" + left + "\"") - # Split right side on AND. rights = right.split(self.__class__.OP_AND) if '' in rights or right and not all(rights): @@ -847,9 +849,14 @@ def _compute_triggers( trigs += [f"{name}{offset}:{trigger}"] for right in rights: + right = right.strip('()') # parentheses don't matter m = self.__class__.REC_RHS_NODE.match(right) - # This will match, bad nodes are detected earlier (type ignore): - suicide_char, name, output, opt_char = m.groups() # type: ignore + if not m: + # Bad nodes should have been detected earlier; fail loudly + raise ValueError( # pragma: no cover + f"Unexpected graph expression: '{right}'" + ) + suicide_char, name, output, opt_char = m.groups() suicide = (suicide_char == self.__class__.SUICIDE) optional = (opt_char == self.__class__.OPTIONAL) if output: diff --git a/tests/unit/test_graph_parser.py b/tests/unit/test_graph_parser.py index 97b7fb45483..ddd443a3597 100644 --- a/tests/unit/test_graph_parser.py +++ b/tests/unit/test_graph_parser.py @@ -16,6 +16,7 @@ """Unit tests for the GraphParser.""" import logging +from typing import Dict, List import pytest from itertools import product from pytest import param @@ -86,45 +87,59 @@ def test_graph_syntax_errors_2(seq, graph, expected_err): @pytest.mark.parametrize( 'graph, expected_err', [ - [ + ( "a b => c", "Bad graph node format" - ], - [ + ), + ( + "a => b c", + "Bad graph node format" + ), + ( "!foo => bar", "Suicide markers must be on the right of a trigger:" - ], - [ + ), + ( "( foo & bar => baz", - "Mismatched parentheses in:" - ], - [ + 'Mismatched parentheses in: "(foo&bar"' + ), + ( + "a => b & c)", + 'Mismatched parentheses in: "b&c)"' + ), + ( + "(a => b & c)", + 'Mismatched parentheses in: "(a"' + ), + ( + "(a => b[+P1]", + 'Mismatched parentheses in: "(a"' + ), + ( """(a | b & c) => d foo => bar (a | b & c) => !d""", "can't trigger both d and !d" - ], - [ + ), + ( "a => b | c", "Illegal OR on right side" - ], - [ + ), + ( "foo && bar => baz", "The graph AND operator is '&'" - ], - [ + ), + ( "foo || bar => baz", "The graph OR operator is '|'" - ], + ), ] ) def test_graph_syntax_errors(graph, expected_err): """Test various graph syntax errors.""" with pytest.raises(GraphParseError) as cm: GraphParser().parse_graph(graph) - assert ( - expected_err in str(cm.value) - ) + assert expected_err in str(cm.value) def test_parse_graph_simple(): @@ -845,3 +860,39 @@ def test_fail_family_triggers_on_tasks(ftrig): "family trigger on non-family namespace" ) ) + + +@pytest.mark.parametrize( + 'graph, expected_triggers', + [ + param( + 'a => b & c', + {'a': [''], 'b': ['a:succeeded'], 'c': ['a:succeeded']}, + id="simple" + ), + param( + 'a => (b & c)', + {'a': [''], 'b': ['a:succeeded'], 'c': ['a:succeeded']}, + id="simple w/ parentheses" + ), + param( + 'a => (b & (c & d))', + { + 'a': [''], + 'b': ['a:succeeded'], + 'c': ['a:succeeded'], + 'd': ['a:succeeded'], + }, + id="more parentheses" + ), + ] +) +def test_RHS_AND(graph: str, expected_triggers: Dict[str, List[str]]): + """Test '&' operator on right hand side of trigger expression.""" + gp = GraphParser() + gp.parse_graph(graph) + triggers = { + task: list(trigs.keys()) + for task, trigs in gp.triggers.items() + } + assert triggers == expected_triggers From 0a408ada07767915b2add2d41468a66dbfd569cb Mon Sep 17 00:00:00 2001 From: Ronnie Dutta <61982285+MetRonnie@users.noreply.github.com> Date: Wed, 1 Nov 2023 13:52:38 +0000 Subject: [PATCH 2/8] Fix type annotation --- cylc/flow/graph_parser.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/cylc/flow/graph_parser.py b/cylc/flow/graph_parser.py index 0656e420091..ad0ec280a3d 100644 --- a/cylc/flow/graph_parser.py +++ b/cylc/flow/graph_parser.py @@ -23,7 +23,8 @@ Dict, List, Tuple, - Optional + Optional, + Union ) import cylc.flow.flags @@ -535,16 +536,15 @@ def _proc_dep_pair( raise GraphParseError( f"Null task name in graph: {left} => {right}") + lefts: Union[List[str], List[Optional[str]]] if not left or (self.__class__.OP_OR in left or '(' in left): - # Treat conditional or bracketed expressions as a single entity. + # Treat conditional or parenthesised expressions as a single entity # Can get [None] or [""] here - lefts: List[Optional[str]] = [left] + lefts = [left] else: # Split non-conditional left-side expressions on AND. # Can get [""] here too - # TODO figure out how to handle this wih mypy: - # assign List[str] to List[Optional[str]] - lefts = left.split(self.__class__.OP_AND) # type: ignore + lefts = left.split(self.__class__.OP_AND) if '' in lefts or left and not all(lefts): raise GraphParseError( f"Null task name in graph: {left} => {right}") From 238ce2890c4ccb2ecc576a5c79902fb2e8a22cba Mon Sep 17 00:00:00 2001 From: Ronnie Dutta <61982285+MetRonnie@users.noreply.github.com> Date: Thu, 2 Nov 2023 11:24:41 +0000 Subject: [PATCH 3/8] Add missing test cases for `next()`/`previous()` (#5777) tests/u: add missing test cases for `next()`/`previous()` * xfail tests in response to bug discovery * https://github.com/cylc/cylc-flow/pull/5777#issuecomment-1772521629 --- tests/unit/cycling/test_iso8601.py | 191 +++++++++++------------------ 1 file changed, 74 insertions(+), 117 deletions(-) diff --git a/tests/unit/cycling/test_iso8601.py b/tests/unit/cycling/test_iso8601.py index 98fc95b9c5b..ae0eb957f47 100644 --- a/tests/unit/cycling/test_iso8601.py +++ b/tests/unit/cycling/test_iso8601.py @@ -14,9 +14,11 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -import pytest from datetime import datetime +import pytest +from pytest import param + from cylc.flow.cycling.iso8601 import ( ISO8601Interval, ISO8601Point, @@ -671,74 +673,52 @@ def test_simple(set_cycling_type): assert not sequence.is_on_sequence(ISO8601Point("20100809T0005")) -def test_next_simple(set_cycling_type): +@pytest.mark.parametrize( + 'value, expected', [ + ('next(T2100Z)', '20100808T2100Z'), + ('next(T00)', '20100809T0000Z'), + ('next(T-15)', '20100808T1615Z'), + ('next(T-45)', '20100808T1545Z'), + ('next(-10)', '21100101T0000Z'), + ('next(-1008)', '21100801T0000Z'), + ('next(--10)', '20101001T0000Z'), + ('next(--0325)', '20110325T0000Z'), + ('next(---10)', '20100810T0000Z'), + ('next(---05T1200Z)', '20100905T1200Z'), + param('next(--08-08)', '20110808T0000Z', marks=pytest.mark.xfail), + ('next(T15)', '20100809T1500Z'), + ('next(T-41)', '20100808T1541Z'), + ] +) +def test_next_simple(value: str, expected: str, set_cycling_type): """Test the generation of CP using 'next' from single input.""" set_cycling_type(ISO8601_CYCLING_TYPE, "Z") - my_now = "20100808T1540Z" - sequence = ( - "next(T2100Z)", # 20100808T2100Z - "next(T00)", # 20100809T0000Z - "next(T-15)", # 20100808T1615Z - "next(T-45)", # 20100808T1545Z - "next(-10)", # 21100101T0000Z - "next(-1008)", # 21100801T0000Z - "next(--10)", # 20101001T0000Z - "next(--0325)", # 20110325T0000Z - "next(---10)", # 20100810T0000Z - "next(---05T1200Z)", # 20100905T1200Z - ) + my_now = "2010-08-08T15:41Z" + assert ingest_time(value, my_now) == expected - output = [] - for point in sequence: - output.append(ingest_time(point, my_now)) - assert output == [ - "20100808T2100Z", - "20100809T0000Z", - "20100808T1615Z", - "20100808T1545Z", - "21100101T0000Z", - "21100801T0000Z", - "20101001T0000Z", - "20110325T0000Z", - "20100810T0000Z", - "20100905T1200Z", +@pytest.mark.parametrize( + 'value, expected', [ + ('previous(T2100Z)', '20100807T2100Z'), + ('previous(T00)', '20100808T0000Z'), + ('previous(T-15)', '20100808T1515Z'), + ('previous(T-45)', '20100808T1445Z'), + ('previous(-10)', '20100101T0000Z'), + ('previous(-1008)', '20100801T0000Z'), + ('previous(--10)', '20091001T0000Z'), + ('previous(--0325)', '20100325T0000Z'), + ('previous(---10)', '20100710T0000Z'), + ('previous(---05T1200Z)', '20100805T1200Z'), + param('previous(--08-08)', '20100808T0000Z', marks=pytest.mark.xfail), + ('previous(T15)', '20100808T1500Z'), + ('previous(T-41)', '20100808T1441Z'), ] - - -def test_previous_simple(set_cycling_type): +) +def test_previous_simple(value: str, expected: str, set_cycling_type): """Test the generation of CP using 'previous' from single input.""" set_cycling_type(ISO8601_CYCLING_TYPE, "Z") - my_now = "20100808T1540Z" - sequence = ( - "previous(T2100Z)", # 20100807T2100Z - "previous(T00)", # 20100808T0000Z - "previous(T-15)", # 20100808T1515Z - "previous(T-45)", # 20100808T1445Z - "previous(-10)", # 20100101T0000Z - "previous(-1008)", # 20100801T0000Z - "previous(--10)", # 20091001T0000Z - "previous(--0325)", # 20100325T0000Z - "previous(---10)", # 20100710T0000Z - "previous(---05T1200Z)", # 20100805T1200Z - ) - - output = [] - - for point in sequence: - output.append(ingest_time(point, my_now)) - assert output == [ - "20100807T2100Z", - "20100808T0000Z", - "20100808T1515Z", - "20100808T1445Z", - "20100101T0000Z", - "20100801T0000Z", - "20091001T0000Z", - "20100325T0000Z", - "20100710T0000Z", - "20100805T1200Z", - ] + my_now = "2010-08-08T15:41Z" + assert ingest_time(value, my_now) == expected def test_sequence(set_cycling_type): @@ -855,63 +835,40 @@ def test_weeks_days(set_cycling_type): ] -def test_cug(set_cycling_type): - """Test the offset CP examples in the Cylc user guide""" +@pytest.mark.parametrize( + 'value, expected', [ + ('next(T-00)', '20180314T1600Z'), + ('previous(T-00)', '20180314T1500Z'), + ('next(T-00; T-15; T-30; T-45)', '20180314T1515Z'), + ('previous(T-00; T-15; T-30; T-45)', '20180314T1500Z'), + ('next(T00)', '20180315T0000Z'), + ('previous(T00)', '20180314T0000Z'), + ('next(T06:30Z)', '20180315T0630Z'), + ('previous(T06:30) -P1D', '20180313T0630Z'), + ('next(T00; T06; T12; T18)', '20180314T1800Z'), + ('previous(T00; T06; T12; T18)', '20180314T1200Z'), + ('next(T00; T06; T12; T18)+P1W', '20180321T1800Z'), + ('PT1H', '20180314T1612Z'), + ('-P1M', '20180214T1512Z'), + ('next(-00)', '21000101T0000Z'), + ('previous(--01)', '20180101T0000Z'), + ('next(---01)', '20180401T0000Z'), + ('previous(--1225)', '20171225T0000Z'), + ('next(-2006)', '20200601T0000Z'), + ('previous(-W101)', '20180305T0000Z'), + ('next(-W-1; -W-3; -W-5)', '20180314T0000Z'), + ('next(-001; -091; -181; -271)', '20180401T0000Z'), + ('previous(-365T12Z)', '20171231T1200Z'), + ] +) +def test_user_guide_examples(value: str, expected: str, set_cycling_type): + """Test the offset CP examples in the Cylc user guide. + + https://cylc.github.io/cylc-doc/stable/html/user-guide/writing-workflows/scheduling.html + """ set_cycling_type(ISO8601_CYCLING_TYPE, "Z") my_now = "2018-03-14T15:12Z" - sequence = ( - "next(T-00)", # 20180314T1600Z - "previous(T-00)", # 20180314T1500Z - "next(T-00; T-15; T-30; T-45)", # 20180314T1515Z - "previous(T-00; T-15; T-30; T-45)", # 20180314T1500Z - "next(T00)", # 20180315T0000Z - "previous(T00)", # 20180314T0000Z - "next(T06:30Z)", # 20180315T0630Z - "previous(T06:30) -P1D", # 20180313T0630Z - "next(T00; T06; T12; T18)", # 20180314T1800Z - "previous(T00; T06; T12; T18)", # 20180314T1200Z - "next(T00; T06; T12; T18)+P1W", # 20180321T1800Z - "PT1H", # 20180314T1612Z - "-P1M", # 20180214T1512Z - "next(-00)", # 21000101T0000Z - "previous(--01)", # 20180101T0000Z - "next(---01)", # 20180401T0000Z - "previous(--1225)", # 20171225T0000Z - "next(-2006)", # 20200601T0000Z - "previous(-W101)", # 20180305T0000Z - "next(-W-1; -W-3; -W-5)", # 20180314T0000Z - "next(-001; -091; -181; -271)", # 20180401T0000Z - "previous(-365T12Z)", # 20171231T1200Z - ) - - output = [] - - for point in sequence: - output.append(ingest_time(point, my_now)) - assert output == [ - "20180314T1600Z", - "20180314T1500Z", - "20180314T1515Z", - "20180314T1500Z", - "20180315T0000Z", - "20180314T0000Z", - "20180315T0630Z", - "20180313T0630Z", - "20180314T1800Z", - "20180314T1200Z", - "20180321T1800Z", - "20180314T1612Z", - "20180214T1512Z", - "21000101T0000Z", - "20180101T0000Z", - "20180401T0000Z", - "20171225T0000Z", - "20200601T0000Z", - "20180305T0000Z", - "20180314T0000Z", - "20180401T0000Z", - "20171231T1200Z", - ] + assert ingest_time(value, my_now) == expected def test_next_simple_no_now(set_cycling_type): From b23a4869c3202053b283beb7869f84dfc69fc73b Mon Sep 17 00:00:00 2001 From: Tim Pillinger <26465611+wxtim@users.noreply.github.com> Date: Thu, 2 Nov 2023 11:38:27 +0000 Subject: [PATCH 4/8] Fix.all xtriggers on an itask are the same (#5791) xtriggers: wait for the latest clock trigger * stop task proxy clock trigger time being cached to prevent all clock triggers being the same. --------- Co-authored-by: Ronnie Dutta <61982285+MetRonnie@users.noreply.github.com> --- changes.d/5791.fix.md | 1 + cylc/flow/task_proxy.py | 42 +++++++++------- cylc/flow/xtrigger_mgr.py | 1 + tests/integration/test_xtrigger_mgr.py | 67 ++++++++++++++++++++++++++ tests/unit/test_task_proxy.py | 5 +- 5 files changed, 96 insertions(+), 20 deletions(-) create mode 100644 changes.d/5791.fix.md create mode 100644 tests/integration/test_xtrigger_mgr.py diff --git a/changes.d/5791.fix.md b/changes.d/5791.fix.md new file mode 100644 index 00000000000..8743451aab8 --- /dev/null +++ b/changes.d/5791.fix.md @@ -0,0 +1 @@ +fix a bug where if multiple clock triggers are set for a task only one was being satisfied. diff --git a/cylc/flow/task_proxy.py b/cylc/flow/task_proxy.py index 5e550c04a99..1a5fe6b71a7 100644 --- a/cylc/flow/task_proxy.py +++ b/cylc/flow/task_proxy.py @@ -26,7 +26,6 @@ from metomi.isodatetime.timezone import get_local_time_zone from cylc.flow import LOG -from cylc.flow.id import Tokens from cylc.flow.platforms import get_platform from cylc.flow.task_action_timer import TimerFlags from cylc.flow.task_state import TaskState, TASK_STATUS_WAITING @@ -39,6 +38,7 @@ ) if TYPE_CHECKING: + from cylc.flow.id import Tokens from cylc.flow.cycling import PointBase from cylc.flow.task_action_timer import TaskActionTimer from cylc.flow.taskdef import TaskDef @@ -48,9 +48,9 @@ class TaskProxy: """Represent an instance of a cycling task in a running workflow. Attributes: - .clock_trigger_time: - Clock trigger time in seconds since epoch. - (Used for wall_clock xtrigger). + .clock_trigger_times: + Memoization of clock trigger times (Used for wall_clock xtrigger): + {offset string: seconds from epoch} .expire_time: Time in seconds since epoch when this task is considered expired. .identity: @@ -154,7 +154,7 @@ class TaskProxy: # Memory optimization - constrain possible attributes to this list. __slots__ = [ - 'clock_trigger_time', + 'clock_trigger_times', 'expire_time', 'identity', 'is_late', @@ -247,7 +247,7 @@ def __init__( self.try_timers: Dict[str, 'TaskActionTimer'] = {} self.non_unique_events = Counter() # type: ignore # TODO: figure out - self.clock_trigger_time: Optional[float] = None + self.clock_trigger_times: Dict[str, int] = {} self.expire_time: Optional[float] = None self.late_time: Optional[float] = None self.is_late = is_late @@ -358,25 +358,31 @@ def get_point_as_seconds(self): self.point_as_seconds += utc_offset_in_seconds return self.point_as_seconds - def get_clock_trigger_time(self, offset_str): - """Compute, cache, and return trigger time relative to cycle point. + def get_clock_trigger_time( + self, + point: 'PointBase', offset_str: Optional[str] = None + ) -> int: + """Compute, cache and return trigger time relative to cycle point. Args: - offset_str: ISO8601Interval string, e.g. "PT2M". - Can be None for zero offset. + point: Task's cycle point. + offset_str: ISO8601 interval string, e.g. "PT2M". + Can be None for zero offset. Returns: Absolute trigger time in seconds since Unix epoch. """ - if self.clock_trigger_time is None: - if offset_str is None: - trigger_time = self.point + offset_str = offset_str if offset_str else 'P0Y' + if offset_str not in self.clock_trigger_times: + if offset_str == 'P0Y': + trigger_time = point else: - trigger_time = self.point + ISO8601Interval(offset_str) - self.clock_trigger_time = int( - point_parse(str(trigger_time)).seconds_since_unix_epoch - ) - return self.clock_trigger_time + trigger_time = point + ISO8601Interval(offset_str) + + offset = int( + point_parse(str(trigger_time)).seconds_since_unix_epoch) + self.clock_trigger_times[offset_str] = offset + return self.clock_trigger_times[offset_str] def get_try_num(self): """Return the number of automatic tries (try number).""" diff --git a/cylc/flow/xtrigger_mgr.py b/cylc/flow/xtrigger_mgr.py index 4512d97b7c8..35b8e88b36c 100644 --- a/cylc/flow/xtrigger_mgr.py +++ b/cylc/flow/xtrigger_mgr.py @@ -388,6 +388,7 @@ def get_xtrig_ctx(self, itask: TaskProxy, label: str) -> SubFuncContext: # External (clock xtrigger): convert offset to trigger_time. # Datetime cycling only. kwargs["trigger_time"] = itask.get_clock_trigger_time( + itask.point, ctx.func_kwargs["offset"] ) else: diff --git a/tests/integration/test_xtrigger_mgr.py b/tests/integration/test_xtrigger_mgr.py new file mode 100644 index 00000000000..07abbdac24d --- /dev/null +++ b/tests/integration/test_xtrigger_mgr.py @@ -0,0 +1,67 @@ +# THIS FILE IS PART OF THE CYLC WORKFLOW ENGINE. +# Copyright (C) NIWA & British Crown (Met Office) & Contributors. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +"""Tests for the behaviour of xtrigger manager. +""" + + +async def test_2_xtriggers(flow, start, scheduler, monkeypatch): + """Test that if an itask has 2 wall_clock triggers with different + offsets that xtrigger manager gets both of them. + + https://github.com/cylc/cylc-flow/issues/5783 + + n.b. Clock 3 exists to check the memoization path is followed, + and causing this test to give greater coverage. + """ + task_point = 1588636800 # 2020-05-05 + ten_years_ahead = 1904169600 # 2030-05-05 + monkeypatch.setattr( + 'cylc.flow.xtriggers.wall_clock.time', + lambda: ten_years_ahead - 1 + ) + id_ = flow({ + 'scheduler': { + 'allow implicit tasks': True + }, + 'scheduling': { + 'initial cycle point': '2020-05-05', + 'xtriggers': { + 'clock_1': 'wall_clock()', + 'clock_2': 'wall_clock(offset=P10Y)', + 'clock_3': 'wall_clock(offset=P10Y)', + }, + 'graph': { + 'R1': '@clock_1 & @clock_2 & @clock_3 => foo' + } + } + }) + schd = scheduler(id_) + async with start(schd): + foo_proxy = schd.pool.get_tasks()[0] + clock_1_ctx = schd.xtrigger_mgr.get_xtrig_ctx(foo_proxy, 'clock_1') + clock_2_ctx = schd.xtrigger_mgr.get_xtrig_ctx(foo_proxy, 'clock_2') + clock_3_ctx = schd.xtrigger_mgr.get_xtrig_ctx(foo_proxy, 'clock_2') + + assert clock_1_ctx.func_kwargs['trigger_time'] == task_point + assert clock_2_ctx.func_kwargs['trigger_time'] == ten_years_ahead + assert clock_3_ctx.func_kwargs['trigger_time'] == ten_years_ahead + + schd.xtrigger_mgr.call_xtriggers_async(foo_proxy) + assert foo_proxy.state.xtriggers == { + 'clock_1': True, + 'clock_2': False, + 'clock_3': False, + } diff --git a/tests/unit/test_task_proxy.py b/tests/unit/test_task_proxy.py index 5369e70f124..98695ecd13f 100644 --- a/tests/unit/test_task_proxy.py +++ b/tests/unit/test_task_proxy.py @@ -60,9 +60,10 @@ def test_get_clock_trigger_time( set_cycling_type(itask_point.TYPE) mock_itask = Mock( point=itask_point.standardise(), - clock_trigger_time=None + clock_trigger_times={} ) - assert TaskProxy.get_clock_trigger_time(mock_itask, offset_str) == expected + assert TaskProxy.get_clock_trigger_time( + mock_itask, mock_itask.point, offset_str) == expected @pytest.mark.parametrize( From fe01d808342e3bef2fb44600d61afdc1dfd1280c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 2 Nov 2023 16:09:02 +0000 Subject: [PATCH 5/8] Prepare release 8.2.3 Workflow: Release stage 1 - create release PR (Cylc 8+ only), run: 29 --- CHANGES.md | 12 ++++++++++++ changes.d/5660.fix.md | 1 - changes.d/5753.fix.md | 1 - changes.d/5776.fix.md | 1 - changes.d/5791.fix.md | 1 - cylc/flow/__init__.py | 2 +- 6 files changed, 13 insertions(+), 5 deletions(-) delete mode 100644 changes.d/5660.fix.md delete mode 100644 changes.d/5753.fix.md delete mode 100644 changes.d/5776.fix.md delete mode 100644 changes.d/5791.fix.md diff --git a/CHANGES.md b/CHANGES.md index 9a5af5524fe..4dfe61f42db 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -11,6 +11,18 @@ $ towncrier create ..md --content "Short description" +## __cylc-8.2.3 (Released 2023-11-02)__ + +### 🔧 Fixes + +[#5660](https://github.com/cylc/cylc-flow/pull/5660) - Re-worked graph n-window algorithm for better efficiency. + +[#5753](https://github.com/cylc/cylc-flow/pull/5753) - Fixed bug where execution time limit polling intervals could end up incorrectly applied + +[#5776](https://github.com/cylc/cylc-flow/pull/5776) - Ensure that submit-failed tasks are marked as incomplete (so remain visible) when running in back-compat mode. + +[#5791](https://github.com/cylc/cylc-flow/pull/5791) - fix a bug where if multiple clock triggers are set for a task only one was being satisfied. + ## __cylc-8.2.2 (Released 2023-10-05)__ ### 🚀 Enhancements diff --git a/changes.d/5660.fix.md b/changes.d/5660.fix.md deleted file mode 100644 index 65cdea538a9..00000000000 --- a/changes.d/5660.fix.md +++ /dev/null @@ -1 +0,0 @@ -Re-worked graph n-window algorithm for better efficiency. diff --git a/changes.d/5753.fix.md b/changes.d/5753.fix.md deleted file mode 100644 index 47380af7ab5..00000000000 --- a/changes.d/5753.fix.md +++ /dev/null @@ -1 +0,0 @@ -Fixed bug where execution time limit polling intervals could end up incorrectly applied diff --git a/changes.d/5776.fix.md b/changes.d/5776.fix.md deleted file mode 100644 index d123c2cb24f..00000000000 --- a/changes.d/5776.fix.md +++ /dev/null @@ -1 +0,0 @@ -Ensure that submit-failed tasks are marked as incomplete (so remain visible) when running in back-compat mode. diff --git a/changes.d/5791.fix.md b/changes.d/5791.fix.md deleted file mode 100644 index 8743451aab8..00000000000 --- a/changes.d/5791.fix.md +++ /dev/null @@ -1 +0,0 @@ -fix a bug where if multiple clock triggers are set for a task only one was being satisfied. diff --git a/cylc/flow/__init__.py b/cylc/flow/__init__.py index 54ecbc1f571..4b5ecfe206e 100644 --- a/cylc/flow/__init__.py +++ b/cylc/flow/__init__.py @@ -53,7 +53,7 @@ def environ_init(): environ_init() -__version__ = '8.2.3.dev' +__version__ = '8.2.3' def iter_entry_points(entry_point_name): From c6c1ef8da3436a23ff2e7c4593b7487bef1cf9b7 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 3 Nov 2023 10:12:51 +0000 Subject: [PATCH 6/8] Bump dev version (#5808) --- cylc/flow/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cylc/flow/__init__.py b/cylc/flow/__init__.py index 4b5ecfe206e..f6e90da3c53 100644 --- a/cylc/flow/__init__.py +++ b/cylc/flow/__init__.py @@ -53,7 +53,7 @@ def environ_init(): environ_init() -__version__ = '8.2.3' +__version__ = '8.2.4.dev' def iter_entry_points(entry_point_name): From c5714f46cc327e1c3cc67b8c4faf116678d28881 Mon Sep 17 00:00:00 2001 From: Ronnie Dutta <61982285+MetRonnie@users.noreply.github.com> Date: Fri, 10 Nov 2023 16:05:36 +0000 Subject: [PATCH 7/8] Update changelog [skip ci] --- changes.d/5801.fix.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 changes.d/5801.fix.md diff --git a/changes.d/5801.fix.md b/changes.d/5801.fix.md new file mode 100644 index 00000000000..e7fd0584090 --- /dev/null +++ b/changes.d/5801.fix.md @@ -0,0 +1 @@ +Fix traceback when using parentheses on right hand side of graph trigger. From 6b1da953e8d2e43a097992a3c2013eb37e2167f6 Mon Sep 17 00:00:00 2001 From: Ronnie Dutta <61982285+MetRonnie@users.noreply.github.com> Date: Mon, 13 Nov 2023 12:55:50 +0000 Subject: [PATCH 8/8] Address Mypy & Flake8 identified problems --- cylc/flow/cfgspec/globalcfg.py | 4 ++-- cylc/flow/scheduler_cli.py | 4 ++-- cylc/flow/task_proxy.py | 1 - 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/cylc/flow/cfgspec/globalcfg.py b/cylc/flow/cfgspec/globalcfg.py index b404a42fdbe..59c5d26d62c 100644 --- a/cylc/flow/cfgspec/globalcfg.py +++ b/cylc/flow/cfgspec/globalcfg.py @@ -22,7 +22,7 @@ from typing import List, Optional, Tuple, Any, Union from contextlib import suppress -from packaging.version import parse as parse_version, Version +from packaging.version import Version from cylc.flow import LOG from cylc.flow import __version__ as CYLC_VERSION @@ -1866,7 +1866,7 @@ def get_version_hierarchy(version: str) -> List[str]: ['', '8', '8.0', '8.0.1', '8.0.1a2', '8.0.1a2.dev'] """ - smart_ver: Version = parse_version(version) + smart_ver = Version(version) base = [str(i) for i in smart_ver.release] hierarchy = [''] hierarchy += ['.'.join(base[:i]) for i in range(1, len(base) + 1)] diff --git a/cylc/flow/scheduler_cli.py b/cylc/flow/scheduler_cli.py index e2f2d98f46a..b1d69770914 100644 --- a/cylc/flow/scheduler_cli.py +++ b/cylc/flow/scheduler_cli.py @@ -25,7 +25,7 @@ import sys from typing import TYPE_CHECKING -from packaging.version import parse as parse_version +from packaging.version import Version from cylc.flow import LOG, __version__ from cylc.flow.exceptions import ( @@ -468,7 +468,7 @@ def _version_check( if not db_file.is_file(): # not a restart return True - this_version = parse_version(__version__) + this_version = Version(__version__) last_run_version = WorkflowDatabaseManager.check_db_compatibility(db_file) for itt, (this, that) in enumerate(zip_longest( diff --git a/cylc/flow/task_proxy.py b/cylc/flow/task_proxy.py index ef5203cbeae..3db5c731ec7 100644 --- a/cylc/flow/task_proxy.py +++ b/cylc/flow/task_proxy.py @@ -42,7 +42,6 @@ from cylc.flow.cycling import PointBase from cylc.flow.task_action_timer import TaskActionTimer from cylc.flow.taskdef import TaskDef - from cylc.flow.id import Tokens class TaskProxy: