step 7: harden docs and prepare beta
This commit is contained in:
85
README.md
85
README.md
@@ -2,11 +2,82 @@
|
||||
|
||||
Dear PyGui gauge widgets for dashboards and telemetry displays.
|
||||
|
||||
Current implementation status: Step 6 high-rate update support is implemented. Analog, digital, bar,
|
||||
level, status, segmented, compass, thermometer, battery, and multi-value gauges render Dear PyGui
|
||||
drawlist visuals. Bar/level gauges draw threshold bands and markers; analog gauges draw ticks,
|
||||
redline bands, markers, and needles.
|
||||
## Install
|
||||
|
||||
Runtime value updates are intended for the Dear PyGui GUI thread. Attached gauges redraw immediately
|
||||
for simple value/configuration changes. Animation and smoothing are configurable per gauge; high-rate
|
||||
examples marshal latest producer-thread values to GUI-thread frame callbacks.
|
||||
```bash
|
||||
uv add dpg-gauges
|
||||
```
|
||||
|
||||
For local development from another project:
|
||||
|
||||
```bash
|
||||
uv add --editable ../dpg-gauges
|
||||
```
|
||||
|
||||
## Minimal Example
|
||||
|
||||
```python
|
||||
import dearpygui.dearpygui as dpg
|
||||
import dpg_gauges as dpgg
|
||||
|
||||
dpg.create_context()
|
||||
with dpg.window(label="Dashboard"):
|
||||
dpgg.analog_gauge(tag="rpm", label="RPM", value=3200, max_value=8000, width=220, height=220)
|
||||
dpgg.digital_gauge(tag="speed", label="Speed", value=88, unit="km/h", width=220, height=90)
|
||||
|
||||
dpg.create_viewport(title="gauges", width=520, height=340)
|
||||
dpg.setup_dearpygui()
|
||||
dpg.show_viewport()
|
||||
dpg.start_dearpygui()
|
||||
dpg.destroy_context()
|
||||
```
|
||||
|
||||
## Dashboard Example
|
||||
|
||||
```python
|
||||
with dpg.window(label="Engine"):
|
||||
with dpgg.gauge_panel(label="Powertrain", width=-1, height=260):
|
||||
with dpgg.gauge_grid(columns=3):
|
||||
dpgg.analog_gauge(tag="rpm", label="RPM", max_value=8000, redline_start=6500)
|
||||
dpgg.bar_gauge(tag="boost", label="Boost", max_value=30, unit="psi")
|
||||
dpgg.status_light(tag="ecu", label="ECU", state=True)
|
||||
```
|
||||
|
||||
## GUI-Thread Contract
|
||||
|
||||
Dear PyGui item operations must happen on the GUI thread. Gauge creation and runtime updates such as
|
||||
`set_value`, `update_gauge`, `set_thresholds`, and `delete_gauge` are GUI-thread functions. Worker
|
||||
threads may produce data, but applications should marshal the latest values to a Dear PyGui frame
|
||||
callback before calling dpg-gauges APIs.
|
||||
|
||||
## Animation And Smoothing
|
||||
|
||||
Animation changes the displayed value over a short duration without changing the raw target value.
|
||||
Smoothing improves readability during high-rate updates.
|
||||
|
||||
```python
|
||||
dpgg.digital_gauge(
|
||||
tag="speed",
|
||||
animation=dpgg.AnimationConfig(enabled=True, duration=0.15),
|
||||
smoothing=dpgg.SmoothingConfig(enabled=True, alpha=0.25, max_step=5),
|
||||
)
|
||||
```
|
||||
|
||||
Disable animation with `animation=False` for immediate response.
|
||||
|
||||
## Thresholds And Markers
|
||||
|
||||
Thresholds and markers are visual only; they do not trigger callbacks or alarms.
|
||||
|
||||
```python
|
||||
dpgg.bar_gauge(
|
||||
tag="coolant",
|
||||
min_value=40,
|
||||
max_value=120,
|
||||
thresholds=[dpgg.ThresholdBand(100, 120, (255, 45, 45, 150))],
|
||||
markers=[dpgg.GaugeMarker(110, (255, 255, 255, 255), thickness=3)],
|
||||
)
|
||||
```
|
||||
|
||||
Implemented gauge types: analog, digital, bar, level, status light, segmented, compass,
|
||||
thermometer, battery, and multi-value.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
## Current status
|
||||
|
||||
Step 6 completed and verified with automated checks. Step 6 commit is pending.
|
||||
Step 7 completed and verified with automated checks. Step 7 commit and beta tag are pending.
|
||||
|
||||
## Completed steps
|
||||
|
||||
@@ -30,6 +30,16 @@ Step 1 - Public API contract and pure core:
|
||||
|
||||
Step 7 - Documentation, hardening, and beta release.
|
||||
|
||||
Implemented during Step 7:
|
||||
|
||||
- Rewrote `README.md` with install, minimal examples, dashboard usage, GUI-thread expectations, animation/smoothing, and thresholds/markers.
|
||||
- Added `docs/GETTING_STARTED.md`, `docs/EXAMPLES.md`, and `docs/API_REFERENCE.md`.
|
||||
- Added docstrings for all public exported callables.
|
||||
- Reviewed public API exports and added `__version__`.
|
||||
- Added Step 7 hardening tests for unknown gauges, duplicate tags, invalid ranges, invalid thresholds, invalid markers, non-finite values, deleted updates, hidden sizing, and animation disabled/enabled behavior.
|
||||
- Bumped package version to `0.1.0b1`.
|
||||
- Verified a local editable install from a temporary project.
|
||||
|
||||
Step 6 - High-rate updates, animation, and stress examples:
|
||||
|
||||
Implemented during Step 6:
|
||||
@@ -103,7 +113,7 @@ Implemented during Step 2:
|
||||
|
||||
## Known issues
|
||||
|
||||
Manual visual/stress example checks for Steps 3, 4, 5, and 6 were not run in this headless session.
|
||||
Manual visual/stress example checks for Steps 3, 4, 5, 6, and 7 were not run in this headless session.
|
||||
|
||||
## Commands used
|
||||
|
||||
@@ -130,6 +140,16 @@ Manual visual/stress example checks for Steps 3, 4, 5, and 6 were not run in thi
|
||||
- Ran `uv run ruff format .`.
|
||||
- Ran `uv run ruff format --check .`.
|
||||
- Ran `uv run pyright`.
|
||||
- Committed Step 6 with `git commit -m "step 6: add animation and high rate update stress tests"` before starting Step 7.
|
||||
- Read `codex/FEATURES.md`, `codex/ARCHITECTURE.md`, `codex/STEPS.md`, and `codex/AGENTS.md` before Step 7 changes.
|
||||
- Implemented Step 7 README/docs, public callable docstrings, hardening tests, version bump, and README/API status updates.
|
||||
- Ran `uv run python - <<'PY' ...` to verify all exported callables have docstrings.
|
||||
- Ran `uv sync`.
|
||||
- Ran `uv run pytest`.
|
||||
- Ran `uv run ruff check .`.
|
||||
- Ran `uv run ruff format --check .`.
|
||||
- Ran `uv run pyright`.
|
||||
- Created `/tmp/opencode/dpg-gauges-editable-check`, ran `uv init --bare --name dpg-gauges-editable-check`, `uv add --editable /home/hector/projects/dpg-gauges`, and `uv run python -c "import dpg_gauges as dpgg; print(dpgg.__name__)"`.
|
||||
- Committed Step 5 with `git commit -m "step 5: complete first gauge catalogue"` before starting Step 6.
|
||||
- Read `codex/FEATURES.md`, `codex/ARCHITECTURE.md`, `codex/STEPS.md`, and `codex/AGENTS.md` before Step 6 changes.
|
||||
- Implemented Step 6 animation/smoothing scheduling refinements, high-rate examples, README status update, diagnostics fields, and tests.
|
||||
@@ -162,4 +182,4 @@ Manual visual/stress example checks for Steps 3, 4, 5, and 6 were not run in thi
|
||||
|
||||
## Next action
|
||||
|
||||
Commit Step 6, then implement Step 7.
|
||||
Commit Step 7 and tag `v0.1.0b1` if requested.
|
||||
|
||||
41
docs/API_REFERENCE.md
Normal file
41
docs/API_REFERENCE.md
Normal file
@@ -0,0 +1,41 @@
|
||||
# API Reference
|
||||
|
||||
## Configuration
|
||||
|
||||
- `configure(**config)`: set package-wide defaults for later gauges.
|
||||
|
||||
## Gauge Creation
|
||||
|
||||
Creation functions are context managers and accept common options such as `tag`, `label`, `value`,
|
||||
`min_value`, `max_value`, `unit`, `width`, `height`, `show`, `tooltip`, `callback`, `theme`,
|
||||
`style`, `animation`, `smoothing`, `thresholds`, and `markers`.
|
||||
|
||||
- `analog_gauge(**config)`
|
||||
- `digital_gauge(**config)`
|
||||
- `bar_gauge(**config)`
|
||||
- `level_gauge(**config)`
|
||||
- `status_light(**config)`
|
||||
- `segmented_gauge(**config)`
|
||||
- `compass_gauge(**config)`
|
||||
- `thermometer_gauge(**config)`
|
||||
- `battery_gauge(**config)`
|
||||
- `multi_value_gauge(**config)`
|
||||
|
||||
## Layout Helpers
|
||||
|
||||
- `gauge_panel(**config)`: child-window panel for grouped gauges.
|
||||
- `gauge_grid(**config)`: lightweight horizontal gauge group.
|
||||
|
||||
## Runtime API
|
||||
|
||||
- `set_value(tag, value)`: set the latest target value.
|
||||
- `get_value(tag)`: return the latest raw target value.
|
||||
- `update_gauge(tag, **config)`: update value and/or configuration.
|
||||
- `configure_gauge(tag, **config)`: update configuration.
|
||||
- `set_thresholds(tag, thresholds)`: replace visual threshold bands.
|
||||
- `set_markers(tag, markers)`: replace visual markers.
|
||||
- `set_show(tag, show)`: show or hide a gauge.
|
||||
- `delete_gauge(tag)`: remove a gauge.
|
||||
- `get_gauge_debug_state(tag)`: return internal debug state.
|
||||
|
||||
Runtime functions are GUI-thread functions unless explicitly documented otherwise.
|
||||
23
docs/EXAMPLES.md
Normal file
23
docs/EXAMPLES.md
Normal file
@@ -0,0 +1,23 @@
|
||||
# Examples
|
||||
|
||||
Run examples with `uv run python examples/<name>.py`.
|
||||
|
||||
- `basic_analog.py`
|
||||
- `basic_digital.py`
|
||||
- `basic_bar.py`
|
||||
- `basic_level.py`
|
||||
- `basic_status.py`
|
||||
- `basic_segmented.py`
|
||||
- `basic_compass.py`
|
||||
- `basic_thermometer.py`
|
||||
- `basic_battery.py`
|
||||
- `basic_multi_value.py`
|
||||
- `thresholds_markers.py`
|
||||
- `animations.py`
|
||||
- `dashboard.py`
|
||||
- `sizing.py`
|
||||
- `live_high_rate.py`
|
||||
- `live_worst_case.py`
|
||||
|
||||
The live examples use a producer thread for data generation and a Dear PyGui frame callback for all
|
||||
gauge updates.
|
||||
19
docs/GETTING_STARTED.md
Normal file
19
docs/GETTING_STARTED.md
Normal file
@@ -0,0 +1,19 @@
|
||||
# Getting Started
|
||||
|
||||
Install with `uv add dpg-gauges`, then import as `import dpg_gauges as dpgg`.
|
||||
|
||||
Create gauges inside a Dear PyGui context:
|
||||
|
||||
```python
|
||||
import dearpygui.dearpygui as dpg
|
||||
import dpg_gauges as dpgg
|
||||
|
||||
dpg.create_context()
|
||||
with dpg.window(label="Dashboard"):
|
||||
dpgg.digital_gauge(tag="speed", label="Speed", value=72, unit="km/h")
|
||||
|
||||
dpgg.set_value("speed", 88)
|
||||
```
|
||||
|
||||
All widget creation and runtime updates are Dear PyGui GUI-thread operations. If telemetry is
|
||||
produced in a worker thread, store the latest value and apply it from a GUI frame callback.
|
||||
@@ -1,7 +1,7 @@
|
||||
[project]
|
||||
name = "dpg-gauges"
|
||||
version = "0.1.0"
|
||||
description = "Add your description here"
|
||||
version = "0.1.0b1"
|
||||
description = "Dear PyGui gauge widgets for dashboards and telemetry displays"
|
||||
readme = "README.md"
|
||||
authors = [
|
||||
{ name = "Hector van der Aa", email = "hector@h3cx.dev" }
|
||||
|
||||
@@ -35,6 +35,8 @@ from .types import (
|
||||
ThresholdBand,
|
||||
)
|
||||
|
||||
__version__ = "0.1.0b1"
|
||||
|
||||
__all__ = [
|
||||
"configure",
|
||||
"GaugeTheme",
|
||||
@@ -67,8 +69,10 @@ __all__ = [
|
||||
"set_show",
|
||||
"delete_gauge",
|
||||
"get_gauge_debug_state",
|
||||
"__version__",
|
||||
]
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""Print a short message for the package console script."""
|
||||
print("dpg-gauges is a library package; import dpg_gauges to use it.")
|
||||
|
||||
@@ -20,6 +20,7 @@ from .types import GaugeMarker, GaugeValue, Tag, ThresholdBand
|
||||
|
||||
|
||||
def configure(**config: Any) -> None:
|
||||
"""Configure package-wide defaults for subsequently created gauges."""
|
||||
configure_gauges(**config)
|
||||
|
||||
|
||||
@@ -38,34 +39,42 @@ gauge_grid = _widget.gauge_grid
|
||||
|
||||
|
||||
def set_value(tag: Tag, value: GaugeValue) -> None:
|
||||
"""Set a gauge target value from the Dear PyGui GUI thread."""
|
||||
set_runtime_value(tag, value)
|
||||
|
||||
|
||||
def get_value(tag: Tag) -> GaugeValue:
|
||||
"""Return the latest raw target value assigned to a gauge."""
|
||||
return get_runtime_value(tag)
|
||||
|
||||
|
||||
def update_gauge(tag: Tag, **config: Any) -> None:
|
||||
"""Update a gauge value and/or configuration fields."""
|
||||
update_runtime_gauge(tag, **config)
|
||||
|
||||
|
||||
def configure_gauge(tag: Tag, **config: Any) -> None:
|
||||
"""Update configuration fields for an existing gauge."""
|
||||
update_runtime_gauge(tag, **config)
|
||||
|
||||
|
||||
def set_thresholds(tag: Tag, thresholds: list[ThresholdBand] | tuple[ThresholdBand, ...]) -> None:
|
||||
"""Replace the visual threshold bands for an existing gauge."""
|
||||
set_runtime_thresholds(tag, thresholds)
|
||||
|
||||
|
||||
def set_markers(tag: Tag, markers: list[GaugeMarker] | tuple[GaugeMarker, ...]) -> None:
|
||||
"""Replace the visual markers for an existing gauge."""
|
||||
set_runtime_markers(tag, markers)
|
||||
|
||||
|
||||
def set_show(tag: Tag, show: bool) -> None:
|
||||
"""Show or hide an existing gauge."""
|
||||
set_runtime_show(tag, show)
|
||||
|
||||
|
||||
def delete_gauge(tag: Tag) -> None:
|
||||
"""Delete an existing gauge and its Dear PyGui shell when attached."""
|
||||
delete_runtime_gauge(tag)
|
||||
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ from .types import Tag
|
||||
|
||||
|
||||
def get_gauge_debug_state(tag: Tag) -> dict[str, Any]:
|
||||
"""Return renderer and state diagnostics for an existing gauge."""
|
||||
state = get_gauge_state(tag)
|
||||
debug = {
|
||||
"tag": state.tag,
|
||||
|
||||
@@ -139,6 +139,7 @@ def create_gauge_context(gauge_type: str, config: ConfigT) -> GaugeContext[Confi
|
||||
|
||||
|
||||
def gauge_panel(**config: Any) -> LayoutContext:
|
||||
"""Create a lightweight child-window container for related gauges."""
|
||||
if not _dpg_context_active:
|
||||
return LayoutContext(None)
|
||||
tag = config.pop("tag", 0)
|
||||
@@ -160,6 +161,7 @@ def gauge_panel(**config: Any) -> LayoutContext:
|
||||
|
||||
|
||||
def gauge_grid(**config: Any) -> LayoutContext:
|
||||
"""Create a lightweight horizontal gauge layout container."""
|
||||
if not _dpg_context_active:
|
||||
return LayoutContext(None)
|
||||
columns = max(int(config.pop("columns", 1)), 1)
|
||||
@@ -175,42 +177,52 @@ def gauge_grid(**config: Any) -> LayoutContext:
|
||||
|
||||
|
||||
def analog_gauge(**config: Any) -> GaugeContext[AnalogGaugeConfig]:
|
||||
"""Create an analog dial gauge with ticks, needle, thresholds, and markers."""
|
||||
return create_gauge_context("analog", AnalogGaugeConfig(**config))
|
||||
|
||||
|
||||
def digital_gauge(**config: Any) -> GaugeContext[DigitalGaugeConfig]:
|
||||
"""Create a large numeric readout gauge."""
|
||||
return create_gauge_context("digital", DigitalGaugeConfig(**config))
|
||||
|
||||
|
||||
def bar_gauge(**config: Any) -> GaugeContext[BarGaugeConfig]:
|
||||
"""Create a horizontal or vertical fill bar gauge."""
|
||||
return create_gauge_context("bar", BarGaugeConfig(**config))
|
||||
|
||||
|
||||
def level_gauge(**config: Any) -> GaugeContext[LevelGaugeConfig]:
|
||||
"""Create a tank or level-style vertical gauge."""
|
||||
return create_gauge_context("level", LevelGaugeConfig(**config))
|
||||
|
||||
|
||||
def status_light(**config: Any) -> GaugeContext[StatusLightConfig]:
|
||||
"""Create an LED-style status indicator gauge."""
|
||||
return create_gauge_context("status_light", StatusLightConfig(**config))
|
||||
|
||||
|
||||
def segmented_gauge(**config: Any) -> GaugeContext[SegmentedGaugeConfig]:
|
||||
"""Create a segmented strip gauge for shift-light or cell-style displays."""
|
||||
return create_gauge_context("segmented", SegmentedGaugeConfig(**config))
|
||||
|
||||
|
||||
def compass_gauge(**config: Any) -> GaugeContext[CompassGaugeConfig]:
|
||||
"""Create a compass gauge that wraps numeric headings to 0-360 degrees."""
|
||||
return create_gauge_context("compass", CompassGaugeConfig(**config))
|
||||
|
||||
|
||||
def thermometer_gauge(**config: Any) -> GaugeContext[ThermometerGaugeConfig]:
|
||||
"""Create a thermometer-style temperature gauge."""
|
||||
return create_gauge_context("thermometer", ThermometerGaugeConfig(**config))
|
||||
|
||||
|
||||
def battery_gauge(**config: Any) -> GaugeContext[BatteryGaugeConfig]:
|
||||
"""Create a battery-style percentage or value gauge."""
|
||||
return create_gauge_context("battery", BatteryGaugeConfig(**config))
|
||||
|
||||
|
||||
def multi_value_gauge(**config: Any) -> GaugeContext[MultiValueGaugeConfig]:
|
||||
"""Create a compact grouped key/value readout gauge."""
|
||||
return create_gauge_context("multi_value", MultiValueGaugeConfig(**config))
|
||||
|
||||
|
||||
|
||||
93
tests/test_step7_hardening.py
Normal file
93
tests/test_step7_hardening.py
Normal file
@@ -0,0 +1,93 @@
|
||||
# pyright: reportGeneralTypeIssues=false
|
||||
|
||||
from collections.abc import Generator
|
||||
|
||||
import dearpygui.dearpygui as dpg
|
||||
import pytest
|
||||
|
||||
import dpg_gauges as dpgg
|
||||
from dpg_gauges.exceptions import GaugeConfigError, GaugeNotFoundError
|
||||
from dpg_gauges.gauges import DigitalGaugeConfig
|
||||
from dpg_gauges.state import clear_registry, get_gauge_state, register_gauge
|
||||
from dpg_gauges.types import AnimationConfig
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def clean_registry() -> Generator[None]:
|
||||
clear_registry()
|
||||
dpgg.configure()
|
||||
yield
|
||||
clear_registry()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def dpg_context() -> Generator[None]:
|
||||
dpg.create_context()
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
dpg.destroy_context()
|
||||
|
||||
|
||||
def test_public_exports_and_version() -> None:
|
||||
assert dpgg.__version__ == "0.1.0b1"
|
||||
for name in dpgg.__all__:
|
||||
assert hasattr(dpgg, name)
|
||||
|
||||
|
||||
def test_unknown_duplicate_and_deleted_gauge_errors() -> None:
|
||||
with pytest.raises(GaugeNotFoundError):
|
||||
dpgg.set_value("missing", 1)
|
||||
|
||||
dpgg.digital_gauge(tag="speed")
|
||||
with pytest.raises(GaugeConfigError):
|
||||
register_gauge("digital", DigitalGaugeConfig(tag="speed"))
|
||||
|
||||
dpgg.delete_gauge("speed")
|
||||
with pytest.raises(GaugeNotFoundError):
|
||||
dpgg.update_gauge("speed", value=2)
|
||||
|
||||
|
||||
def test_invalid_range_threshold_marker_and_non_finite_value() -> None:
|
||||
with pytest.raises(GaugeConfigError):
|
||||
dpgg.digital_gauge(tag="bad", min_value=10, max_value=0)
|
||||
|
||||
dpgg.bar_gauge(tag="bar", min_value=0, max_value=100)
|
||||
with pytest.raises(GaugeConfigError):
|
||||
dpgg.set_thresholds("bar", [dpgg.ThresholdBand(float("nan"), 10, (255, 0, 0))])
|
||||
with pytest.raises(GaugeConfigError):
|
||||
dpgg.set_markers("bar", [dpgg.GaugeMarker(float("nan"), (255, 255, 255))])
|
||||
|
||||
dpgg.set_value("bar", float("inf"))
|
||||
state = get_gauge_state("bar")
|
||||
assert state.invalid_value is True
|
||||
assert state.clamped_value is None
|
||||
|
||||
|
||||
def test_hidden_layout_sizing_survives_show_later(dpg_context: None) -> None:
|
||||
with dpg.window(label="Hidden"):
|
||||
dpgg.digital_gauge(tag="hidden", value=1, width=180, height=80, show=False)
|
||||
|
||||
state = get_gauge_state("hidden")
|
||||
assert state.last_nonzero_width == 180
|
||||
assert state.last_nonzero_height == 80
|
||||
|
||||
dpgg.set_show("hidden", True)
|
||||
assert state.config.show is True
|
||||
assert state.last_nonzero_width == 180
|
||||
|
||||
|
||||
def test_animation_disabled_and_enabled_modes() -> None:
|
||||
dpgg.digital_gauge(tag="instant", value=0, animation=False)
|
||||
dpgg.set_value("instant", 100)
|
||||
instant = get_gauge_state("instant")
|
||||
assert instant.animation_state.active is False
|
||||
assert instant.displayed_value == 100
|
||||
|
||||
dpgg.digital_gauge(
|
||||
tag="animated", value=0, animation=AnimationConfig(enabled=True, duration=1.0)
|
||||
)
|
||||
dpgg.set_value("animated", 100)
|
||||
animated = get_gauge_state("animated")
|
||||
assert animated.animation_state.active is True
|
||||
assert animated.animation_state.target_value == 100
|
||||
Reference in New Issue
Block a user