Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion board_configs/desktop/board_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import sys

from displaydev import env_bool, env_float, env_int
from boarddev import env_float, env_int
from displaydev.auto import AutoDisplay

_width = env_int("PYDEVICES_WIDTH", 320)
Expand Down
2 changes: 1 addition & 1 deletion board_configs/pgdisplay/board_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import sys

if sys.platform == "win32":
from displaydev import env_get, env_set
from boarddev import env_get, env_set

# SDL2's default Windows audio driver (WASAPI) has a compatibility issue
# with pygame.mixer.Channel's play()/queue() small-chunk playback pattern
Expand Down
2 changes: 1 addition & 1 deletion board_configs/psdisplay/board_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
Board configuration for PyScript.
"""

from displaydev import env_int
from boarddev import env_int
from displaydev.psdisplay import PSDisplay

width = 320
Expand Down
2 changes: 1 addition & 1 deletion board_configs/sdldisplay/linux_kms/board_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

import sys

from displaydev import env_set
from boarddev import env_set

# Why: force SDL's KMS/DRM backend before SDLDisplay constructs the window.
env_set("SDL_VIDEODRIVER", "kmsdrm")
Expand Down
7 changes: 4 additions & 3 deletions docs/board-configs.md
Original file line number Diff line number Diff line change
Expand Up @@ -195,9 +195,10 @@ Panel size overrides (before `import board_config`): `PYDEVICES_WIDTH`,
geometry from `display_drv`, not module-level names on `board_config`.

Set the env vars **before** `import board_config` (or any import that loads
it). Parsing lives in
[`displaydev.env_int`](https://github.com/PyDevices/pydevices/blob/main/lib/displaydev/__init__.py)
and friends.
it). On a host with no environment, `boarddev.env_set("PYDEVICES_WIDTH", 800)`
does the same from Python. Parsing lives in
[`boarddev.env_int`](https://github.com/PyDevices/pydevices/blob/main/lib/boarddev.py)
and friends (`env_get`, `env_float`, `env_bool`, `env_set`).

Per-board configs under `board_configs/` describe hardware; they never
construct an app.
Expand Down
2 changes: 1 addition & 1 deletion docs/displaydev.md
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,7 @@ It replaces the display's `refresh_period_ms` (33 ms, or 16 in the browser)
when the driver starts. Everything that paces frames reads that one attribute:
`appdev.App`'s refresh timer, the display's frame clock, and
`display_driver`'s LVGL refresh timer, so LVGL and non-LVGL apps both follow
it. A board config can set it with `displaydev.env_set("PYDEVICES_REFRESH_MS",
it. A board config can set it with `boarddev.env_set("PYDEVICES_REFRESH_MS",
16)` before it builds the display, on hosts where there is no environment.

Unset, zero or not a number leaves the display's own period. An app that
Expand Down
2 changes: 1 addition & 1 deletion docs/newcomers.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ A board config exposes neutral hardware capabilities; it does not create an appl
| lib/appdev/ | Optional application event dispatcher and coordinator. |
| lib/multimer/ | Portable timer providers and scheduling primitives. |
| lib/events.py and lib/keys.py | Neutral input event and key definitions. |
| lib/boarddev.py | Lazy peripheral access for MicroPython board configurations. |
| lib/boarddev.py | Lazy peripheral access for MicroPython board configurations, and the portable environment helpers (`env_get`, `env_int`, `env_float`, `env_bool`, `env_set`). |
| docs/ | Product, board, driver, installation, and architecture documentation. |
| tests/ | Cross-platform unit tests. |

Expand Down
2 changes: 1 addition & 1 deletion lib/audiodev/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ on the first write, which is why they survive review.
| Instead of | Write | Why |
|------------|-------|-----|
| `del buf[:n]` | `buf[:n] = b""` | MicroPython and CircuitPython bytearrays support no item deletion at all (`TypeError`). Slice assignment behaves identically on all four interpreters, including `[:]`, `n == len`, and `n > len`. |
| `os.environ` | `displaydev.env_get` / `env_set` | Only CPython has `os.environ`; the others have `getenv`/`putenv` only. `env_set` walks `os.environ` → `os.putenv` → a process-local override. |
| `os.environ` | `boarddev.env_get` / `env_set` | Only CPython has `os.environ`; the others have `getenv`/`putenv` only. `env_set` walks `os.environ` → `os.putenv` → a process-local override. |
| `time.monotonic()` | the module's `_monotonic_ms()` / `_elapsed_ms()` | MicroPython measures time with `ticks_ms`, which wraps; comparisons need `ticks_diff`. |
| assuming `threading` | the guarded `threading is not None` paths | Bare MicroPython and CircuitPython have no `threading`, so `_lock` is `None` and the async rebuild degrades to a synchronous one. |

Expand Down
119 changes: 119 additions & 0 deletions lib/boarddev.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,20 @@
It also carries the one thing neither ``audiodev`` nor a board can answer on
its own: whether this machine's audio comes from a board or from a host
backend. See :func:`pcm_out`.

And it is where the portable environment helpers live -- :func:`env_get`,
:func:`env_int`, :func:`env_float`, :func:`env_bool` and :func:`env_set` --
because a board config, an app and every PyDevices package read their
``PYDEVICES_*`` settings through them, on interpreters where ``os.environ``
may not exist::

from boarddev import env_int, env_set

env_set("PYDEVICES_WIDTH", 800) # before board_config is imported
period = env_int("PYDEVICES_REFRESH_MS", 0)

This module imports nothing at module scope, so any package -- ``displaydev``
included -- can import from it without a cycle. Keep it that way.
"""


