diff --git a/docs/changes/newsfragments/8345.breaking b/docs/changes/newsfragments/8345.breaking new file mode 100644 index 000000000000..f0dde66245ac --- /dev/null +++ b/docs/changes/newsfragments/8345.breaking @@ -0,0 +1,26 @@ +The default format of ``DataSetProtocol.run_timestamp``, +``DataSetProtocol.completed_timestamp`` and +``qcodes.dataset.sqlite.queries.raw_time_to_str_time`` changed from +``"%Y-%m-%d %H:%M:%S"`` to ``"%Y-%m-%d %H:%M:%S%z"``, i.e. the rendered +timestamps now include the UTC offset of the local timezone, e.g. +``"2026-07-31 10:27:25+0200"``. + +Timestamps are stored in the QCoDeS database as POSIX timestamps, which do not +capture the timezone in which the measurement was performed. They are therefore +still rendered in the local timezone of the machine reading the dataset, but +the resulting string is no longer ambiguous. This also applies to the +``run_timestamp`` and ``completed_timestamp`` attributes written when exporting +a dataset to NetCDF or xarray. + +Pass an explicit ``fmt`` argument to restore the previous behaviour. + +Parameter cache timestamps are now timezone aware. ``ParameterBase.cache.timestamp`` +(and therefore ``Parameter.get_latest.get_timestamp()``) returns a +``datetime`` with ``tzinfo`` set to the local timezone rather than a naive +``datetime``. Code that compares these timestamps to naive ``datetime`` objects +will raise ``TypeError`` and must be updated to use timezone aware datetimes. + +Consequently the ``"ts"`` field of a parameter snapshot changed from +``"%Y-%m-%d %H:%M:%S"`` to ISO 8601 format including the UTC offset, e.g. +``"2026-07-31 10:27:25+02:00"``. Snapshots can be parsed with +``datetime.datetime.fromisoformat``. diff --git a/docs/changes/newsfragments/8345.improved_driver b/docs/changes/newsfragments/8345.improved_driver new file mode 100644 index 000000000000..caf3a6092c90 --- /dev/null +++ b/docs/changes/newsfragments/8345.improved_driver @@ -0,0 +1,5 @@ +Fixed the timezone offset written into the waveform and sequence files +generated by the Tektronix AWG70000A drivers. The offset was computed manually +from ``time.timezone``, which gave the offset the wrong sign and ignored +daylight saving time, so e.g. a machine in UTC+02:00 wrote ``-02:00``. The +offset is now taken from the local timezone of the timestamp itself. diff --git a/docs/examples/DataSet/Linking to parent datasets.ipynb b/docs/examples/DataSet/Linking to parent datasets.ipynb index b6e2092b2c12..1a23976dcdda 100644 --- a/docs/examples/DataSet/Linking to parent datasets.ipynb +++ b/docs/examples/DataSet/Linking to parent datasets.ipynb @@ -65,7 +65,7 @@ } ], "source": [ - "now = str(datetime.datetime.now())\n", + "now = str(datetime.datetime.now(datetime.UTC))\n", "tutorial_db_path = Path.cwd().parent / \"example_output\" / \"linking_datasets_tutorial.db\"\n", "initialise_or_create_database_at(tutorial_db_path)\n", "load_or_create_experiment(\"tutorial \" + now, \"no sample\")" diff --git a/pyproject.toml b/pyproject.toml index 95c0ac1af8d9..0ff7239f30e9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -258,7 +258,7 @@ extend-select = [ # it may be worth fixing some or these in the future # PYI036 disable until https://github.com/astral-sh/ruff/issues/9794 is fixed ignore = ["E501", "G004", "PLR2004", "PLR0913", "PLR0911", "PLR0912", "PLR0915", "PLW0602", "PLW0603", "PLW2901", "PYI036", "PLR0917", - "DTZ007", "DTZ005", "DTZ006", "N999", "BLE001", "TRY004", "TRY002", "S110"] # new defaults of of 0.16.0 that we don't want to enable yet + "N999", "BLE001", "TRY004", "TRY002", "S110"] # new defaults of 0.16.0 that we don't want to enable yet # we want to explicitly use the micro symbol # not the greek letter diff --git a/src/qcodes/dataset/data_set_protocol.py b/src/qcodes/dataset/data_set_protocol.py index c12dbc3d4956..cd31082e8809 100644 --- a/src/qcodes/dataset/data_set_protocol.py +++ b/src/qcodes/dataset/data_set_protocol.py @@ -149,12 +149,12 @@ def exp_id(self) -> int: ... @property def sample_name(self) -> str: ... - def run_timestamp(self, fmt: str = "%Y-%m-%d %H:%M:%S") -> str | None: ... + def run_timestamp(self, fmt: str = "%Y-%m-%d %H:%M:%S%z") -> str | None: ... @property def run_timestamp_raw(self) -> float | None: ... - def completed_timestamp(self, fmt: str = "%Y-%m-%d %H:%M:%S") -> str | None: ... + def completed_timestamp(self, fmt: str = "%Y-%m-%d %H:%M:%S%z") -> str | None: ... @property def completed_timestamp_raw(self) -> float | None: ... @@ -505,7 +505,7 @@ def _reshape_array_for_cache( new_data = param_data.ravel() return new_data - def run_timestamp(self, fmt: str = "%Y-%m-%d %H:%M:%S") -> str | None: + def run_timestamp(self, fmt: str = "%Y-%m-%d %H:%M:%S%z") -> str | None: """ Returns run timestamp in a human-readable format @@ -513,18 +513,30 @@ def run_timestamp(self, fmt: str = "%Y-%m-%d %H:%M:%S") -> str | None: started. If the run has not yet been started, this function returns None. - Consult with :func:`time.strftime` for information about the format. + The timestamp is rendered in the local timezone of the machine reading + the dataset since the raw timestamp stored in the dataset is a POSIX + timestamp which does not capture the timezone of the measurement. The + default format therefore includes the UTC offset. + + Consult with :meth:`datetime.datetime.strftime` for information about + the format. """ return raw_time_to_str_time(self.run_timestamp_raw, fmt) - def completed_timestamp(self, fmt: str = "%Y-%m-%d %H:%M:%S") -> str | None: + def completed_timestamp(self, fmt: str = "%Y-%m-%d %H:%M:%S%z") -> str | None: """ Returns timestamp when measurement run was completed in a human-readable format If the run (or the dataset) is not completed, then returns None. - Consult with ``time.strftime`` for information about the format. + The timestamp is rendered in the local timezone of the machine reading + the dataset since the raw timestamp stored in the dataset is a POSIX + timestamp which does not capture the timezone of the measurement. The + default format therefore includes the UTC offset. + + Consult with :meth:`datetime.datetime.strftime` for information about + the format. """ return raw_time_to_str_time(self.completed_timestamp_raw, fmt) diff --git a/src/qcodes/dataset/sqlite/db_overview.py b/src/qcodes/dataset/sqlite/db_overview.py index 08150f018224..e596439f46aa 100644 --- a/src/qcodes/dataset/sqlite/db_overview.py +++ b/src/qcodes/dataset/sqlite/db_overview.py @@ -79,7 +79,7 @@ def _format_timestamp(ts: float | None) -> tuple[str, str]: if ts is None or ts == 0: return "", "" try: - dt = datetime.datetime.fromtimestamp(ts) + dt = datetime.datetime.fromtimestamp(ts, tz=datetime.UTC).astimezone() except (OSError, ValueError, OverflowError): return "", "" return dt.strftime("%Y-%m-%d"), dt.strftime("%H:%M:%S") diff --git a/src/qcodes/dataset/sqlite/queries.py b/src/qcodes/dataset/sqlite/queries.py index 07b6f5a77dd9..31292df97ebf 100644 --- a/src/qcodes/dataset/sqlite/queries.py +++ b/src/qcodes/dataset/sqlite/queries.py @@ -5,6 +5,7 @@ from __future__ import annotations +import datetime import logging import sqlite3 import time @@ -2288,12 +2289,33 @@ def get_raw_run_attributes( def raw_time_to_str_time( - raw_timestamp: float | None, fmt: str = "%Y-%m-%d %H:%M:%S" + raw_timestamp: float | None, fmt: str = "%Y-%m-%d %H:%M:%S%z" ) -> str | None: + """ + Convert a raw (POSIX) timestamp into a human readable string. + + The timestamp is rendered in the local timezone of the machine that reads + the dataset. Note that raw timestamps stored in the database are POSIX + timestamps and therefore do not capture the timezone in which the + measurement was performed. The default format therefore includes the UTC + offset of the local timezone to make the resulting string unambiguous. + + Args: + raw_timestamp: POSIX timestamp to convert or None. + fmt: Format to render the timestamp in. + Consult :meth:`datetime.datetime.strftime` for the format options. + + Returns: + The formatted timestamp or None if the raw timestamp was None. + + """ if raw_timestamp is None: return None else: - return time.strftime(fmt, time.localtime(raw_timestamp)) + local_time = datetime.datetime.fromtimestamp( + raw_timestamp, tz=datetime.UTC + ).astimezone() + return local_time.strftime(fmt) def _check_if_table_found(conn: AtomicConnection, table_name: str) -> bool: diff --git a/src/qcodes/instrument_drivers/Keysight/Infiniium.py b/src/qcodes/instrument_drivers/Keysight/Infiniium.py index 3c42542aa796..8fdb6ee41adb 100644 --- a/src/qcodes/instrument_drivers/Keysight/Infiniium.py +++ b/src/qcodes/instrument_drivers/Keysight/Infiniium.py @@ -1,5 +1,5 @@ import re -from datetime import datetime +from datetime import UTC, datetime from io import BytesIO from os.path import splitext from pathlib import Path @@ -1266,7 +1266,9 @@ def screenshot( if isinstance(path, Path): path = str(path) - time_str = datetime.now().strftime(time_fmt) if with_time else "" + time_str = ( + datetime.now(UTC).astimezone().strftime(time_fmt) if with_time else "" + ) img_name, img_type = splitext(path) img_path = ( f"{img_name}{divider if with_time else ''}{time_str}{img_type.lower()}" diff --git a/src/qcodes/instrument_drivers/tektronix/AWG70000A.py b/src/qcodes/instrument_drivers/tektronix/AWG70000A.py index 42f6483e0aac..15c5e4972c0b 100644 --- a/src/qcodes/instrument_drivers/tektronix/AWG70000A.py +++ b/src/qcodes/instrument_drivers/tektronix/AWG70000A.py @@ -923,17 +923,10 @@ def _makeWFMXFileHeader(num_samples: int, markers_included: bool) -> str: raise ValueError("num_samples must be at least 2400.") # form the timestamp string - timezone = time.timezone - tz_m, _ = divmod(timezone, 60) # returns (minutes, seconds) - tz_h, tz_m = divmod(tz_m, 60) - if np.sign(tz_h) == -1: - signstr = "-" - tz_h *= -1 - else: - signstr = "+" - timestr = dt.datetime.now().strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] - timestr += signstr - timestr += f"{tz_h:02.0f}:{tz_m:02.0f}" + now = dt.datetime.now(dt.UTC).astimezone() + tz_str = now.strftime("%z") # e.g. "+0100" + timestr = now.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + timestr += tz_str[:3] + ":" + tz_str[3:] # "+01:00" hdr = ET.Element( "DataFile", attrib={"offset": "0" * offsetdigits, "version": "0.1"} @@ -1485,17 +1478,10 @@ def _makeSMLFile( N = lstlens[0] # form the timestamp string - timezone = time.timezone - tz_m, _ = divmod(timezone, 60) - tz_h, tz_m = divmod(tz_m, 60) - if np.sign(tz_h) == -1: - signstr = "-" - tz_h *= -1 - else: - signstr = "+" - timestr = dt.datetime.now().strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] - timestr += signstr - timestr += f"{tz_h:02.0f}:{tz_m:02.0f}" + now = dt.datetime.now(dt.UTC).astimezone() + tz_str = now.strftime("%z") # e.g. "+0100" + timestr = now.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + timestr += tz_str[:3] + ":" + tz_str[3:] # "+01:00" datafile = ET.Element( "DataFile", attrib={"offset": "0" * offsetdigits, "version": "0.1"} diff --git a/src/qcodes/interactive_widget.py b/src/qcodes/interactive_widget.py index b1d085be482c..4138e7e12470 100644 --- a/src/qcodes/interactive_widget.py +++ b/src/qcodes/interactive_widget.py @@ -6,7 +6,7 @@ import io import operator import traceback -from datetime import datetime +from datetime import UTC, datetime from functools import partial, reduce from pathlib import Path from typing import TYPE_CHECKING, Any, Literal @@ -389,8 +389,8 @@ def _get_timestamp_button(ds: DataSetProtocol) -> Box: end_timestamp = ds.completed_timestamp_raw if start_timestamp is not None and end_timestamp is not None: total_time = str( - datetime.fromtimestamp(end_timestamp) - - datetime.fromtimestamp(start_timestamp) + datetime.fromtimestamp(end_timestamp, tz=UTC) + - datetime.fromtimestamp(start_timestamp, tz=UTC) ) else: total_time = "?" diff --git a/src/qcodes/logger/logger.py b/src/qcodes/logger/logger.py index 1b77143bf45c..83a0c0a1af2d 100644 --- a/src/qcodes/logger/logger.py +++ b/src/qcodes/logger/logger.py @@ -14,7 +14,7 @@ from collections import OrderedDict from contextlib import contextmanager from copy import copy -from datetime import datetime +from datetime import UTC, datetime from typing import TYPE_CHECKING, Any, Self if TYPE_CHECKING: @@ -163,7 +163,7 @@ def generate_log_file_name() -> str: """ pid = str(os.getpid()) - dt_str = datetime.now().strftime("%y%m%d") + dt_str = datetime.now(UTC).astimezone().strftime("%y%m%d") python_log_name = f"{dt_str}-{pid}-{PYTHON_LOG_NAME}" return python_log_name diff --git a/src/qcodes/parameters/cache.py b/src/qcodes/parameters/cache.py index 6a43badb127f..bbdea60fa534 100644 --- a/src/qcodes/parameters/cache.py +++ b/src/qcodes/parameters/cache.py @@ -1,6 +1,6 @@ from __future__ import annotations -from datetime import datetime, timedelta +from datetime import UTC, datetime, timedelta from typing import TYPE_CHECKING, Any, Generic, Literal, Protocol, overload from typing_extensions import TypeVar @@ -187,7 +187,7 @@ def _update_with( self._value = value self._raw_value = raw_value if timestamp is None: - self._timestamp = datetime.now() + self._timestamp = datetime.now(UTC).astimezone() else: self._timestamp = timestamp self._marked_valid = True @@ -199,7 +199,7 @@ def _timestamp_expired(self) -> bool: if self._max_val_age is None: # parameter cannot expire return False - oldest_accepted_timestamp = datetime.now() - timedelta( + oldest_accepted_timestamp = datetime.now(UTC).astimezone() - timedelta( seconds=self._max_val_age ) too_old = self._timestamp < oldest_accepted_timestamp diff --git a/src/qcodes/parameters/parameter_base.py b/src/qcodes/parameters/parameter_base.py index d8752d7ddc0c..8887f38b39e0 100644 --- a/src/qcodes/parameters/parameter_base.py +++ b/src/qcodes/parameters/parameter_base.py @@ -754,7 +754,7 @@ def snapshot_base( if isinstance(state["ts"], datetime): dttime: datetime = state["ts"] - state["ts"] = dttime.strftime("%Y-%m-%d %H:%M:%S") + state["ts"] = dttime.isoformat(sep=" ", timespec="seconds") for attr in set(self._meta_attrs): if attr == "instrument" and self._instrument is not None: diff --git a/tests/dataset/test_dataset_export.py b/tests/dataset/test_dataset_export.py index bf1036e348f3..bf7e21911689 100644 --- a/tests/dataset/test_dataset_export.py +++ b/tests/dataset/test_dataset_export.py @@ -1,5 +1,6 @@ from __future__ import annotations +import datetime import json import logging import os @@ -1440,6 +1441,115 @@ def _assert_xarray_metadata_is_as_expected( ) +def _assert_timestamp_str_matches_raw( + timestamp_str: object, raw_timestamp: float +) -> None: + """ + Assert that an exported timestamp string is timezone aware and describes the + same instant as the raw (POSIX) timestamp it was generated from. + """ + assert isinstance(timestamp_str, str) + # parsing with an explicit ``%z`` fails unless the string carries a UTC offset + parsed = datetime.datetime.strptime(timestamp_str, "%Y-%m-%d %H:%M:%S%z") + assert parsed.tzinfo is not None + expected_offset = ( + datetime.datetime.fromtimestamp(raw_timestamp, tz=datetime.UTC) + .astimezone() + .utcoffset() + ) + assert parsed.utcoffset() == expected_offset + # the exported string is truncated to whole seconds + assert 0 <= raw_timestamp - parsed.timestamp() < 1 + + +def test_export_to_xarray_timestamps_are_timezone_aware( + mock_dataset: DataSet, +) -> None: + run_timestamp_raw = mock_dataset.run_timestamp_raw + completed_timestamp_raw = mock_dataset.completed_timestamp_raw + assert run_timestamp_raw is not None + assert completed_timestamp_raw is not None + + xr_ds = mock_dataset.to_xarray_dataset() + + _assert_timestamp_str_matches_raw(xr_ds.attrs["run_timestamp"], run_timestamp_raw) + _assert_timestamp_str_matches_raw( + xr_ds.attrs["completed_timestamp"], completed_timestamp_raw + ) + assert xr_ds.attrs["run_timestamp_raw"] == run_timestamp_raw + assert xr_ds.attrs["completed_timestamp_raw"] == completed_timestamp_raw + + +def test_export_to_xarray_dataset_dict_timestamps_are_timezone_aware( + mock_dataset: DataSet, +) -> None: + run_timestamp_raw = mock_dataset.run_timestamp_raw + completed_timestamp_raw = mock_dataset.completed_timestamp_raw + assert run_timestamp_raw is not None + assert completed_timestamp_raw is not None + + xr_dss = mock_dataset.to_xarray_dataset_dict() + + for xr_ds in xr_dss.values(): + _assert_timestamp_str_matches_raw( + xr_ds.attrs["run_timestamp"], run_timestamp_raw + ) + _assert_timestamp_str_matches_raw( + xr_ds.attrs["completed_timestamp"], completed_timestamp_raw + ) + + +def test_export_netcdf_timestamps_are_timezone_aware( + tmp_path_factory: TempPathFactory, mock_dataset: DataSet +) -> None: + run_timestamp_raw = mock_dataset.run_timestamp_raw + completed_timestamp_raw = mock_dataset.completed_timestamp_raw + assert run_timestamp_raw is not None + assert completed_timestamp_raw is not None + + path = str(tmp_path_factory.mktemp("export_netcdf_timestamps")) + mock_dataset.export(export_type="netcdf", path=path, prefix="qcodes_") + file_path = os.path.join( + path, f"qcodes_{mock_dataset.captured_run_id}_{mock_dataset.guid}.nc" + ) + + loaded_ds = xr.open_dataset(file_path) + _assert_timestamp_str_matches_raw( + loaded_ds.attrs["run_timestamp"], run_timestamp_raw + ) + _assert_timestamp_str_matches_raw( + loaded_ds.attrs["completed_timestamp"], completed_timestamp_raw + ) + + reloaded_dataset = load_from_netcdf(file_path) + assert reloaded_dataset.run_timestamp_raw == run_timestamp_raw + assert reloaded_dataset.completed_timestamp_raw == completed_timestamp_raw + assert reloaded_dataset.run_timestamp() == mock_dataset.run_timestamp() + assert reloaded_dataset.completed_timestamp() == mock_dataset.completed_timestamp() + + +def test_export_to_xarray_timestamps_of_incomplete_dataset( + experiment: Experiment, +) -> None: + dataset = new_data_set("dataset") + xparam = ParamSpecBase("x", "numeric") + yparam = ParamSpecBase("y", "numeric") + idps = InterDependencies_(dependencies={yparam: (xparam,)}) + dataset.set_interdependencies(idps) + dataset.mark_started() + dataset.add_results([{"x": 0, "y": 1}]) + + run_timestamp_raw = dataset.run_timestamp_raw + assert run_timestamp_raw is not None + assert dataset.completed_timestamp_raw is None + + xr_ds = dataset.to_xarray_dataset() + + _assert_timestamp_str_matches_raw(xr_ds.attrs["run_timestamp"], run_timestamp_raw) + assert xr_ds.attrs["completed_timestamp"] == "" + assert xr_ds.attrs["completed_timestamp_raw"] == -1 + + def test_multi_index_options_grid(mock_dataset_grid: DataSet) -> None: assert mock_dataset_grid.description.shapes is None diff --git a/tests/dataset/test_dataset_loading.py b/tests/dataset/test_dataset_loading.py index 37a37970254b..ebdb732f2553 100644 --- a/tests/dataset/test_dataset_loading.py +++ b/tests/dataset/test_dataset_loading.py @@ -1,5 +1,6 @@ from __future__ import annotations +import datetime import time from math import floor @@ -175,7 +176,9 @@ def test_run_timestamp_with_default_format() -> None: run_ts = ds.run_timestamp() assert run_ts is not None # Note that here we also test the default format of `run_timestamp` - actual_run_timestamp_raw = time.mktime(time.strptime(run_ts, "%Y-%m-%d %H:%M:%S")) + actual_run_timestamp_raw = datetime.datetime.strptime( + run_ts, "%Y-%m-%d %H:%M:%S%z" + ).timestamp() # Note that because the default format precision is 1 second, we add this # second to the right side of the comparison @@ -231,9 +234,9 @@ def test_completed_timestamp_with_default_format() -> None: assert completed_ts is not None # Note that here we also test the default format of `completed_timestamp` - actual_completed_timestamp_raw = time.mktime( - time.strptime(completed_ts, "%Y-%m-%d %H:%M:%S") - ) + actual_completed_timestamp_raw = datetime.datetime.strptime( + completed_ts, "%Y-%m-%d %H:%M:%S%z" + ).timestamp() # Note that because the default format precision is 1 second, we add this # second to the right side of the comparison diff --git a/tests/dataset/test_links.py b/tests/dataset/test_links.py index 3db8d88e65df..e0b3571299f5 100644 --- a/tests/dataset/test_links.py +++ b/tests/dataset/test_links.py @@ -1,7 +1,7 @@ import json import random import re -from datetime import datetime, timedelta +from datetime import UTC, datetime, timedelta import hypothesis.strategies as hst import pytest @@ -27,7 +27,9 @@ def _timestamp() -> int: Return a random timestamp that is approximately one day in the past. """ - timestamp = datetime.now() - timedelta(days=1, seconds=random.randint(1, 1000)) + timestamp = datetime.now(UTC) - timedelta( + days=1, seconds=random.randint(1, 1000) + ) return round(timestamp.timestamp() * 1000) known_types = ("fit", "analysis", "step") diff --git a/tests/parameter/test_get_latest.py b/tests/parameter/test_get_latest.py index d2c6780afa54..856f7443933f 100644 --- a/tests/parameter/test_get_latest.py +++ b/tests/parameter/test_get_latest.py @@ -1,5 +1,5 @@ import time -from datetime import datetime, timedelta +from datetime import UTC, datetime, timedelta from typing import Any import pytest @@ -15,11 +15,11 @@ def test_get_latest() -> None: # Create a gettable parameter local_parameter = Parameter("test_param", set_cmd=None, get_cmd=None) - before_set = datetime.now() + before_set = datetime.now(UTC) time.sleep(sleep_delta) local_parameter.set(1) time.sleep(sleep_delta) - after_set = datetime.now() + after_set = datetime.now(UTC) # Check we return last set value, with the correct timestamp assert local_parameter.get_latest() == 1 @@ -67,7 +67,7 @@ def test_get_latest_unknown() -> None: local_parameter.cache._value = value # type: ignore[attr-defined] local_parameter.cache._raw_value = value # type: ignore[attr-defined] assert local_parameter.get_latest.get_timestamp() is None - before_get = datetime.now() + before_get = datetime.now(UTC) assert local_parameter._get_count == 0 assert local_parameter.get_latest() == value assert local_parameter._get_count == 1 @@ -89,7 +89,7 @@ def test_get_latest_known() -> None: value = 1 local_parameter = BetterGettableParam("test_param", set_cmd=None, get_cmd=None) # fake a parameter that has a value acquired 10 sec ago - start = datetime.now() + start = datetime.now(UTC) set_time = start - timedelta(seconds=10) local_parameter.cache._update_with(value=value, raw_value=value, timestamp=set_time) assert local_parameter._get_count == 0 @@ -128,7 +128,7 @@ def test_get_latest_no_get() -> None: def test_max_val_age() -> None: value = 1 - start = datetime.now() + start = datetime.now(UTC) local_parameter = BetterGettableParam( "test_param", set_cmd=None, max_val_age=1, initial_value=value ) diff --git a/tests/parameter/test_group_parameter.py b/tests/parameter/test_group_parameter.py index 6c580e036ce0..9464b031e1cc 100644 --- a/tests/parameter/test_group_parameter.py +++ b/tests/parameter/test_group_parameter.py @@ -1,5 +1,5 @@ import re -from datetime import datetime +from datetime import UTC, datetime from typing import TYPE_CHECKING, Any import pytest @@ -213,9 +213,9 @@ def test_update_group_parameter_reflected_in_cache_of_all_params() -> None: assert dummy.a.cache.get(get_if_invalid=False) is None assert dummy.b.cache.get(get_if_invalid=False) is None - before = datetime.now() + before = datetime.now(UTC) group.update() - after = datetime.now() + after = datetime.now(UTC) assert dummy.a.cache.timestamp is not None assert before <= dummy.a.cache.timestamp @@ -237,9 +237,9 @@ def test_get_group_param_updates_cache_of_other_param() -> None: assert dummy.a.cache.get(get_if_invalid=False) is None assert dummy.b.cache.get(get_if_invalid=False) is None - before = datetime.now() + before = datetime.now(UTC) assert dummy.a.get() == 0 - after = datetime.now() + after = datetime.now(UTC) assert dummy.a.cache.timestamp is not None assert before <= dummy.a.cache.timestamp @@ -261,9 +261,9 @@ def test_set_group_param_updates_cache_of_other_param() -> None: assert dummy.a.cache.get(get_if_invalid=False) is None assert dummy.b.cache.get(get_if_invalid=False) is None - before = datetime.now() + before = datetime.now(UTC) dummy.a.set(10) - after = datetime.now() + after = datetime.now(UTC) assert dummy.a.cache.timestamp is not None assert before <= dummy.a.cache.timestamp diff --git a/tests/parameter/test_parameter_cache.py b/tests/parameter/test_parameter_cache.py index 1768b7de72f5..ace9e1e5b4b5 100644 --- a/tests/parameter/test_parameter_cache.py +++ b/tests/parameter/test_parameter_cache.py @@ -1,7 +1,7 @@ from __future__ import annotations import time -from datetime import datetime, timedelta +from datetime import UTC, datetime, timedelta from typing import TYPE_CHECKING, Any, Literal import pytest @@ -53,11 +53,11 @@ def test_get_cache() -> None: # Create a gettable parameter local_parameter = Parameter("test_param", set_cmd=None, get_cmd=None) - before_set = datetime.now() + before_set = datetime.now(UTC) time.sleep(sleep_delta) local_parameter.set(1) time.sleep(sleep_delta) - after_set = datetime.now() + after_set = datetime.now(UTC) # Check we return last set value, with the correct timestamp assert local_parameter.cache.get() == 1 @@ -103,7 +103,7 @@ def test_get_cache_unknown() -> None: local_parameter.cache._value = value # type: ignore[attr-defined] local_parameter.cache._raw_value = value # type: ignore[attr-defined] assert local_parameter.cache.timestamp is None - before_get = datetime.now() + before_get = datetime.now(UTC) assert local_parameter._get_count == 0 assert local_parameter.cache.get() == value assert local_parameter._get_count == 1 @@ -124,7 +124,7 @@ def test_get_cache_known() -> None: value = 1 local_parameter = BetterGettableParam("test_param", set_cmd=None, get_cmd=None) # fake a parameter that has a value acquired 10 sec ago - start = datetime.now() + start = datetime.now(UTC) set_time = start - timedelta(seconds=10) local_parameter.cache._update_with(value=value, raw_value=value, timestamp=set_time) assert local_parameter._get_count == 0 @@ -165,9 +165,9 @@ def test_set_raw_value_on_cache() -> None: value = 1 scale = 10 local_parameter = BetterGettableParam("test_param", set_cmd=None, scale=scale) - before = datetime.now() + before = datetime.now(UTC) local_parameter.cache._set_from_raw_value(value * scale) - after = datetime.now() + after = datetime.now(UTC) assert local_parameter.cache.get(get_if_invalid=False) == value assert local_parameter.cache.raw_value == value * scale timestamp = local_parameter.cache.timestamp @@ -178,7 +178,7 @@ def test_set_raw_value_on_cache() -> None: def test_max_val_age() -> None: value = 1 - start = datetime.now() + start = datetime.now(UTC) local_parameter = BetterGettableParam( "test_param", set_cmd=None, max_val_age=1, initial_value=value ) @@ -232,7 +232,7 @@ def __init__(self, *args: Any, **kwargs: Any): self.set = self._wrap_set(self.set_raw) local_parameter = LocalParameter("test_param", instrument=None, max_val_age=1) - start = datetime.now() + start = datetime.now(UTC) set_time = start - timedelta(seconds=10) local_parameter.cache._update_with(value=value, raw_value=value, timestamp=set_time) diff --git a/tests/parameter/test_snapshot.py b/tests/parameter/test_snapshot.py index bd2dc32c3261..72ec0e37330a 100644 --- a/tests/parameter/test_snapshot.py +++ b/tests/parameter/test_snapshot.py @@ -1,6 +1,6 @@ from __future__ import annotations -from datetime import datetime, timedelta +from datetime import UTC, datetime, timedelta from typing import TYPE_CHECKING, Any, Literal, TypeVar from typing_extensions import ParamSpec @@ -137,7 +137,7 @@ def test_snapshot_timestamp_of_non_gettable_depends_only_on_cache_validity( if cache_is_valid: assert t0 is not None - ts = datetime.strptime(s["ts"], "%Y-%m-%d %H:%M:%S") + ts = datetime.fromisoformat(s["ts"]) t0_up_to_seconds = t0.replace(microsecond=0) assert ts >= t0_up_to_seconds else: @@ -159,7 +159,7 @@ def test_snapshot_timestamp_for_valid_cache_depends_on_cache_update( assert timestamp is not None p.cache._timestamp = timestamp - timedelta(days=31) # type: ignore[attr-defined] - tu = datetime.now() + tu = datetime.now(UTC) assert p.cache.timestamp is not None assert p.cache.timestamp < tu # pre-condition if update != NOT_PASSED: @@ -167,7 +167,7 @@ def test_snapshot_timestamp_for_valid_cache_depends_on_cache_update( else: s = p.snapshot() - ts = datetime.strptime(s["ts"], "%Y-%m-%d %H:%M:%S") + ts = datetime.fromisoformat(s["ts"]) tu_up_to_seconds = tu.replace(microsecond=0) cache_gets_updated_on_snapshot_call = ( @@ -197,7 +197,7 @@ def test_snapshot_timestamp_for_invalid_cache_depends_only_on_snapshot_flags( ) if cache_gets_updated_on_snapshot_call: - tu = datetime.now() + tu = datetime.now(UTC) else: tu = None @@ -207,7 +207,7 @@ def test_snapshot_timestamp_for_invalid_cache_depends_only_on_snapshot_flags( s = p.snapshot() if cache_gets_updated_on_snapshot_call: - ts = datetime.strptime(s["ts"], "%Y-%m-%d %H:%M:%S") + ts = datetime.fromisoformat(s["ts"]) assert tu is not None tu_up_to_seconds = tu.replace(microsecond=0) assert ts >= tu_up_to_seconds