diff --git a/board_configs/desktop/board_config.py b/board_configs/desktop/board_config.py index 6b01a689..e96ee720 100644 --- a/board_configs/desktop/board_config.py +++ b/board_configs/desktop/board_config.py @@ -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) diff --git a/board_configs/pgdisplay/board_config.py b/board_configs/pgdisplay/board_config.py index 54f63ff1..0b1d1b3d 100644 --- a/board_configs/pgdisplay/board_config.py +++ b/board_configs/pgdisplay/board_config.py @@ -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 diff --git a/board_configs/psdisplay/board_config.py b/board_configs/psdisplay/board_config.py index 1d96734f..6d474823 100644 --- a/board_configs/psdisplay/board_config.py +++ b/board_configs/psdisplay/board_config.py @@ -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 diff --git a/board_configs/sdldisplay/linux_kms/board_config.py b/board_configs/sdldisplay/linux_kms/board_config.py index 9a1580a3..ce198602 100644 --- a/board_configs/sdldisplay/linux_kms/board_config.py +++ b/board_configs/sdldisplay/linux_kms/board_config.py @@ -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") diff --git a/docs/board-configs.md b/docs/board-configs.md index 988ca4ac..287fe1ae 100644 --- a/docs/board-configs.md +++ b/docs/board-configs.md @@ -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. diff --git a/docs/displaydev.md b/docs/displaydev.md index f85470d6..a758a5bb 100644 --- a/docs/displaydev.md +++ b/docs/displaydev.md @@ -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 diff --git a/docs/newcomers.md b/docs/newcomers.md index ebb6aa54..a4ddd910 100644 --- a/docs/newcomers.md +++ b/docs/newcomers.md @@ -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. | diff --git a/lib/audiodev/README.md b/lib/audiodev/README.md index 569726fc..00696ce4 100644 --- a/lib/audiodev/README.md +++ b/lib/audiodev/README.md @@ -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. | diff --git a/lib/boarddev.py b/lib/boarddev.py index 93f2172b..70cd59f1 100644 --- a/lib/boarddev.py +++ b/lib/boarddev.py @@ -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. """ @@ -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 diff --git a/lib/displaydev/__init__.py b/lib/displaydev/__init__.py index 0497dfea..8d89db2c 100644 --- a/lib/displaydev/__init__.py +++ b/lib/displaydev/__init__.py @@ -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 @@ -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 { @@ -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) @@ -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 diff --git a/lib/displaydev/auto.py b/lib/displaydev/auto.py index 6e0b6d6a..b0a0bc0d 100644 --- a/lib/displaydev/auto.py +++ b/lib/displaydev/auto.py @@ -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") @@ -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") diff --git a/tests/README.md b/tests/README.md index 37792872..2d9bdebd 100644 --- a/tests/README.md +++ b/tests/README.md @@ -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 | diff --git a/tests/portability_probe.py b/tests/portability_probe.py index 24616be1..da544ae5 100644 --- a/tests/portability_probe.py +++ b/tests/portability_probe.py @@ -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" @@ -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 diff --git a/tests/test_autodisplay.py b/tests/test_autodisplay.py index bc1de56d..54f649c3 100644 --- a/tests/test_autodisplay.py +++ b/tests/test_autodisplay.py @@ -125,7 +125,7 @@ def test_win32_prefers_windisplay(self): pg_mod.PGDisplay = mock.Mock(name="PGDisplay_should_not_be_used") with mock.patch.object(ad, "host_kind", return_value="desktop"), mock.patch.object( ad.sys, "platform", "win32" - ), mock.patch("displaydev.env_set") as env_set, mock.patch.dict( + ), mock.patch("boarddev.env_set") as env_set, mock.patch.dict( sys.modules, {"displaydev.windisplay": win_mod, "displaydev.pgdisplay": pg_mod} ): result = AutoDisplay(width=10, height=10, quiet=True) @@ -141,8 +141,8 @@ def test_win32_sets_directsound_when_windisplay_unavailable(self): pg_mod.PGDisplay = mock.Mock(return_value=display) with mock.patch.object(ad, "host_kind", return_value="desktop"), mock.patch.object( ad.sys, "platform", "win32" - ), mock.patch("displaydev.env_get", return_value=None) as env_get, mock.patch( - "displaydev.env_set" + ), mock.patch("boarddev.env_get", return_value=None) as env_get, mock.patch( + "boarddev.env_set" ) as env_set, mock.patch.dict( sys.modules, {"displaydev.windisplay": None, "displaydev.pgdisplay": pg_mod} ): @@ -158,7 +158,7 @@ def test_win32_skips_directsound_for_pyscript(self): ps_mod.PSDisplay = mock.Mock(return_value=display) with mock.patch.object(ad, "host_kind", return_value="pyscript"), mock.patch.object( ad.sys, "platform", "win32" - ), mock.patch("displaydev.env_set") as env_set, mock.patch.dict( + ), mock.patch("boarddev.env_set") as env_set, mock.patch.dict( sys.modules, {"displaydev.psdisplay": ps_mod} ): AutoDisplay(width=10, height=10, canvas_id="c", quiet=True) diff --git a/tests/test_desktop_board_config.py b/tests/test_desktop_board_config.py index 9f20d2de..b2721545 100644 --- a/tests/test_desktop_board_config.py +++ b/tests/test_desktop_board_config.py @@ -48,9 +48,6 @@ def test_eager_mcu_shape_with_mocked_autodisplay(self): display.fill = mock.Mock() display.get_events = mock.Mock(name="get_events") displaydev_mod = types.ModuleType("displaydev") - displaydev_mod.env_bool = lambda name, default=False: default - displaydev_mod.env_float = lambda name, default=0.0: default - displaydev_mod.env_int = lambda name, default=0: default displaydev_auto = types.ModuleType("displaydev.auto") displaydev_auto.AutoDisplay = mock.Mock(return_value=display) displaydev_mod.auto = displaydev_auto diff --git a/tests/test_displaydev_fps.py b/tests/test_displaydev_fps.py index be3fa5a4..5769766c 100644 --- a/tests/test_displaydev_fps.py +++ b/tests/test_displaydev_fps.py @@ -13,6 +13,7 @@ import _env # noqa: F401 from _support import quiet +import boarddev import displaydev import multimer from appdev import App @@ -240,7 +241,7 @@ def tearDown(self): def _clean_env(self, **env): for k in ("PYDEVICES_FPS", "PYDEVICES_FPS_PRINT"): - displaydev._overrides.pop(k, None) + boarddev._overrides.pop(k, None) patched = {k: v for k, v in os.environ.items() if not k.startswith("PYDEVICES_FPS")} patched.update(env) return mock.patch.dict(os.environ, patched, clear=True) diff --git a/tests/test_env.py b/tests/test_env.py index 45d7902b..46a10450 100644 --- a/tests/test_env.py +++ b/tests/test_env.py @@ -1,58 +1,84 @@ # SPDX-FileCopyrightText: 2026 Brad Barnett # # SPDX-License-Identifier: MIT -"""Tests for cross-interpreter ``displaydev`` env helpers.""" +"""Tests for the cross-interpreter env helpers in ``boarddev``.""" import os import unittest import _env # noqa: F401 -import displaydev +import boarddev class TestEnvBool(unittest.TestCase): def test_missing_returns_default(self): os.environ.pop("PYDEVICES_TEST_ENV_BOOL", None) - displaydev._overrides.pop("PYDEVICES_TEST_ENV_BOOL", None) - self.assertFalse(displaydev.env_bool("PYDEVICES_TEST_ENV_BOOL", False)) - self.assertTrue(displaydev.env_bool("PYDEVICES_TEST_ENV_BOOL", True)) + boarddev._overrides.pop("PYDEVICES_TEST_ENV_BOOL", None) + self.assertFalse(boarddev.env_bool("PYDEVICES_TEST_ENV_BOOL", False)) + self.assertTrue(boarddev.env_bool("PYDEVICES_TEST_ENV_BOOL", True)) def test_truthy_values(self): for value in ("1", "true", "TRUE", " yes ", "on"): os.environ["PYDEVICES_TEST_ENV_BOOL"] = value - self.assertTrue(displaydev.env_bool("PYDEVICES_TEST_ENV_BOOL", False)) + self.assertTrue(boarddev.env_bool("PYDEVICES_TEST_ENV_BOOL", False)) def test_falsey_values(self): for value in ("0", "false", "NO", " off "): os.environ["PYDEVICES_TEST_ENV_BOOL"] = value - self.assertFalse(displaydev.env_bool("PYDEVICES_TEST_ENV_BOOL", True)) + self.assertFalse(boarddev.env_bool("PYDEVICES_TEST_ENV_BOOL", True)) def test_unknown_value_uses_default(self): os.environ["PYDEVICES_TEST_ENV_BOOL"] = "maybe" - self.assertFalse(displaydev.env_bool("PYDEVICES_TEST_ENV_BOOL", False)) - self.assertTrue(displaydev.env_bool("PYDEVICES_TEST_ENV_BOOL", True)) + self.assertFalse(boarddev.env_bool("PYDEVICES_TEST_ENV_BOOL", False)) + self.assertTrue(boarddev.env_bool("PYDEVICES_TEST_ENV_BOOL", True)) def test_env_set_override_without_os_environ(self): - displaydev._overrides.pop("PYDEVICES_TEST_ENV_SET", None) + boarddev._overrides.pop("PYDEVICES_TEST_ENV_SET", None) os.environ.pop("PYDEVICES_TEST_ENV_SET", None) - displaydev.env_set("PYDEVICES_TEST_ENV_SET", "1") - self.assertTrue(displaydev.env_bool("PYDEVICES_TEST_ENV_SET", False)) - displaydev.env_set("PYDEVICES_TEST_ENV_SET", "0") - self.assertFalse(displaydev.env_bool("PYDEVICES_TEST_ENV_SET", True)) + boarddev.env_set("PYDEVICES_TEST_ENV_SET", "1") + self.assertTrue(boarddev.env_bool("PYDEVICES_TEST_ENV_SET", False)) + boarddev.env_set("PYDEVICES_TEST_ENV_SET", "0") + self.assertFalse(boarddev.env_bool("PYDEVICES_TEST_ENV_SET", True)) def test_env_float(self): - displaydev.env_set("PYDEVICES_TEST_ENV_FLOAT", "1.25") - self.assertEqual(displaydev.env_float("PYDEVICES_TEST_ENV_FLOAT", 2), 1.25) - displaydev.env_set("PYDEVICES_TEST_ENV_FLOAT", "invalid") - self.assertEqual(displaydev.env_float("PYDEVICES_TEST_ENV_FLOAT", 2), 2.0) + boarddev.env_set("PYDEVICES_TEST_ENV_FLOAT", "1.25") + self.assertEqual(boarddev.env_float("PYDEVICES_TEST_ENV_FLOAT", 2), 1.25) + boarddev.env_set("PYDEVICES_TEST_ENV_FLOAT", "invalid") + self.assertEqual(boarddev.env_float("PYDEVICES_TEST_ENV_FLOAT", 2), 2.0) def tearDown(self): os.environ.pop("PYDEVICES_TEST_ENV_BOOL", None) os.environ.pop("PYDEVICES_TEST_ENV_SET", None) - displaydev._overrides.pop("PYDEVICES_TEST_ENV_BOOL", None) - displaydev._overrides.pop("PYDEVICES_TEST_ENV_FLOAT", None) - displaydev._overrides.pop("PYDEVICES_TEST_ENV_SET", None) + boarddev._overrides.pop("PYDEVICES_TEST_ENV_BOOL", None) + boarddev._overrides.pop("PYDEVICES_TEST_ENV_FLOAT", None) + boarddev._overrides.pop("PYDEVICES_TEST_ENV_SET", None) + + +class TestTheyLeftDisplaydev(unittest.TestCase): + """A hard break: the helpers moved to boarddev and displaydev keeps none.""" + + NAMES = ("env_get", "env_int", "env_float", "env_bool", "env_set", "_env_raw", "_overrides") + + def test_from_displaydev_import_fails(self): + import displaydev + + for name in self.NAMES: + with self.subTest(name=name): + self.assertFalse(hasattr(displaydev, name)) + self.assertNotIn(name, displaydev.__all__) + with self.assertRaises(ImportError): + exec("from displaydev import " + name, {}) + + def test_displaydev_still_reads_them_through_boarddev(self): + """displaydev reads PYDEVICES_* through boarddev at call time.""" + import inspect + + import displaydev + + source = inspect.getsource(displaydev) + self.assertIn("from boarddev import env_bool, env_float", source) + self.assertIn("from boarddev import env_int", source) if __name__ == "__main__": diff --git a/tests/test_lvgl_display_driver.py b/tests/test_lvgl_display_driver.py index e6f219d6..1f8f3bbc 100644 --- a/tests/test_lvgl_display_driver.py +++ b/tests/test_lvgl_display_driver.py @@ -36,7 +36,7 @@ import _env from _support import quiet -import displaydev +import boarddev from displaydev import DisplayDriver DISPLAY_DRIVER = _env.ROOT / "lib" / "display_driver.py" @@ -377,7 +377,7 @@ def show(self, _timer=None): class TestFramePeriod(unittest.TestCase): def _lvgl_period(self, **env): - displaydev._overrides.pop("PYDEVICES_REFRESH_MS", None) + boarddev._overrides.pop("PYDEVICES_REFRESH_MS", None) clean = {k: v for k, v in os.environ.items() if not k.startswith("PYDEVICES_")} clean.update(env) with mock.patch.dict(os.environ, clean, clear=True), quiet(): diff --git a/tests/test_portability.py b/tests/test_portability.py index 72ad264b..1f58d949 100644 --- a/tests/test_portability.py +++ b/tests/test_portability.py @@ -129,7 +129,7 @@ def test_list_deletion_is_still_allowed(self): class OsEnvironTests(unittest.TestCase): """Only CPython has ``os.environ``; the others have getenv/putenv only.""" - def test_portable_modules_use_displaydev_env_helpers(self): + def test_portable_modules_use_boarddev_env_helpers(self): for rel in PORTABLE: for node in ast.walk(_parse(rel)): if ( @@ -139,7 +139,7 @@ def test_portable_modules_use_displaydev_env_helpers(self): and node.value.id == "os" ): self.fail( - "{}:{}: os.environ is CPython-only. Use displaydev " + "{}:{}: os.environ is CPython-only. Use boarddev " "env_get / env_set, which fall back to " "os.putenv.".format(rel, node.lineno) ) diff --git a/tests/test_refresh_override.py b/tests/test_refresh_override.py index 3aa7c581..05ca84dc 100644 --- a/tests/test_refresh_override.py +++ b/tests/test_refresh_override.py @@ -16,7 +16,7 @@ import _env # noqa: F401 from _support import quiet -import displaydev +import boarddev import multimer from appdev import App from displaydev import DisplayDriver @@ -57,13 +57,13 @@ def init(self): class _Env: def _env(self, **env): - displaydev._overrides.pop("PYDEVICES_REFRESH_MS", None) + boarddev._overrides.pop("PYDEVICES_REFRESH_MS", None) clean = {k: v for k, v in os.environ.items() if not k.startswith("PYDEVICES_")} clean.update(env) return mock.patch.dict(os.environ, clean, clear=True) def tearDown(self): - displaydev._overrides.pop("PYDEVICES_REFRESH_MS", None) + boarddev._overrides.pop("PYDEVICES_REFRESH_MS", None) app = App.current() if app is not None: app._perform_teardown() @@ -98,7 +98,7 @@ def test_nonsense_leaves_the_period_alone(self): def test_env_set_feeds_it_without_an_environment(self): """A board config on a host with no os.environ uses env_set.""" with self._env(): - displaydev.env_set("PYDEVICES_REFRESH_MS", 40) + boarddev.env_set("PYDEVICES_REFRESH_MS", 40) self.assertEqual(40, Panel().refresh_period_ms) def test_the_frame_clock_follows(self): diff --git a/tests/test_standalone.py b/tests/test_standalone.py index a90280de..7dac710c 100644 --- a/tests/test_standalone.py +++ b/tests/test_standalone.py @@ -5,7 +5,8 @@ Each test copies *only* one package into a temporary directory and imports it in a fresh subprocess whose path contains nothing else from the repository. -``displaydev`` also receives shared ``events.py`` / ``keys.py``. +``displaydev`` also receives shared ``events.py`` / ``keys.py``, and +``boarddev.py``, whose environment helpers a driver reads when it starts. """ import os @@ -149,6 +150,9 @@ def test_displaydev_imports_and_runs_in_isolation(self): shutil.copytree(_env.DISPLAYDEV_DIR, os.path.join(tmp, "displaydev")) shutil.copyfile(_env.EVENTS_PY, os.path.join(tmp, "events.py")) shutil.copyfile(_env.KEYS_PY, os.path.join(tmp, "keys.py")) + shutil.copyfile( + os.path.join(_env.ROOT, "lib", "boarddev.py"), os.path.join(tmp, "boarddev.py") + ) env = dict(os.environ) env["PYTHONPATH"] = tmp