Expand Down Expand Up @@ -122,3 +136,108 @@ def pcm_in(format=None, **kwargs):
def audio_out(format=None, **kwargs):
""":class:`~audiodev.sample_out.AudioOut` sample player, board or host."""
return _role("audio_out", format, kwargs)


# --- portable environment variables ---------------------------------------
#
# CPython has ``os.environ``; MicroPython and CircuitPython have ``getenv``
# and sometimes ``putenv``, and some ports have neither. ``env_set`` records
# every value in a process-local table as well as in whatever the host
# offers, so a value a page or a board config sets is visible to every
# reader in the same process, on every interpreter.

# Process-local overrides for ports without ``os.environ`` / ``os.putenv``.
_overrides = {}


def env_set(name, value):
"""Set an environment variable portably (CPython, MicroPython, CircuitPython).

Always records a process-local override so ``env_bool`` sees the value even
when the host ``os`` module has no ``environ``. When available, also updates
``os.environ`` or calls ``os.putenv``.
"""
text = "" if value is None else str(value)
_overrides[name] = text

import os

environ = getattr(os, "environ", None)
if environ is not None:
try:
environ[name] = text
return
except Exception:
pass
putenv = getattr(os, "putenv", None)
if putenv is not None:
try:
putenv(name, text)
except Exception:
pass


def env_bool(name, default=False):
"""Read a truthy/falsey environment variable with a portable fallback chain."""
raw = _env_raw(name)
if raw is None:
return bool(default)
text = str(raw).strip().lower()
if text in ("1", "true", "yes", "on"):
return True
if text in ("0", "false", "no", "off"):
return False
return bool(default)


def env_get(name, default=None):
"""Read a string environment variable portably (honors ``env_set`` overrides)."""
raw = _env_raw(name)
if raw is None:
return default
return raw


def env_int(name, default=0):
"""Read an integer environment variable portably (honors ``env_set`` overrides)."""
raw = _env_raw(name)
if raw is None:
return int(default)
try:
return int(str(raw).strip())
except (TypeError, ValueError):
return int(default)


def env_float(name, default=0.0):
"""Read a floating-point environment variable portably."""
raw = _env_raw(name)
if raw is None:
return float(default)
try:
return float(str(raw).strip())
except (TypeError, ValueError):
return float(default)


def _env_raw(name):
if name in _overrides:
return _overrides[name]

import os

environ = getattr(os, "environ", None)
if environ is not None:
try:
value = environ.get(name)
except Exception:
value = None
if value is not None:
return value
getenv = getattr(os, "getenv", None)
if getenv is None:
return None
try:
return getenv(name)
except Exception:
return None
108 changes: 6 additions & 102 deletions lib/displaydev/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,11 +70,6 @@ def byteswap(buf):
"color565",
"color565_swapped",
"color_rgb",
"env_bool",
"env_float",
"env_get",
"env_int",
"env_set",
]

_DEFAULT_AUTO_REFRESH_PERIOD = 33
Expand All @@ -84,103 +79,6 @@ def byteswap(buf):
_DESKTOP_WINDOW_CHROME_W = 16
_DESKTOP_WINDOW_CHROME_H = 48

# Process-local overrides for ports without ``os.environ`` / ``os.putenv``.
_overrides = {}


def env_set(name, value):
"""Set an environment variable portably (CPython, MicroPython, CircuitPython).

Always records a process-local override so ``env_bool`` sees the value even
when the host ``os`` module has no ``environ``. When available, also updates
``os.environ`` or calls ``os.putenv``.
"""
text = "" if value is None else str(value)
_overrides[name] = text

import os

environ = getattr(os, "environ", None)
if environ is not None:
try:
environ[name] = text
return
except Exception:
pass
putenv = getattr(os, "putenv", None)
if putenv is not None:
try:
putenv(name, text)
except Exception:
pass


def env_bool(name, default=False):
"""Read a truthy/falsey environment variable with a portable fallback chain."""
raw = _env_raw(name)
if raw is None:
return bool(default)
text = str(raw).strip().lower()
if text in ("1", "true", "yes", "on"):
return True
if text in ("0", "false", "no", "off"):
return False
return bool(default)


