diff --git a/README.md b/README.md index 076879e..350e506 100644 --- a/README.md +++ b/README.md @@ -87,6 +87,25 @@ To explore the full capabilities, including the property inspector and real-time uv run examples/demo.py ``` +## Development + +You can run formatting, type checking and tests with [`uv`](https://github.com/astral-sh/uv). + +### Running tests locally + +GitHub Actions installs additional system packages for Qt-based tests (see `.github/workflows/tests.yml`). +On a fresh Linux environment you can mirror that setup by running: + +```bash +sudo scripts/install_test_system_deps.sh +``` + +Headless environments also need the same Qt variables the CI sets: + +```bash +QT_API=pyside6 QT_QPA_PLATFORM=offscreen uv run pytest +``` + ## License This project is licensed under a hybrid model depending on the Qt binding used: diff --git a/fieldview/layers/text_layer.py b/fieldview/layers/text_layer.py index 2883e6f..9291d26 100644 --- a/fieldview/layers/text_layer.py +++ b/fieldview/layers/text_layer.py @@ -1,4 +1,4 @@ -from typing import TYPE_CHECKING, Optional, List, Dict, Set, Union +from typing import TYPE_CHECKING, ClassVar, Optional, List, Dict, Set, Union if TYPE_CHECKING: from PySide6.QtGui import QPainter, QColor, QFont, QFontMetrics, QFontDatabase @@ -243,14 +243,54 @@ class ValueLayer(TextLayer): Renders numerical values. """ + UNIT_SUFFIXES: ClassVar[tuple[str, ...]] = ( + "℃", # Degree Celsius + "℉", # Degree Fahrenheit + "%", # Percentage + "㎐", # Hertz + "㎑", # Kilohertz + "㎒", # Megahertz + "㎓", # Gigahertz + "㎂", # Microampere + "㎃", # Milliampere + "㎄", # Kiloampere + "㎎", # Milligram + "㎏", # Kilogram + "㎍", # Microgram + "㎕", # Microliter + "㎖", # Milliliter + "㎗", # Deciliter + "㎘", # Kiloliter + "㎜", # Millimeter + "㎝", # Centimeter + "㎞", # Kilometer + "㎡", # Square meter + "㎧", # Meters per second + "㎨", # Meters per second squared + "㎩", # Kilopascal + "㎪", # Megapascal + "㎫", # Gigapascal + "㎷", # Microvolt + "㎸", # Millivolt + "㎹", # Megavolt + "㎽", # Milliampere-hour + "㎾", # Kilowatt + "㎿", # Megawatt + "㏀", # Kiloohm + "㏁", # Megaohm + "㏃", # Becquerel + "㏄", # Cubic centimeter + "㏈", # Decibel + "㏊", # Hectare + "㏕", # Lux + "㏗", # pH + ) + def __init__(self, data_container, parent: Optional[DataLayer] = None): super().__init__(data_container, parent) self._decimal_places = 2 - self._suffix = "" - self._postfix = "" # Same as suffix? antigravity.md says both. Let's assume prefix/suffix or just suffix. - # antigravity.md says "Can add suffix, postfix". Maybe prefix/suffix? - # Let's implement prefix and suffix. self._prefix = "" + self._postfix = "" @property def decimal_places(self) -> int: @@ -263,11 +303,24 @@ def decimal_places(self, value: int): @property def suffix(self) -> str: - return self._suffix + return self._postfix @suffix.setter def suffix(self, value: str): - self._suffix = value + if not isinstance(value, str): + raise TypeError("Suffix must be a string") + self._postfix = value + self.update_layer() + + @property + def postfix(self) -> str: + return self._postfix + + @postfix.setter + def postfix(self, value: str): + if not isinstance(value, str): + raise TypeError("Postfix must be a string") + self._postfix = value self.update_layer() @property @@ -276,13 +329,15 @@ def prefix(self) -> str: @prefix.setter def prefix(self, value: str): + if not isinstance(value, str): + raise TypeError("Prefix must be a string") self._prefix = value self.update_layer() def _get_text(self, index: int, value: Optional[float], label: str) -> str: if value is None: return "" - return f"{self._prefix}{value:.{self._decimal_places}f}{self._suffix}" + return f"{self._prefix}{value:.{self._decimal_places}f}{self._postfix}" class LabelLayer(TextLayer): diff --git a/pyproject.toml b/pyproject.toml index a02cab7..37703a2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "fieldview" -version = "0.3.1" +version = "0.3.2" description = "Python + Qt based 2D data visualization solution for irregular data points" readme = "README.md" authors = [{name = "FieldView Team", email = "dev@fieldview.org"}] @@ -60,4 +60,3 @@ module = [ ignore_missing_imports = true ignore_errors = true - diff --git a/scripts/install_test_system_deps.sh b/scripts/install_test_system_deps.sh new file mode 100755 index 0000000..447cc46 --- /dev/null +++ b/scripts/install_test_system_deps.sh @@ -0,0 +1,26 @@ +#!/bin/bash +set -euo pipefail + +if [[ $(id -u) -ne 0 ]]; then + echo "This script must be run as root (e.g., with sudo)." >&2 + exit 1 +fi + +apt-get update +apt-get install -y \ + libegl1 \ + libxkbcommon-x11-0 \ + libdbus-1-3 \ + libxcb-icccm4 \ + libxcb-image0 \ + libxcb-keysyms1 \ + libxcb-randr0 \ + libxcb-render-util0 \ + libxcb-xinerama0 \ + libxcb-xinput0 \ + libxcb-xfixes0 \ + x11-utils \ + libxcb-cursor0 \ + libfontconfig1 \ + libglib2.0-0 \ + libgl1 diff --git a/tests/test_text_layer.py b/tests/test_text_layer.py index 0fa3eee..4ff8afc 100644 --- a/tests/test_text_layer.py +++ b/tests/test_text_layer.py @@ -1,5 +1,7 @@ from typing import TYPE_CHECKING +import pytest + if TYPE_CHECKING: from PySide6.QtGui import QFont from PySide6.QtWidgets import QStyleOptionGraphicsItem @@ -28,6 +30,41 @@ def test_value_layer(qtbot): assert layer._get_text(0, 10.12345, "") == "Val: 10.123 m" +def test_value_layer_prefix_property_updates_layer(qtbot, monkeypatch): + dc = DataContainer() + dc.set_data([[0, 0]], [5.4321]) + layer = ValueLayer(dc) + + update_calls: list[None] = [] + monkeypatch.setattr(layer, "update_layer", lambda: update_calls.append(None)) + + layer.prefix = "~" + + assert layer.prefix == "~" + assert len(update_calls) == 1 + + with pytest.raises(TypeError): + layer.prefix = 123 # type: ignore[assignment] + + +def test_value_layer_suffix_and_postfix(qtbot): + dc = DataContainer() + dc.set_data([[0, 0]], [1.2345]) + layer = ValueLayer(dc) + + layer.postfix = "㎧" + assert layer._get_text(0, 1.2345, "") == "1.23㎧" + + layer.prefix = "~" + assert layer._get_text(0, 1.2345, "") == "~1.23㎧" + + layer.suffix = "℃" + assert layer.postfix == "℃" + assert layer._get_text(0, 1.2345, "") == "~1.23℃" + + assert "℃" in ValueLayer.UNIT_SUFFIXES + + def test_label_layer(qtbot): dc = DataContainer() dc.set_data([[0, 0]], [10], ["Test Label"]) diff --git a/uv.lock b/uv.lock index 37f9ca6..fc7aa3b 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 3 +revision = 2 requires-python = ">=3.10" resolution-markers = [ "python_full_version >= '3.12'", @@ -500,7 +500,7 @@ wheels = [ [[package]] name = "fieldview" -version = "0.3.0" +version = "0.3.2" source = { editable = "." } dependencies = [ { name = "numpy" },