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.
|
Dear PyGui gauge widgets for dashboards and telemetry displays.
|
||||||
|
|
||||||
Current implementation status: Step 6 high-rate update support is implemented. Analog, digital, bar,
|
## Install
|
||||||
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.
|
|
||||||
|
|
||||||
Runtime value updates are intended for the Dear PyGui GUI thread. Attached gauges redraw immediately
|
```bash
|
||||||
for simple value/configuration changes. Animation and smoothing are configurable per gauge; high-rate
|
uv add dpg-gauges
|
||||||
examples marshal latest producer-thread values to GUI-thread frame callbacks.
|
```
|
||||||
|
|
||||||
|
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
|
## 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
|
## Completed steps
|
||||||
|
|
||||||
@@ -30,6 +30,16 @@ Step 1 - Public API contract and pure core:
|
|||||||
|
|
||||||
Step 7 - Documentation, hardening, and beta release.
|
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:
|
Step 6 - High-rate updates, animation, and stress examples:
|
||||||
|
|
||||||
Implemented during Step 6:
|
Implemented during Step 6:
|
||||||
@@ -103,7 +113,7 @@ Implemented during Step 2:
|
|||||||
|
|
||||||
## Known issues
|
## 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
|
## 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 .`.
|
||||||
- Ran `uv run ruff format --check .`.
|
- Ran `uv run ruff format --check .`.
|
||||||
- Ran `uv run pyright`.
|
- 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.
|
- 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.
|
- 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.
|
- 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
|
## 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]
|
[project]
|
||||||
name = "dpg-gauges"
|
name = "dpg-gauges"
|
||||||
version = "0.1.0"
|
version = "0.1.0b1"
|
||||||
description = "Add your description here"
|
description = "Dear PyGui gauge widgets for dashboards and telemetry displays"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
authors = [
|
authors = [
|
||||||
{ name = "Hector van der Aa", email = "hector@h3cx.dev" }
|
{ name = "Hector van der Aa", email = "hector@h3cx.dev" }
|
||||||
|
|||||||
@@ -35,6 +35,8 @@ from .types import (
|
|||||||
ThresholdBand,
|
ThresholdBand,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
__version__ = "0.1.0b1"
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"configure",
|
"configure",
|
||||||
"GaugeTheme",
|
"GaugeTheme",
|
||||||
@@ -67,8 +69,10 @@ __all__ = [
|
|||||||
"set_show",
|
"set_show",
|
||||||
"delete_gauge",
|
"delete_gauge",
|
||||||
"get_gauge_debug_state",
|
"get_gauge_debug_state",
|
||||||
|
"__version__",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
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.")
|
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:
|
def configure(**config: Any) -> None:
|
||||||
|
"""Configure package-wide defaults for subsequently created gauges."""
|
||||||
configure_gauges(**config)
|
configure_gauges(**config)
|
||||||
|
|
||||||
|
|
||||||
@@ -38,34 +39,42 @@ gauge_grid = _widget.gauge_grid
|
|||||||
|
|
||||||
|
|
||||||
def set_value(tag: Tag, value: GaugeValue) -> None:
|
def set_value(tag: Tag, value: GaugeValue) -> None:
|
||||||
|
"""Set a gauge target value from the Dear PyGui GUI thread."""
|
||||||
set_runtime_value(tag, value)
|
set_runtime_value(tag, value)
|
||||||
|
|
||||||
|
|
||||||
def get_value(tag: Tag) -> GaugeValue:
|
def get_value(tag: Tag) -> GaugeValue:
|
||||||
|
"""Return the latest raw target value assigned to a gauge."""
|
||||||
return get_runtime_value(tag)
|
return get_runtime_value(tag)
|
||||||
|
|
||||||
|
|
||||||
def update_gauge(tag: Tag, **config: Any) -> None:
|
def update_gauge(tag: Tag, **config: Any) -> None:
|
||||||
|
"""Update a gauge value and/or configuration fields."""
|
||||||
update_runtime_gauge(tag, **config)
|
update_runtime_gauge(tag, **config)
|
||||||
|
|
||||||
|
|
||||||
def configure_gauge(tag: Tag, **config: Any) -> None:
|
def configure_gauge(tag: Tag, **config: Any) -> None:
|
||||||
|
"""Update configuration fields for an existing gauge."""
|
||||||
update_runtime_gauge(tag, **config)
|
update_runtime_gauge(tag, **config)
|
||||||
|
|
||||||
|
|
||||||
def set_thresholds(tag: Tag, thresholds: list[ThresholdBand] | tuple[ThresholdBand, ...]) -> None:
|
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)
|
set_runtime_thresholds(tag, thresholds)
|
||||||
|
|
||||||
|
|
||||||
def set_markers(tag: Tag, markers: list[GaugeMarker] | tuple[GaugeMarker, ...]) -> None:
|
def set_markers(tag: Tag, markers: list[GaugeMarker] | tuple[GaugeMarker, ...]) -> None:
|
||||||
|
"""Replace the visual markers for an existing gauge."""
|
||||||
set_runtime_markers(tag, markers)
|
set_runtime_markers(tag, markers)
|
||||||
|
|
||||||
|
|
||||||
def set_show(tag: Tag, show: bool) -> None:
|
def set_show(tag: Tag, show: bool) -> None:
|
||||||
|
"""Show or hide an existing gauge."""
|
||||||
set_runtime_show(tag, show)
|
set_runtime_show(tag, show)
|
||||||
|
|
||||||
|
|
||||||
def delete_gauge(tag: Tag) -> None:
|
def delete_gauge(tag: Tag) -> None:
|
||||||
|
"""Delete an existing gauge and its Dear PyGui shell when attached."""
|
||||||
delete_runtime_gauge(tag)
|
delete_runtime_gauge(tag)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ from .types import Tag
|
|||||||
|
|
||||||
|
|
||||||
def get_gauge_debug_state(tag: Tag) -> dict[str, Any]:
|
def get_gauge_debug_state(tag: Tag) -> dict[str, Any]:
|
||||||
|
"""Return renderer and state diagnostics for an existing gauge."""
|
||||||
state = get_gauge_state(tag)
|
state = get_gauge_state(tag)
|
||||||
debug = {
|
debug = {
|
||||||
"tag": state.tag,
|
"tag": state.tag,
|
||||||
|
|||||||
@@ -139,6 +139,7 @@ def create_gauge_context(gauge_type: str, config: ConfigT) -> GaugeContext[Confi
|
|||||||
|
|
||||||
|
|
||||||
def gauge_panel(**config: Any) -> LayoutContext:
|
def gauge_panel(**config: Any) -> LayoutContext:
|
||||||
|
"""Create a lightweight child-window container for related gauges."""
|
||||||
if not _dpg_context_active:
|
if not _dpg_context_active:
|
||||||
return LayoutContext(None)
|
return LayoutContext(None)
|
||||||
tag = config.pop("tag", 0)
|
tag = config.pop("tag", 0)
|
||||||
@@ -160,6 +161,7 @@ def gauge_panel(**config: Any) -> LayoutContext:
|
|||||||
|
|
||||||
|
|
||||||
def gauge_grid(**config: Any) -> LayoutContext:
|
def gauge_grid(**config: Any) -> LayoutContext:
|
||||||
|
"""Create a lightweight horizontal gauge layout container."""
|
||||||
if not _dpg_context_active:
|
if not _dpg_context_active:
|
||||||
return LayoutContext(None)
|
return LayoutContext(None)
|
||||||
columns = max(int(config.pop("columns", 1)), 1)
|
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]:
|
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))
|
return create_gauge_context("analog", AnalogGaugeConfig(**config))
|
||||||
|
|
||||||
|
|
||||||
def digital_gauge(**config: Any) -> GaugeContext[DigitalGaugeConfig]:
|
def digital_gauge(**config: Any) -> GaugeContext[DigitalGaugeConfig]:
|
||||||
|
"""Create a large numeric readout gauge."""
|
||||||
return create_gauge_context("digital", DigitalGaugeConfig(**config))
|
return create_gauge_context("digital", DigitalGaugeConfig(**config))
|
||||||
|
|
||||||
|
|
||||||
def bar_gauge(**config: Any) -> GaugeContext[BarGaugeConfig]:
|
def bar_gauge(**config: Any) -> GaugeContext[BarGaugeConfig]:
|
||||||
|
"""Create a horizontal or vertical fill bar gauge."""
|
||||||
return create_gauge_context("bar", BarGaugeConfig(**config))
|
return create_gauge_context("bar", BarGaugeConfig(**config))
|
||||||
|
|
||||||
|
|
||||||
def level_gauge(**config: Any) -> GaugeContext[LevelGaugeConfig]:
|
def level_gauge(**config: Any) -> GaugeContext[LevelGaugeConfig]:
|
||||||
|
"""Create a tank or level-style vertical gauge."""
|
||||||
return create_gauge_context("level", LevelGaugeConfig(**config))
|
return create_gauge_context("level", LevelGaugeConfig(**config))
|
||||||
|
|
||||||
|
|
||||||
def status_light(**config: Any) -> GaugeContext[StatusLightConfig]:
|
def status_light(**config: Any) -> GaugeContext[StatusLightConfig]:
|
||||||
|
"""Create an LED-style status indicator gauge."""
|
||||||
return create_gauge_context("status_light", StatusLightConfig(**config))
|
return create_gauge_context("status_light", StatusLightConfig(**config))
|
||||||
|
|
||||||
|
|
||||||
def segmented_gauge(**config: Any) -> GaugeContext[SegmentedGaugeConfig]:
|
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))
|
return create_gauge_context("segmented", SegmentedGaugeConfig(**config))
|
||||||
|
|
||||||
|
|
||||||
def compass_gauge(**config: Any) -> GaugeContext[CompassGaugeConfig]:
|
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))
|
return create_gauge_context("compass", CompassGaugeConfig(**config))
|
||||||
|
|
||||||
|
|
||||||
def thermometer_gauge(**config: Any) -> GaugeContext[ThermometerGaugeConfig]:
|
def thermometer_gauge(**config: Any) -> GaugeContext[ThermometerGaugeConfig]:
|
||||||
|
"""Create a thermometer-style temperature gauge."""
|
||||||
return create_gauge_context("thermometer", ThermometerGaugeConfig(**config))
|
return create_gauge_context("thermometer", ThermometerGaugeConfig(**config))
|
||||||
|
|
||||||
|
|
||||||
def battery_gauge(**config: Any) -> GaugeContext[BatteryGaugeConfig]:
|
def battery_gauge(**config: Any) -> GaugeContext[BatteryGaugeConfig]:
|
||||||
|
"""Create a battery-style percentage or value gauge."""
|
||||||
return create_gauge_context("battery", BatteryGaugeConfig(**config))
|
return create_gauge_context("battery", BatteryGaugeConfig(**config))
|
||||||
|
|
||||||
|
|
||||||
def multi_value_gauge(**config: Any) -> GaugeContext[MultiValueGaugeConfig]:
|
def multi_value_gauge(**config: Any) -> GaugeContext[MultiValueGaugeConfig]:
|
||||||
|
"""Create a compact grouped key/value readout gauge."""
|
||||||
return create_gauge_context("multi_value", MultiValueGaugeConfig(**config))
|
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