def env_get(name, default=None):
"""Read a string environment variable portably (honors ``env_set`` overrides)."""
raw = _env_raw(name)
if raw is None:
return default
return raw


def env_int(name, default=0):
"""Read an integer environment variable portably (honors ``env_set`` overrides)."""
raw = _env_raw(name)
if raw is None:
return int(default)
try:
return int(str(raw).strip())
except (TypeError, ValueError):
return int(default)


def env_float(name, default=0.0):
"""Read a floating-point environment variable portably."""
raw = _env_raw(name)
if raw is None:
return float(default)
try:
return float(str(raw).strip())
except (TypeError, ValueError):
return float(default)


def _env_raw(name):
if name in _overrides:
return _overrides[name]

import os

environ = getattr(os, "environ", None)
if environ is not None:
try:
value = environ.get(name)
except Exception:
value = None
if value is not None:
return value
getenv = getattr(os, "getenv", None)
if getenv is None:
return None
try:
return getenv(name)
except Exception:
return None


def capabilities():
"""Static metadata for the modular displaydev install model (no backend imports)."""
return {
Expand Down Expand Up @@ -738,6 +636,10 @@ def __init__(self, *, quiet=False):
f"{self.__class__.__name__}: warning: slow byteswap fallback; "
"install utils/byteswap (GitHub MIP) for viper/numpy swap"
)
# The environment helpers are boarddev's, imported where they are read
# so displaydev never re-exports them.
from boarddev import env_bool, env_float

every = env_float("PYDEVICES_FPS_PRINT", 0)
if every > 0:
self.fps_print(every)
Expand All @@ -754,6 +656,8 @@ def _apply_refresh_override(self):
:attr:`refresh_period_ms`, so LVGL and non-LVGL apps both follow it.
Unset, empty, zero or not a number leaves the backend's period alone.
"""
from boarddev import env_int

period = env_int("PYDEVICES_REFRESH_MS", 0)
if period <= 0:
return
Expand Down
4 changes: 2 additions & 2 deletions lib/displaydev/auto.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ def AutoDisplay(
A ``PSDisplay``, ``JNDisplay``, ``WinDisplay``, ``PGDisplay``, or
``SDLDisplay`` with ``get_events`` set for board_config wiring.
"""
from displaydev import env_get
from boarddev import env_get
if canvas_id is None:
canvas_id = env_get("PYDEVICES_CANVAS_ID", "display_canvas")

Expand Down Expand Up @@ -120,7 +120,7 @@ def AutoDisplay(
pass
# pygame / SDL fallback: SDL2 WASAPI glitches with pygame small-chunk
# playback; DirectSound does not. Must land before PGDisplay.pg.init().
from displaydev import env_get, env_set
from boarddev import env_get, env_set

if env_get("SDL_AUDIODRIVER") is None:
env_set("SDL_AUDIODRIVER", "directsound")
Expand Down
3 changes: 2 additions & 1 deletion tests/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ Do not run audiodev tests while another process is playing audio — see
| `test_displaydev_auto.py` | backends must not import `displaydev.auto` |
| `test_displaydev_lifecycle.py` / `test_displaydev_capabilities.py` | quit lifecycle and `capabilities()` |
| `test_displaydev_fps.py` | frame-rate meter: nothing wrapped while off, frames/present/busy counted while on, `flush_rect` as flushes not frames, the appdev refresh calling a wrapper installed late |
| `test_env.py` / `test_color.py` / `test_byteswap.py` / `test_desktop_scale.py` | displaydev helpers |
| `test_color.py` / `test_byteswap.py` / `test_desktop_scale.py` | displaydev helpers |
| `test_env.py` | `boarddev`'s environment helpers, and that `displaydev` no longer carries them |
| `test_needs_refresh.py` / `test_backend_isolation.py` | backend flags and imports |
| `test_jndisplay_scroll.py` | JNDisplay scroll (needs IPython + Pillow) |
| `test_pgdisplay_frame_recorder.py` | frame-recorder surface |
Expand Down
6 changes: 3 additions & 3 deletions tests/portability_probe.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,9 +76,9 @@ def probe_env_helpers():
environment variable, so that path alone would leave this unguarded.
"""
try:
from displaydev import env_get, env_set
from boarddev import env_get, env_set
except ImportError:
print(" skip: displaydev is not installed for this interpreter")
print(" skip: boarddev is not installed for this interpreter")
return

name = "PYDEVICES_PROBE_VAR"
Expand Down Expand Up @@ -173,7 +173,7 @@ def probe_board_config():
try:
import displaydev # noqa: F401
except ImportError:
print(" skip: displaydev is not installed for this interpreter")
print(" skip: boarddev is not installed for this interpreter")
return

import board_config
Expand Down
Loading
Loading