From fc35a5b632299702ba4faac3891d5aa33a889af6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 24 Aug 2026 08:25:35 +0000 Subject: [PATCH 1/8] Render qcodes timestamps in local timezone; handle new %z format Co-authored-by: astafan8 <15662810+astafan8@users.noreply.github.com> --- plottr/data/qcodes_dataset.py | 18 ++++++++++++++---- test/pytest/test_qcodes_data.py | 21 +++++++++++++-------- 2 files changed, 27 insertions(+), 12 deletions(-) diff --git a/plottr/data/qcodes_dataset.py b/plottr/data/qcodes_dataset.py index 551aa160..7d57c632 100644 --- a/plottr/data/qcodes_dataset.py +++ b/plottr/data/qcodes_dataset.py @@ -47,19 +47,29 @@ def _get_names_of_standalone_parameters(paramspecs: List['ParamSpec'] def _split_timestamp(ts: Optional[str]) -> Tuple[str, str]: """Split a qcodes timestamp string into (date, time) components. - Uses datetime parsing instead of string slicing for robustness. + Uses datetime parsing instead of string slicing for robustness. This + handles both the legacy qcodes timestamp format (``"YYYY-MM-DD HH:MM:SS"``) + and the newer one that includes the UTC offset of the local timezone + (``"YYYY-MM-DD HH:MM:SS%z"``, e.g. ``"2026-07-31 10:27:25+0200"``). - :param ts: timestamp string as returned by ``ds.run_timestamp()`` - (typically ``"YYYY-MM-DD HH:MM:SS"``), or None. + Timezone-aware timestamps are converted to the local timezone of the + machine running plottr before formatting, so the rendered date and time + always reflect the local (PC) timezone. The UTC offset itself is dropped + from the returned components. + + :param ts: timestamp string as returned by ``ds.run_timestamp()``, or None. :returns: (date_str, time_str) or ('', '') if ts is None or unparsable. """ if ts is None: return '', '' try: dt = datetime.fromisoformat(ts) - return dt.strftime('%Y-%m-%d'), dt.strftime('%H:%M:%S') except (ValueError, TypeError): return '', '' + if dt.tzinfo is not None: + # Render in the local timezone of the machine running plottr. + dt = dt.astimezone() + return dt.strftime('%Y-%m-%d'), dt.strftime('%H:%M:%S') class IndependentParameterDict(TypedDict): diff --git a/test/pytest/test_qcodes_data.py b/test/pytest/test_qcodes_data.py index a188c0b6..c75348c9 100644 --- a/test/pytest/test_qcodes_data.py +++ b/test/pytest/test_qcodes_data.py @@ -13,7 +13,8 @@ get_ds_structure, get_ds_info, get_runs_from_db, - ds_to_datadict) + ds_to_datadict, + _split_timestamp) @pytest.fixture(scope='function') @@ -201,17 +202,21 @@ def test_get_ds_info(experiment): # timestamps are difficult to test for, so we will cheat here and # instead of hard-coding timestamps we will just get them from the dataset - # The same applies to the guid as it contains the timestamp - started_ts = dataset.run_timestamp() - completed_ts = dataset.completed_timestamp() + # The same applies to the guid as it contains the timestamp. + # We parse the qcodes timestamps the same way ``get_ds_info`` does, so that + # this test is robust to the qcodes timestamp format (in particular the + # newer format that appends the local UTC offset, e.g. + # "2026-07-31 10:27:25+0200"). + started_date, started_time = _split_timestamp(dataset.run_timestamp()) + completed_date, completed_time = _split_timestamp(dataset.completed_timestamp()) expected_ds_info = { 'experiment': '2d_softsweep', 'sample': 'no sample', - 'completed_date': completed_ts[:10], - 'completed_time': completed_ts[11:], - 'started_date': started_ts[:10], - 'started_time': started_ts[11:], + 'completed_date': completed_date, + 'completed_time': completed_time, + 'started_date': started_date, + 'started_time': started_time, 'name': 'results', 'structure': None, 'records': 0, From 079dfb04ae7671f9c77641fd000c97fd0bb4b7e5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 24 Aug 2026 08:26:17 +0000 Subject: [PATCH 2/8] Guard timestamp formatting/conversion with consistent error handling Co-authored-by: astafan8 <15662810+astafan8@users.noreply.github.com> --- plottr/data/qcodes_dataset.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/plottr/data/qcodes_dataset.py b/plottr/data/qcodes_dataset.py index 7d57c632..b05a0ae3 100644 --- a/plottr/data/qcodes_dataset.py +++ b/plottr/data/qcodes_dataset.py @@ -64,12 +64,12 @@ def _split_timestamp(ts: Optional[str]) -> Tuple[str, str]: return '', '' try: dt = datetime.fromisoformat(ts) - except (ValueError, TypeError): + if dt.tzinfo is not None: + # Render in the local timezone of the machine running plottr. + dt = dt.astimezone() + return dt.strftime('%Y-%m-%d'), dt.strftime('%H:%M:%S') + except (ValueError, TypeError, OSError, OverflowError): return '', '' - if dt.tzinfo is not None: - # Render in the local timezone of the machine running plottr. - dt = dt.astimezone() - return dt.strftime('%Y-%m-%d'), dt.strftime('%H:%M:%S') class IndependentParameterDict(TypedDict): From 94c97027f562359fa2be0f3b18f14f5244367e91 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 25 Aug 2026 06:12:13 +0000 Subject: [PATCH 3/8] Fix mypy CI crash on qcodes; test _split_timestamp independently Co-authored-by: astafan8 <15662810+astafan8@users.noreply.github.com> --- pyproject.toml | 11 +++++++ test/pytest/test_qcodes_data.py | 54 +++++++++++++++++++++++++++------ 2 files changed, 55 insertions(+), 10 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 9c8124b7..980ec61b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -117,6 +117,17 @@ module = [ ] ignore_missing_imports = true +# qcodes ships a py.typed marker, so mypy type-checks its source when following +# imports from plottr. mypy 2.3.1 crashes with an INTERNAL ERROR while analysing +# ``qcodes.dataset.data_set_protocol``. Skip following imports into qcodes so we +# do not type-check a third-party library (and work around the mypy crash). +[[tool.mypy.overrides]] +module = [ + "qcodes.*", +] +follow_imports = "skip" +follow_imports_for_stubs = true + [tool.versioningit] default-version = "0.0" diff --git a/test/pytest/test_qcodes_data.py b/test/pytest/test_qcodes_data.py index c75348c9..10930f0a 100644 --- a/test/pytest/test_qcodes_data.py +++ b/test/pytest/test_qcodes_data.py @@ -1,3 +1,5 @@ +import datetime + import numpy as np import pytest from packaging import version @@ -181,6 +183,39 @@ def test_get_ds_structure(experiment): assert structure == expected_structure +def test_split_timestamp_naive(): + # The legacy qcodes timestamp format has no timezone information and is + # returned as-is (split into date and time). Expected values are hard-coded + # so this does not depend on the helper's own logic. + assert _split_timestamp("2026-07-31 10:27:25") == ("2026-07-31", "10:27:25") + + +def test_split_timestamp_none_and_invalid(): + assert _split_timestamp(None) == ("", "") + assert _split_timestamp("") == ("", "") + assert _split_timestamp("not a timestamp") == ("", "") + + +def test_split_timestamp_timezone_aware_rendered_in_local_time(): + # The new qcodes timestamp format includes the UTC offset. Such timestamps + # must be converted to the local timezone of the machine before being split. + # We compute the expected local date/time independently from the helper: + # take a fixed absolute instant, convert it to local wall-clock time via + # ``datetime.fromtimestamp`` (a different code path than the helper), and + # verify the helper agrees for the same instant given in different offsets. + utc_instant = datetime.datetime(2026, 7, 31, 8, 27, 25, + tzinfo=datetime.timezone.utc) + local = datetime.datetime.fromtimestamp(utc_instant.timestamp()) + expected = (local.strftime("%Y-%m-%d"), local.strftime("%H:%M:%S")) + + # Same instant expressed as UTC (+00:00) and as +02:00. + assert _split_timestamp("2026-07-31 08:27:25+00:00") == expected + assert _split_timestamp("2026-07-31 10:27:25+02:00") == expected + # qcodes renders the offset without a colon (e.g. "+0000"); ensure that + # format is handled too. + assert _split_timestamp("2026-07-31 08:27:25+0000") == expected + + def test_get_ds_info(experiment): N = 5 @@ -203,20 +238,19 @@ def test_get_ds_info(experiment): # timestamps are difficult to test for, so we will cheat here and # instead of hard-coding timestamps we will just get them from the dataset # The same applies to the guid as it contains the timestamp. - # We parse the qcodes timestamps the same way ``get_ds_info`` does, so that - # this test is robust to the qcodes timestamp format (in particular the - # newer format that appends the local UTC offset, e.g. - # "2026-07-31 10:27:25+0200"). - started_date, started_time = _split_timestamp(dataset.run_timestamp()) - completed_date, completed_time = _split_timestamp(dataset.completed_timestamp()) + # To avoid testing ``get_ds_info`` against the very helper it uses + # (``_split_timestamp``), we derive the expected local date/time + # independently from the raw unix timestamps exposed by qcodes. + started = datetime.datetime.fromtimestamp(dataset.run_timestamp_raw) + completed = datetime.datetime.fromtimestamp(dataset.completed_timestamp_raw) expected_ds_info = { 'experiment': '2d_softsweep', 'sample': 'no sample', - 'completed_date': completed_date, - 'completed_time': completed_time, - 'started_date': started_date, - 'started_time': started_time, + 'completed_date': completed.strftime('%Y-%m-%d'), + 'completed_time': completed.strftime('%H:%M:%S'), + 'started_date': started.strftime('%Y-%m-%d'), + 'started_time': started.strftime('%H:%M:%S'), 'name': 'results', 'structure': None, 'records': 0, From ba9ab3df890da5fef521cd9c485746b5a353e081 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 25 Aug 2026 10:08:44 +0000 Subject: [PATCH 4/8] Add date-boundary test case for _split_timestamp Co-authored-by: astafan8 <15662810+astafan8@users.noreply.github.com> --- test/pytest/test_qcodes_data.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/test/pytest/test_qcodes_data.py b/test/pytest/test_qcodes_data.py index 10930f0a..3ffaec6e 100644 --- a/test/pytest/test_qcodes_data.py +++ b/test/pytest/test_qcodes_data.py @@ -216,6 +216,20 @@ def test_split_timestamp_timezone_aware_rendered_in_local_time(): assert _split_timestamp("2026-07-31 08:27:25+0000") == expected +def test_split_timestamp_timezone_aware_crosses_date_boundary(): + # Converting to local time can push the timestamp onto a different + # calendar date than the one written in the input string. Expected + # values are again derived independently via ``datetime.fromtimestamp``. + utc_instant = datetime.datetime(2026, 7, 31, 23, 30, 0, + tzinfo=datetime.timezone.utc) + local = datetime.datetime.fromtimestamp(utc_instant.timestamp()) + expected = (local.strftime("%Y-%m-%d"), local.strftime("%H:%M:%S")) + + # Same instant, expressed with a +02:00 offset, which shifts the + # calendar date in the input string to the next day. + assert _split_timestamp("2026-08-01 01:30:00+02:00") == expected + + def test_get_ds_info(experiment): N = 5 From f66ae69eb03d41a87b49c5f52f0a3289b19f61ac Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 25 Aug 2026 10:09:54 +0000 Subject: [PATCH 5/8] Pin local timezone in date-boundary test for determinism Co-authored-by: astafan8 <15662810+astafan8@users.noreply.github.com> --- test/pytest/test_qcodes_data.py | 33 ++++++++++++++++++++++----------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/test/pytest/test_qcodes_data.py b/test/pytest/test_qcodes_data.py index 3ffaec6e..d66c3654 100644 --- a/test/pytest/test_qcodes_data.py +++ b/test/pytest/test_qcodes_data.py @@ -1,4 +1,5 @@ import datetime +import time import numpy as np import pytest @@ -216,18 +217,28 @@ def test_split_timestamp_timezone_aware_rendered_in_local_time(): assert _split_timestamp("2026-07-31 08:27:25+0000") == expected -def test_split_timestamp_timezone_aware_crosses_date_boundary(): +def test_split_timestamp_timezone_aware_crosses_date_boundary(monkeypatch): # Converting to local time can push the timestamp onto a different - # calendar date than the one written in the input string. Expected - # values are again derived independently via ``datetime.fromtimestamp``. - utc_instant = datetime.datetime(2026, 7, 31, 23, 30, 0, - tzinfo=datetime.timezone.utc) - local = datetime.datetime.fromtimestamp(utc_instant.timestamp()) - expected = (local.strftime("%Y-%m-%d"), local.strftime("%H:%M:%S")) - - # Same instant, expressed with a +02:00 offset, which shifts the - # calendar date in the input string to the next day. - assert _split_timestamp("2026-08-01 01:30:00+02:00") == expected + # calendar date than the one written in the input string. The local + # timezone is pinned explicitly so the boundary crossing is guaranteed + # regardless of the host machine's own timezone. Expected values are + # again derived independently via ``datetime.fromtimestamp``. + if not hasattr(time, "tzset"): + pytest.skip("time.tzset is not available on this platform") + + monkeypatch.setenv("TZ", "Europe/Berlin") + time.tzset() + try: + utc_instant = datetime.datetime(2026, 7, 31, 23, 30, 0, + tzinfo=datetime.timezone.utc) + local = datetime.datetime.fromtimestamp(utc_instant.timestamp()) + expected = (local.strftime("%Y-%m-%d"), local.strftime("%H:%M:%S")) + + # Same instant, expressed with a +02:00 offset, which shifts the + # calendar date in the input string to the next day. + assert _split_timestamp("2026-08-01 01:30:00+02:00") == expected + finally: + time.tzset() def test_get_ds_info(experiment): From 75e31987b45c030190ec5b1a907bee003ed00f01 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 25 Aug 2026 12:03:45 +0000 Subject: [PATCH 6/8] Restore qcodes mypy type-checking; downgrade mypy to working version Co-authored-by: astafan8 <15662810+astafan8@users.noreply.github.com> --- pyproject.toml | 11 ----------- test_requirements.txt | 2 +- 2 files changed, 1 insertion(+), 12 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 980ec61b..9c8124b7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -117,17 +117,6 @@ module = [ ] ignore_missing_imports = true -# qcodes ships a py.typed marker, so mypy type-checks its source when following -# imports from plottr. mypy 2.3.1 crashes with an INTERNAL ERROR while analysing -# ``qcodes.dataset.data_set_protocol``. Skip following imports into qcodes so we -# do not type-check a third-party library (and work around the mypy crash). -[[tool.mypy.overrides]] -module = [ - "qcodes.*", -] -follow_imports = "skip" -follow_imports_for_stubs = true - [tool.versioningit] default-version = "0.0" diff --git a/test_requirements.txt b/test_requirements.txt index 4b58a050..fd992016 100644 --- a/test_requirements.txt +++ b/test_requirements.txt @@ -2,7 +2,7 @@ qcodes pytest pytest-qt hypothesis -mypy==2.3.1 +mypy==2.1.0 PySide6-stubs pandas-stubs watchdog \ No newline at end of file From c65e066e17ae58e10560bf5dc07346c7fe9f5fc2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 25 Aug 2026 12:06:57 +0000 Subject: [PATCH 7/8] Document why mypy is pinned to 2.1.0 in test_requirements.txt Co-authored-by: astafan8 <15662810+astafan8@users.noreply.github.com> --- test_requirements.txt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test_requirements.txt b/test_requirements.txt index fd992016..9c9eb11a 100644 --- a/test_requirements.txt +++ b/test_requirements.txt @@ -2,6 +2,10 @@ qcodes pytest pytest-qt hypothesis +# mypy >=2.2.0 crashes with an INTERNAL ERROR while following imports into +# qcodes' data_set_protocol.py (verified up to 2.3.1, the latest release as +# of writing). Pinned to 2.1.0, the newest version that doesn't crash; bump +# once upstream fixes this (see https://github.com/python/mypy/issues). mypy==2.1.0 PySide6-stubs pandas-stubs From 879f568921b8ce3e945667a1b27fac2276a9eb00 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 25 Aug 2026 14:07:05 +0000 Subject: [PATCH 8/8] Link upstream mypy issue #21741 in test_requirements.txt comment Co-authored-by: astafan8 <15662810+astafan8@users.noreply.github.com> --- test_requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test_requirements.txt b/test_requirements.txt index 9c9eb11a..0a59f9e4 100644 --- a/test_requirements.txt +++ b/test_requirements.txt @@ -5,7 +5,7 @@ hypothesis # mypy >=2.2.0 crashes with an INTERNAL ERROR while following imports into # qcodes' data_set_protocol.py (verified up to 2.3.1, the latest release as # of writing). Pinned to 2.1.0, the newest version that doesn't crash; bump -# once upstream fixes this (see https://github.com/python/mypy/issues). +# once upstream fixes this (see https://github.com/python/mypy/issues/21741). mypy==2.1.0 PySide6-stubs pandas-stubs