diff --git a/changes.d/5928.break.md b/changes.d/5928.break.md new file mode 100644 index 00000000000..6d9fc41d13c --- /dev/null +++ b/changes.d/5928.break.md @@ -0,0 +1 @@ +Fixed behaviour of `initial cycle point = next(...)` which could result in a time in the past, and equivalently for `previous()`. E.g. if the current time is `2024-01-01T12:30Z`, `next(--01-01)` now evaluates to `2025-01-01T00:00Z` instead of `2024-01-01T00:00Z`. diff --git a/conda-environment.yml b/conda-environment.yml index fed33d57a88..7463b46f886 100644 --- a/conda-environment.yml +++ b/conda-environment.yml @@ -9,7 +9,7 @@ dependencies: - graphviz # for static graphing # Note: can't pin jinja2 any higher than this until we give up on Cylc 7 back-compat - jinja2 >=3.0,<3.1 - - metomi-isodatetime >=1!3.0.0, <1!3.2.0 + - metomi-isodatetime >=1!3.2.0, <1!3.3.0 - packaging # Constrain protobuf version for compatible Scheduler-UIS comms across hosts - protobuf >=4.24.4,<4.25.0 diff --git a/cylc/flow/commands.py b/cylc/flow/commands.py index 62be927f00b..16cfd6958f2 100644 --- a/cylc/flow/commands.py +++ b/cylc/flow/commands.py @@ -197,7 +197,7 @@ async def stop( # schedule shutdown after wallclock time passes provided time parser = TimePointParser() schd.set_stop_clock( - int(parser.parse(clock_time).seconds_since_unix_epoch) + parser.parse(clock_time).seconds_since_unix_epoch ) schd._update_workflow_state() elif task is not None: diff --git a/cylc/flow/cycling/iso8601.py b/cylc/flow/cycling/iso8601.py index 9b93bc77e6e..07cf8c90b79 100644 --- a/cylc/flow/cycling/iso8601.py +++ b/cylc/flow/cycling/iso8601.py @@ -19,32 +19,52 @@ import contextlib from functools import lru_cache import re -from typing import List, Optional, TYPE_CHECKING, Tuple +from typing import ( + TYPE_CHECKING, + List, + Optional, + Tuple, +) -from metomi.isodatetime.data import Calendar, CALENDAR, Duration +from metomi.isodatetime.data import ( + CALENDAR, + Calendar, +) from metomi.isodatetime.dumpers import TimePointDumper -from metomi.isodatetime.timezone import ( - get_local_time_zone, get_local_time_zone_format, TimeZoneFormatMode) from metomi.isodatetime.exceptions import IsodatetimeError from metomi.isodatetime.parsers import ISO8601SyntaxError -from cylc.flow.time_parser import CylcTimeParser +from metomi.isodatetime.timezone import ( + TimeZoneFormatMode, + get_local_time_zone, + get_local_time_zone_format, +) + from cylc.flow.cycling import ( - PointBase, IntervalBase, SequenceBase, ExclusionBase, cmp + ExclusionBase, + IntervalBase, + PointBase, + SequenceBase, + cmp, ) from cylc.flow.exceptions import ( CylcConfigError, IntervalParsingError, PointParsingError, SequenceDegenerateError, - WorkflowConfigError + WorkflowConfigError, ) -from cylc.flow.wallclock import get_current_time_string from cylc.flow.parsec.validate import IllegalValueError +from cylc.flow.time_parser import CylcTimeParser +from cylc.flow.wallclock import get_current_time_string + if TYPE_CHECKING: from metomi.isodatetime.data import TimePoint from metomi.isodatetime.parsers import ( - DurationParser, TimePointParser, TimeRecurrenceParser) + DurationParser, + TimePointParser, + TimeRecurrenceParser, + ) CYCLER_TYPE_ISO8601 = "iso8601" CYCLER_TYPE_SORT_KEY_ISO8601 = 1 @@ -706,14 +726,6 @@ def ingest_time(value: str, now: Optional[str] = None) -> str: now = get_current_time_string() now_point = parser.parse(now) - # correct for year in 'now' if year is the only date unit specified - - # https://github.com/cylc/cylc-flow/issues/4805#issuecomment-1103928604 - if re.search(r"\(-\d{2}[);T]", value): - now_point += Duration(years=1) - # likewise correct for month if year and month are the only date units - elif re.search(r"\(-\d{4}[);T]", value): - now_point += Duration(months=1) - # perform whatever transformation is required offset = None if is_prev_next: @@ -806,28 +818,6 @@ def prev_next( cycle_point = timepoints[my_diff.index(min(my_diff))] - # ensure truncated dates do not have time from 'now' included' - - # https://github.com/metomi/isodatetime/issues/212 - if 'T' not in value.split(')')[0]: - # NOTE: Strictly speaking we shouldn't forcefully mutate TimePoints - # in this way as they're meant to be immutable since - # https://github.com/metomi/isodatetime/pull/165, however it - # should be ok as long as the TimePoint is not used as a dict key and - # we don't call any of the TimePoint's cached methods until after we've - # finished mutating it. - cycle_point._hour_of_day = 0 - cycle_point._minute_of_hour = 0 - cycle_point._second_of_minute = 0 - # likewise ensure month and day from 'now' are not included - # where they did not appear in the truncated datetime - if re.search(r"\(-\d{2}[);T]", value): - # case 1 - year only - cycle_point._month_of_year = 1 - cycle_point._day_of_month = 1 - elif re.search(r"\(-(-\d{2}|\d{4})[;T)]", value): - # case 2 - month only or year and month - cycle_point._day_of_month = 1 - return cycle_point, offset diff --git a/cylc/flow/task_proxy.py b/cylc/flow/task_proxy.py index 620b647f909..580343618f6 100644 --- a/cylc/flow/task_proxy.py +++ b/cylc/flow/task_proxy.py @@ -414,7 +414,7 @@ def get_point_as_seconds(self): """Compute and store my cycle point as seconds since epoch.""" if self.point_as_seconds is None: iso_timepoint = point_parse(str(self.point)) - self.point_as_seconds = int(iso_timepoint.seconds_since_unix_epoch) + self.point_as_seconds = iso_timepoint.seconds_since_unix_epoch if iso_timepoint.time_zone.unknown: utc_offset_hours, utc_offset_minutes = ( get_local_time_zone()) @@ -448,7 +448,7 @@ def get_clock_trigger_time( else: trigger_time = point_time + interval_parse(offset_str) - self.clock_trigger_times[offset_str] = int( + self.clock_trigger_times[offset_str] = ( trigger_time.seconds_since_unix_epoch ) return self.clock_trigger_times[offset_str] diff --git a/setup.cfg b/setup.cfg index 9ade7832161..26f777f9a12 100644 --- a/setup.cfg +++ b/setup.cfg @@ -68,7 +68,7 @@ install_requires = graphene>=2.1,<3 # Note: can't pin jinja2 any higher than this until we give up on Cylc 7 back-compat jinja2==3.0.* - metomi-isodatetime>=1!3.0.0,<1!3.2.0 + metomi-isodatetime>=1!3.2.0,<1!3.3.0 # Constrain protobuf version for compatible Scheduler-UIS comms across hosts packaging protobuf>=4.24.4,<4.25.0 diff --git a/tests/unit/cycling/test_iso8601.py b/tests/unit/cycling/test_iso8601.py index 7455e27f0ac..c7dfda28048 100644 --- a/tests/unit/cycling/test_iso8601.py +++ b/tests/unit/cycling/test_iso8601.py @@ -700,7 +700,7 @@ def test_simple(set_cycling_type): ('next(--0325)', '20110325T0000Z'), ('next(---10)', '20100810T0000Z'), ('next(---05T1200Z)', '20100905T1200Z'), - param('next(--08-08)', '20110808T0000Z', marks=pytest.mark.xfail), + ('next(--08-08)', '20110808T0000Z'), ('next(T15)', '20100809T1500Z'), ('next(T-41)', '20100808T1541Z'), ] @@ -724,7 +724,7 @@ def test_next_simple(value: str, expected: str, set_cycling_type): ('previous(--0325)', '20100325T0000Z'), ('previous(---10)', '20100710T0000Z'), ('previous(---05T1200Z)', '20100805T1200Z'), - param('previous(--08-08)', '20100808T0000Z', marks=pytest.mark.xfail), + ('previous(--08-08)', '20100808T0000Z'), ('previous(T15)', '20100808T1500Z'), ('previous(T-41)', '20100808T1441Z'), ] @@ -870,7 +870,7 @@ def test_weeks_days(set_cycling_type): ('previous(--1225)', '20171225T0000Z'), ('next(-2006)', '20200601T0000Z'), ('previous(-W101)', '20180305T0000Z'), - ('next(-W-1; -W-3; -W-5)', '20180314T0000Z'), + ('next(-W-1; -W-3; -W-5)', '20180316T0000Z'), ('next(-001; -091; -181; -271)', '20180401T0000Z'), ('previous(-365T12Z)', '20171231T1200Z'), ] diff --git a/tests/unit/test_workflow_status.py b/tests/unit/test_workflow_status.py index f0c92fd1530..3eb27b94523 100644 --- a/tests/unit/test_workflow_status.py +++ b/tests/unit/test_workflow_status.py @@ -89,7 +89,7 @@ def schd( WORKFLOW_STATUS_RUNNING_TO_STOP % 4 ), ( - {'stop_clock_time': int(STOP_TIME.seconds_since_unix_epoch)}, + {'stop_clock_time': STOP_TIME.seconds_since_unix_epoch}, WorkflowStatus.RUNNING, WORKFLOW_STATUS_RUNNING_TO_STOP % str(STOP_TIME) ),