From ba86918e6b8b1d7cc75996729484295526a6904b Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Tue, 19 Dec 2023 18:01:52 -0500 Subject: [PATCH] fix: fix quantity edit (#257) * fix: fix quantity edit * fix test --- .../useq_widgets/_column_info.py | 21 +++++++++----- tests/test_useq_widgets.py | 28 +++++++++++++++---- 2 files changed, 36 insertions(+), 13 deletions(-) diff --git a/src/pymmcore_widgets/useq_widgets/_column_info.py b/src/pymmcore_widgets/useq_widgets/_column_info.py index 2435ca6bc..70fbd0d89 100644 --- a/src/pymmcore_widgets/useq_widgets/_column_info.py +++ b/src/pymmcore_widgets/useq_widgets/_column_info.py @@ -325,20 +325,25 @@ def text_to_quant(self, text: str | None) -> pint.Quantity | None: return None # pragma: no cover -pattern = r"(?:(?P\d+):)?(?:(?P\d+):)?(?P\d+)([.,](?P\d+))?" +time_pattern = re.compile( + r"^(?:(?P\d+):)?(?:(?P\d+):)?(?P\d+)?(?:[.,](?P\d+))?$" +) +# dateutil is a better way to parse time intervals... +# but it's an additional dependency for a tiny feature def parse_timedelta(time_str: str) -> timedelta: - match = re.match(pattern, time_str) + match = time_pattern.match(time_str) if not match: # pragma: no cover raise ValueError(f"Invalid time interval format: {time_str}") hours = int(match["hours"]) if match["hours"] else 0 minutes = int(match["min"]) if match["min"] else 0 - seconds = int(match["sec"]) - ms = int(match["ms"]) if match["ms"] else 0 - return timedelta(hours=hours, minutes=minutes, seconds=seconds, microseconds=ms) + seconds = int(match["sec"]) if match["sec"] else 0 + frac_sec = match["frac"] + frac_sec = float(f"0.{frac_sec}") if frac_sec else 0 + return timedelta(hours=hours, minutes=minutes, seconds=seconds + frac_sec) class QQuantityLineEdit(QLineEdit): @@ -405,8 +410,10 @@ def quantity(self) -> pint.Quantity | int | float: class QTimeLineEdit(QQuantityLineEdit): - def __init__(self, parent: QWidget | None = None) -> None: - super().__init__(None, parent) + def __init__( + self, contents: str | None = None, parent: QWidget | None = None + ) -> None: + super().__init__(contents, parent) self.setDimensionality("second") self.setStyleSheet("QLineEdit { border: none; }") diff --git a/tests/test_useq_widgets.py b/tests/test_useq_widgets.py index 800720826..fa84eb855 100644 --- a/tests/test_useq_widgets.py +++ b/tests/test_useq_widgets.py @@ -1,6 +1,7 @@ from __future__ import annotations import enum +from datetime import timedelta from typing import TYPE_CHECKING import pint @@ -22,8 +23,9 @@ ) from pymmcore_widgets.useq_widgets._column_info import ( FloatColumn, - QQuantityLineEdit, + QTimeLineEdit, TextColumn, + parse_timedelta, ) from pymmcore_widgets.useq_widgets._positions import MDAButton, QFileDialog, _MDAPopup @@ -132,7 +134,7 @@ def test_mda_wdg(qtbot: QtBot): @pytest.mark.parametrize("ext", ["json", "yaml", "foo"]) def test_mda_wdg_load_save( qtbot: QtBot, tmp_path: Path, monkeypatch: pytest.MonkeyPatch, ext: str -): +) -> None: from pymmcore_widgets.useq_widgets._mda_sequence import QFileDialog wdg = MDASequenceWidget() @@ -166,8 +168,8 @@ def test_mda_wdg_load_save( assert dest.read_text() == mda_no_meta.yaml(exclude_defaults=True) -def test_qquant_line_edit(qtbot: QtBot): - wdg = QQuantityLineEdit("1 s") +def test_qquant_line_edit(qtbot: QtBot) -> None: + wdg = QTimeLineEdit("1.0 s") wdg.show() qtbot.addWidget(wdg) wdg.setUreg(pint.UnitRegistry()) @@ -411,7 +413,7 @@ def test_grid_plan_widget(qtbot: QtBot) -> None: assert wdg.fovWidth() == 6 -def test_proper_checked_index(qtbot): +def test_proper_checked_index(qtbot) -> None: """Testing that the proper tab is checked when setting a value https://github.com/pymmcore-plus/pymmcore-widgets/issues/205 @@ -436,7 +438,7 @@ def test_proper_checked_index(qtbot): ) -def test_mda_wdg_with_autofocus(qtbot: QtBot): +def test_mda_wdg_with_autofocus(qtbot: QtBot) -> None: wdg = MDASequenceWidget() qtbot.addWidget(wdg) wdg.show() @@ -463,3 +465,17 @@ def test_mda_wdg_with_autofocus(qtbot: QtBot): assert wdg.value().autofocus_plan assert not wdg.value().stage_positions[0].sequence assert not wdg.value().stage_positions[1].sequence + + +def test_parse_time() -> None: + assert parse_timedelta("2") == timedelta(seconds=2) + assert parse_timedelta("0.5") == timedelta(seconds=0.5) + assert parse_timedelta("0.500") == timedelta(seconds=0.5) + assert parse_timedelta("0.75") == timedelta(seconds=0.75) + # this syntax still fails... it assumes the 2 is hours, and the 30 is seconds... + # assert parse_timedelta("2:30") == timedelta(minutes=2, seconds=30) + assert parse_timedelta("1:20:15") == timedelta(hours=1, minutes=20, seconds=15) + assert parse_timedelta("0:00:00.500000") == timedelta(seconds=0.5) + assert parse_timedelta("3:40:10.500") == timedelta( + hours=3, minutes=40, seconds=10.5 + )