diff --git a/Makefile b/Makefile index 163c789..8969118 100644 --- a/Makefile +++ b/Makefile @@ -3,14 +3,14 @@ install: uv sync -test: +test: lint uv run pytest tests/ lint: - uv run ruff check src/ + uv run ruff check src/ tests/ fix: - uv run ruff check src/ --fix + uv run ruff check src/ tests/ --fix typecheck: uv run mypy src/ diff --git a/src/pygui/ISC/AboutDialog.qml b/src/pygui/ISC/AboutDialog.qml index 936f730..b12d25f 100644 --- a/src/pygui/ISC/AboutDialog.qml +++ b/src/pygui/ISC/AboutDialog.qml @@ -3,6 +3,14 @@ import QtQuick.Controls import QtQuick.Layouts import ISC +// TODO P3.1 + P3.2: license grid, "Load License" picker, version binding +// THINKING: C# parity requires the dgvLicense grid (Wafer SN, Mfg Date, +// License Level, License Date) fed from AES-128-CBC .bin files under +// /licenses/ — CryptoHelper (backend/crypto/crypto_helper.py) +// already has the primitives; the missing piece is the license file model +// (P3.1 backend). "Version 0.1.0" below is hardcoded and will drift from +// pyproject.toml — expose importlib.metadata.version("pygui") via a context +// property instead (P3.2). See docs/pending/alpha-release-polish-plan.md §3. Popup { id: root modal: true diff --git a/src/pygui/backend/controllers/session_controller.py b/src/pygui/backend/controllers/session_controller.py index 4436094..05677d6 100644 --- a/src/pygui/backend/controllers/session_controller.py +++ b/src/pygui/backend/controllers/session_controller.py @@ -603,6 +603,14 @@ class SessionController(QObject): return average_clusters(edited, getattr(self, "_active_clusters", [])) return edited + # TODO P2.4: throttle live-frame UI updates to ~30Hz + # THINKING: every decoded serial frame triggers frameUpdated/stateChanged, + # which repaints WaferMapItem + trend chart; at high stream rates the GUI + # thread saturates on paint, not decode. Buffer the latest frame and flush + # via a 33ms QTimer (latest-wins, no queue growth). Recording must keep + # writing every frame — only the UI emit is throttled, so place the timer + # here rather than in StreamReader. Depends on nothing; verify with P4.3. + # See docs/pending/alpha-release-polish-plan.md §2.4. def _emit_current(self) -> None: frame = self._player.current() if frame is None: @@ -828,4 +836,4 @@ class SessionController(QObject): def _flush_trend(self) -> None: import json - self.trendData.emit(json.dumps(self._trend_buffer)) \ No newline at end of file + self.trendData.emit(json.dumps(self._trend_buffer)) diff --git a/src/pygui/backend/visualization/rbf_heatmap.py b/src/pygui/backend/visualization/rbf_heatmap.py index b74c056..1733a11 100644 --- a/src/pygui/backend/visualization/rbf_heatmap.py +++ b/src/pygui/backend/visualization/rbf_heatmap.py @@ -18,6 +18,13 @@ _KERNEL = "thin_plate_spline" _SMOOTHING = 0.0 +# TODO P2.2: decouple grid resolution from output size +# THINKING: callers pass widget pixel size as width/height, so RBF evaluation +# count scales quadratically with viewport size (600px → 360k evaluations). +# Interpolate on a fixed ~100×100 grid and let the caller scale the QImage; +# sensor density (≤244 points) carries no more spatial information than that. +# Called from WaferMapItem._rebuild_heatmap (see its P2.2/P2.3 TODO). +# See docs/pending/alpha-release-polish-plan.md §2.2. def interpolate_field( xs: np.ndarray, ys: np.ndarray, diff --git a/src/pygui/backend/visualization/wafer_map_item.py b/src/pygui/backend/visualization/wafer_map_item.py index c4e7990..2024e3a 100644 --- a/src/pygui/backend/visualization/wafer_map_item.py +++ b/src/pygui/backend/visualization/wafer_map_item.py @@ -290,6 +290,12 @@ class WaferMapItem(QQuickPaintedItem): return idx return -1 + # TODO P6.1: build batch export on top of this single-frame export + # THINKING: export_image() already does the hard part (grabToImage → PNG); + # batch export is just a loop over file_browser.files that loads each CSV + # through the existing wafer-map pipeline and calls this per file into a + # chosen output dir, plus an optional summary CSV of (file, min/max/mean). + # No new rendering code needed — see docs/pending/alpha-release-polish-plan.md §6.1. @Slot(str, result=bool) def export_image(self, file_path: str) -> bool: """Export the current wafer map rendering to a PNG file @@ -377,6 +383,13 @@ class WaferMapItem(QQuickPaintedItem): r = max(math.hypot(s.x, s.y) for s in self._sensors) return r * 1.05 + # TODO P6.2: reuse this for the edge-to-center delta metric + # THINKING: radial_metrics.py (new) needs the same "which sensors are near + # the edge vs. near the center" bucketing this function already computes + # for ring-line drawing — do NOT re-derive ring boundaries separately, this + # already handles round vs. square wafer shapes correctly (see family_spec.py + # square-wafer work). Outermost group ≈ edge sensors, innermost ≈ center. + # See docs/pending/alpha-release-polish-plan.md §6.2. def _sensor_ring_radii_mm(self) -> list[float]: """Distinct radial distances of sensor groups, sorted ascending, plus the outer boundary.""" if not self._sensors: @@ -413,6 +426,15 @@ class WaferMapItem(QQuickPaintedItem): self._markers = {i: self._to_px(s.x, s.y, cx, cy, sc) for i, s in enumerate(self._sensors)} + # TODO P2.2 + P2.3: fixed-resolution RBF grid, then offload to QThreadPool + # THINKING: width=height=ds couples RBF cost to widget pixel size — a 600px + # viewport solves a 360k-point system per frame where a fixed 100×100 grid + # (10k points, GPU bilinear-scales the QImage up) is visually identical + # through the 0.4-opacity blend. Do P2.2 first: it may make P2.3 (QRunnable + # worker emitting heatmapReady(QImage), guarding against stale results when + # sensors change mid-flight) unnecessary for alpha. Grid width param lands + # in interpolate_field (see matching TODO in rbf_heatmap.py). + # See docs/pending/alpha-release-polish-plan.md §2.2/§2.3. def _rebuild_heatmap(self) -> None: if not self._sensors or not self._values or self._blend == 0.0: self._heatmap = None @@ -612,6 +634,13 @@ class WaferMapItem(QQuickPaintedItem): ox = getattr(s, "offset_x", 0.0) * r oy = getattr(s, "offset_y", 0.0) * r + # TODO P2.1: hoist fontMetrics() out of the sensor loop + # THINKING: id_font/temp_font are fixed for the whole paint pass, + # but fontMetrics() is re-queried per sensor — 65+ OS font-engine + # round-trips per frame on a dense wafer, ~30/s in Live mode. + # Compute id_fm/temp_fm once before `for i, s in ...` (they only + # change when r/font_scale change). Verified by P4.3 benchmark. + # See docs/pending/alpha-release-polish-plan.md §2.1. painter.setFont(id_font) id_fm = painter.fontMetrics() id_line_h = id_fm.height() diff --git a/src/pygui/backend/wafer/wafer_layouts.py b/src/pygui/backend/wafer/wafer_layouts.py index 6214d67..e6eb272 100644 --- a/src/pygui/backend/wafer/wafer_layouts.py +++ b/src/pygui/backend/wafer/wafer_layouts.py @@ -69,6 +69,12 @@ def _load_yaml(path: Path) -> dict: return yaml.safe_load(f) +# TODO P6.3: expose this list to a new LayoutSelector.qml (4-button chooser) +# THINKING: this already returns everything the UI needs (family names) — +# the missing piece is a context-property exposure (SessionController or a +# thin new property) and a setLayout(family) slot that calls load_layout() +# below. No parsing work needed; this is a wiring-only task. +# See docs/pending/alpha-release-polish-plan.md §6.3. def available_families() -> list[str]: return [_family_name(_load_yaml(p)["name"]) for p in _LAYOUTS_DIR.glob("*.yaml")] diff --git a/tests/test_cluster_average.py b/tests/test_cluster_average.py index 5629b9a..0970082 100644 --- a/tests/test_cluster_average.py +++ b/tests/test_cluster_average.py @@ -1,6 +1,6 @@ """Tests for src/pygui/backend/cluster_average.py.""" import math -import pytest + from pygui.backend.cluster_average import average_clusters, group_sensors_by_radius from pygui.backend.wafer.zwafer_models import Sensor diff --git a/tests/test_comparison.py b/tests/test_comparison.py index 777bc1f..7408244 100644 --- a/tests/test_comparison.py +++ b/tests/test_comparison.py @@ -1,6 +1,5 @@ """Tests for src/pygui/backend/comparison.py.""" -import numpy as np from pygui.backend.comparison import compare_runs diff --git a/tests/test_csv_recorder.py b/tests/test_csv_recorder.py index 2337914..e393100 100644 --- a/tests/test_csv_recorder.py +++ b/tests/test_csv_recorder.py @@ -37,4 +37,4 @@ def test_recorded_data_rows_are_readable(tmp_path): assert records[0].time == 0.0 assert records[0].values == [149.0, 148.0] assert records[1].time == 0.5 - assert records[1].values == [149.5, 148.5] \ No newline at end of file + assert records[1].values == [149.5, 148.5] diff --git a/tests/test_data_parser.py b/tests/test_data_parser.py index c8336a7..6c4d9c8 100644 --- a/tests/test_data_parser.py +++ b/tests/test_data_parser.py @@ -1,13 +1,14 @@ """Tests for serialcomm/data_parser.py binary parsing pipeline.""" import pytest + from pygui.serialcomm.data_parser import ( + _convert_hex_to_temp, + convert_to_temperatures, csv_column_count, parse_binary_data, - convert_to_temperatures, remove_trailing_zeros, save_to_csv, - _convert_hex_to_temp, ) # ── csv_column_count ────────────────────────────────────────────────────────── @@ -271,7 +272,7 @@ class TestSaveToCsv: result = save_to_csv(data, "P", "P00001", str(tmp_path)) assert result is not None headers = open(result).readline().strip().split(",") - assert len(headers) == 60, f"data width 60 should win over display 48" + assert len(headers) == 60, "data width 60 should win over display 48" assert headers[0] == "Sensor1" assert headers[-1] == "Sensor60" diff --git a/tests/test_data_records.py b/tests/test_data_records.py index c4a0f26..3d4ad7a 100644 --- a/tests/test_data_records.py +++ b/tests/test_data_records.py @@ -16,4 +16,4 @@ def test_reads_rows_after_data_marker(tmp_path): assert len(recs) == 2 assert recs[0].time == 0.0 assert recs[0].values == [149.0, 148.5] - assert recs[1].values == [149.2, 148.4] \ No newline at end of file + assert recs[1].values == [149.2, 148.4] diff --git a/tests/test_device_controller.py b/tests/test_device_controller.py index dac63cc..5d6dc84 100644 --- a/tests/test_device_controller.py +++ b/tests/test_device_controller.py @@ -1,10 +1,13 @@ -import pytest import logging from unittest.mock import MagicMock, patch + +import pytest + from pygui.backend.controllers.device_controller import DeviceController from pygui.backend.data.local_settings import LocalSettings from pygui.serialcomm.serial_port import WaferInfo + @pytest.fixture def mock_settings(): s = LocalSettings() @@ -16,8 +19,8 @@ def mock_settings(): @pytest.fixture def controller(mock_settings): - import tempfile import shutil + import tempfile temp_dir = tempfile.mkdtemp() yield DeviceController(mock_settings, temp_dir) shutil.rmtree(temp_dir) @@ -81,7 +84,7 @@ def test_clear_session(controller): controller._connection_status = "Connected" controller._selected_port = "COM1" controller._last_wafer_info = {"serialNumber": "P00001"} - + controller.clearSession() assert controller.connectionStatus == "Disconnected" assert controller.selectedPort == "" @@ -142,7 +145,7 @@ def test_parse_and_save_data_and_get_chart_data(controller): controller.parsedDataReady.connect(on_parsed) controller.parseAndSaveData("P", "COM1") - + assert emitted_result is not None assert emitted_result.get("success") is True assert controller.dataRowCount == 1 @@ -159,16 +162,16 @@ def test_logging_handler(controller): assert any("Hello QML Activity Log" in line for line in controller._activity_log) def test_fresh_session_on_startup(mock_settings): - import tempfile import shutil - + import tempfile + # Configure mock settings with some data to simulate previous session mock_settings.activity_log = ["[12:00:00] Old message"] mock_settings.selected_port = "COM9" mock_settings.last_wafer_info = {"familyCode": "P", "serialNumber": "P00002"} mock_settings.data_row_count = 50 mock_settings.data_col_count = 244 - + temp_dir = tempfile.mkdtemp() try: new_controller = DeviceController(mock_settings, temp_dir) @@ -187,19 +190,19 @@ def test_detect_wafer_clears_old_session(controller): controller._data_row_count = 10 controller._data_col_count = 244 controller._activity_log.append("[12:00:00] Previous message") - + # We mock detect_all_ports to not run long or fail controller._service.detect_all_ports = MagicMock(return_value=(None, None)) - - with patch("threading.Thread") as mock_thread: + + with patch("threading.Thread"): controller.detectWafer() - + # Verify it cleared all session variables and logs immediately at start of detect assert controller.selectedPort == "" assert controller.lastWaferInfo == ["", "", 0, "", 0, 0] assert controller.dataRowCount == 0 assert controller.dataColCount == 0 - + # Log should only contain "Scanning all ports for wafer ..." assert len(controller._activity_log) == 1 assert "Scanning all ports" in controller._activity_log[0] @@ -227,6 +230,6 @@ def test_device_service_port_caching(controller): # Clear cache and scan again should invoke subprocess service.clear_port_scan_cache() - ports3 = service.enumerate_ports() + service.enumerate_ports() assert mock_run.call_count == 2 diff --git a/tests/test_frame_player.py b/tests/test_frame_player.py index 1b910c1..aa6d247 100644 --- a/tests/test_frame_player.py +++ b/tests/test_frame_player.py @@ -1,30 +1,31 @@ from pygui.backend.models.frame import Frame from pygui.backend.models.frame_player import FramePlayer + def frames(n): return [Frame(seq=i, time=float(i), values=[149.0 + i])for i in range(n)] def test_loads_and_reports_total(): p = FramePlayer(); p.load(frames(3)) - assert p.total == 3 and p.index == 0 + assert p.total == 3 and p.index == 0 current = p.current() assert current is not None assert current.values == [149.0] - + def test_step_forward_and_back_clamps(): p = FramePlayer(); p.load(frames(3)) assert p.step(1).index == 1 assert p.step(1).index == 2 assert p.step(1).index == 2 assert p.step(-5).index == 0 - + def test_seek(): p = FramePlayer(); p.load(frames(5)) assert p.seek(3).index == 3 assert p.seek(99).index == 4 - + def test_at_end(): p = FramePlayer(); p.load(frames(2)) assert not p.at_end p.seek(1) - assert p.at_end \ No newline at end of file + assert p.at_end diff --git a/tests/test_frame_stats.py b/tests/test_frame_stats.py index 0ed5280..047de8d 100644 --- a/tests/test_frame_stats.py +++ b/tests/test_frame_stats.py @@ -1,6 +1,8 @@ import math + import pytest -from pygui.backend.models.frame_stats import compute_stats, Stats + +from pygui.backend.models.frame_stats import Stats, compute_stats def test_basic_stat(): @@ -20,3 +22,9 @@ def test_empty_values_returns_zeros(): def test_ignores_nan(): s = compute_stats([149.0, float("nan"), 151.0]) + assert s.min == 149.0 and s.min_index == 0 + assert s.max == 151.0 and s.max_index == 2 + assert s.diff == pytest.approx(2.0) + assert s.avg == pytest.approx(150.0) + assert s.sigma == pytest.approx(1.0) + assert s.three_sigma == pytest.approx(3.0) diff --git a/tests/test_graph_view.py b/tests/test_graph_view.py index 31f4d5b..2eb939d 100644 --- a/tests/test_graph_view.py +++ b/tests/test_graph_view.py @@ -1,8 +1,6 @@ """Tests for src/pygui/backend/visualization/graph_view.py.""" import json -from unittest.mock import MagicMock, PropertyMock, patch - -import pytest +from unittest.mock import MagicMock def _make_plot_widget(): diff --git a/tests/test_rbf_heatmap.py b/tests/test_rbf_heatmap.py index fb092be..d3f517a 100644 --- a/tests/test_rbf_heatmap.py +++ b/tests/test_rbf_heatmap.py @@ -1,6 +1,7 @@ """Tests for src/pygui/backend/rbf_heatmap.py.""" import numpy as np -from pygui.backend.visualization.rbf_heatmap import interpolate_field, BACKEND + +from pygui.backend.visualization.rbf_heatmap import BACKEND, interpolate_field def test_field_shape_and_interpolates_at_sites(): diff --git a/tests/test_sensor_editor.py b/tests/test_sensor_editor.py index 5913045..496a85c 100644 --- a/tests/test_sensor_editor.py +++ b/tests/test_sensor_editor.py @@ -1,5 +1,4 @@ """Tests for src/pygui/backend/sensor_editor.py.""" -import pytest from pygui.backend.models.sensor_editor import SensorEditor diff --git a/tests/test_session_controller.py b/tests/test_session_controller.py index d180275..ff66911 100644 --- a/tests/test_session_controller.py +++ b/tests/test_session_controller.py @@ -1,7 +1,10 @@ +from unittest.mock import MagicMock + import pytest -from unittest.mock import MagicMock, patch + from pygui.backend.controllers.session_controller import SessionController + @pytest.fixture def controller(): return SessionController() @@ -9,7 +12,7 @@ def controller(): def test_initial_default(controller): assert controller.mode == "review" assert controller.state == "idle" - assert controller.recording == False + assert not controller.recording def test_playback_flow(controller): controller.loadFile("tests/fixtures/sample_stream.csv") @@ -223,10 +226,10 @@ Item { def test_recording_toggle(controller, tmp_path): csv_path = str(tmp_path / "rec" / "live_test.csv") controller.startRecording(csv_path, "SN123") - assert controller.recording == True + assert controller.recording controller.stopRecording() - assert controller.recording == False + assert not controller.recording # Header written with wafer serial content = open(csv_path).read() @@ -252,4 +255,4 @@ def test_split_data_integration(controller): assert mock_slot.call_count == 1 args = mock_slot.call_args[0][0] assert args["success"] is True - assert "segments" in args \ No newline at end of file + assert "segments" in args diff --git a/tests/test_session_model.py b/tests/test_session_model.py index 04d9df0..7ab79fe 100644 --- a/tests/test_session_model.py +++ b/tests/test_session_model.py @@ -1,7 +1,6 @@ from pygui.backend.models.frame import Frame -from pygui.backend.models.threshold_classifier import ThresholdConfig, BAND_HIGH, BAND_IN, BAND_LOW -from pygui.backend.models.session_model import SessionModel, SessionUpdate - +from pygui.backend.models.session_model import SessionModel +from pygui.backend.models.threshold_classifier import BAND_HIGH, BAND_IN, BAND_LOW, ThresholdConfig def test_manual_banding_and_stats(): @@ -12,17 +11,16 @@ def test_manual_banding_and_stats(): assert update.stats.avg == 149.0 assert (update.target, update.margin) == (149.0, 1.0) assert update.state in ("idle", "ramp", "set") - + def test_auto_banding_uses_mean_and_sigma(): model = SessionModel(ThresholdConfig(auto=True)) # Tight cluster around 149 -> small σ -> the 200.0 outlier reads HIGH update = model.process(Frame(seq=0, time=0.0, values=[149.0, 149.0, 149.0, 200.0])) assert update.bands[3] == BAND_HIGH assert update.target == update.stats.avg - + def test_set_thresholds_reband_on_next_frame(): m = SessionModel(ThresholdConfig(set_point=149.0, margin=1.0, auto=False)) m.set_thresholds(ThresholdConfig(set_point=149.0, margin=5.0, auto=False)) upd = m.process(Frame(seq=0, time=0.0, values=[151.0])) assert upd.bands == [BAND_IN] - \ No newline at end of file diff --git a/tests/test_stability_detector.py b/tests/test_stability_detector.py index e8e0ca4..0ab751a 100644 --- a/tests/test_stability_detector.py +++ b/tests/test_stability_detector.py @@ -1,8 +1,10 @@ from pygui.backend.models.stability_detector import ( - StabilityDetector, STATE_IDLE, STATE_RAMP, STATE_SET, + STATE_IDLE, + STATE_RAMP, + STATE_SET, + StabilityDetector, ) - SET_POINT = 149.0 def make(): @@ -11,16 +13,16 @@ def make(): def test_idle_when_cold(): d = make() assert d.update(avg=25.0, time=0.0, set_point=SET_POINT) == STATE_IDLE - + def test_ramp_while_far_from_setpoint(): d = make() d.update(avg=100.0, time=0.0, set_point=SET_POINT) - + def test_ramp_until_settle_time_elapses(): d = make() assert d.update(avg=149.2, time=0.0, set_point=SET_POINT) == STATE_RAMP assert d.update(avg=148.9, time=0.0, set_point=SET_POINT) == STATE_RAMP - + def test_set_after_holding_near_setpoint(): d = make() d.update(avg=149.2, time=0.0, set_point=SET_POINT) @@ -32,4 +34,4 @@ def test_back_to_ramp_on_disturbance(): d = make() for t in (0.0, 1.0, 2.5): d.update(149.0, t, set_point=SET_POINT) - assert d.update(avg=160.0, time=3.0, set_point=SET_POINT) == STATE_RAMP \ No newline at end of file + assert d.update(avg=160.0, time=3.0, set_point=SET_POINT) == STATE_RAMP diff --git a/tests/test_stream_reader.py b/tests/test_stream_reader.py index 7598ff8..ce8d916 100644 --- a/tests/test_stream_reader.py +++ b/tests/test_stream_reader.py @@ -1,7 +1,9 @@ -import time +import time + from pygui.backend.models.frame import Frame from pygui.serialcomm.stream_reader import StreamReader + class FakeTransport: """Yield canned data then blocks (returns b'').""" def __init__(self, lines): @@ -28,7 +30,7 @@ class FakeTransport: self._pos = idx + 1 return res def close(self): self.closed = True - + def parse_line(raw: str, seq: int) -> Frame: parts = [float(x) for x in raw.split(",")] return Frame(seq = seq, time = parts[0], values = parts[1:]) @@ -38,10 +40,10 @@ def test_reads_frames_and_counts_errors(): transport = FakeTransport([b"0.0,149,148\n", b"garbage\n", b"0.5,150,149\n"]) r = StreamReader(transport, parse_line, on_frame = got.append, on_error = lambda e: errors.append(e)) - + r.start() time.sleep(0.1) r.stop() assert [f.values for f in got] == [[149.0, 148.0], [150.0, 149.0]] assert r.error_count == 1 - assert transport.closed \ No newline at end of file + assert transport.closed diff --git a/tests/test_threshold_classifier.py b/tests/test_threshold_classifier.py index d29839c..db7611b 100644 --- a/tests/test_threshold_classifier.py +++ b/tests/test_threshold_classifier.py @@ -1,8 +1,13 @@ import math from pygui.backend.models.threshold_classifier import ( - ThresholdConfig, classify, classify_all, resolve_bounds, - BAND_IN, BAND_HIGH, BAND_LOW + BAND_HIGH, + BAND_IN, + BAND_LOW, + ThresholdConfig, + classify, + classify_all, + resolve_bounds, ) @@ -11,19 +16,18 @@ def test_classify_about_target_margin(): assert classify(149.9, target=149.0, margin=1.0) ==BAND_IN # within assert classify(150.5, target=149.0, margin=1.0) ==BAND_HIGH # 1.5 above assert classify(147.5, target=149.0, margin=1.0) ==BAND_LOW # 1.5 below - - + + def test_manual_bounds_use_set_point_and_margin(): cfg = ThresholdConfig(set_point=149.0, margin=1.0, auto=False) assert resolve_bounds([200.0, 0.0], cfg) == (149.0, 1.0) - + def test_auto_bounds_use_mean_and_sigma(): cfg = ThresholdConfig(auto=True) target, margin = resolve_bounds([148.0, 150.0, 149.0], cfg) assert target == 149.0 - assert margin == math.sqrt(2 / 3) - + assert margin == math.sqrt(2 / 3) + def test_classify_all_manual(): cfg = ThresholdConfig(set_point=149.0, margin=1.0, auto=False) assert classify_all([149.0, 151.0, 147.0], cfg) == [BAND_IN, BAND_HIGH, BAND_LOW] - \ No newline at end of file diff --git a/tests/test_wafer_layouts.py b/tests/test_wafer_layouts.py index e558daa..57ed111 100644 --- a/tests/test_wafer_layouts.py +++ b/tests/test_wafer_layouts.py @@ -1,6 +1,7 @@ """Tests for src/pygui/backend/wafer_layouts.py.""" import pytest -from pygui.backend.wafer.wafer_layouts import load_layout, available_families + +from pygui.backend.wafer.wafer_layouts import available_families, load_layout from pygui.backend.wafer.zwafer_models import Sensor diff --git a/tests/test_wafer_map_item.py b/tests/test_wafer_map_item.py index bc99450..7485e47 100644 --- a/tests/test_wafer_map_item.py +++ b/tests/test_wafer_map_item.py @@ -1,9 +1,8 @@ """Tests for src/pygui/backend/visualization/wafer_map_item.py.""" -from unittest.mock import MagicMock, patch +from unittest.mock import MagicMock import pytest - # Use the session-scoped qapp fixture from conftest.py so Qt Property/Signal # descriptors work properly. pytestmark = pytest.mark.usefixtures("qapp")