mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-10-04 22:35:08 +00:00
fix(plugin-system): load/enable failures, atomic state files, pip lock, test-double parity (#645)
- load_plugin: an on_enable() that raises unregisters the instance, so the next load retries instead of returning True "already loaded". - get_plugin_info: guard plugin.get_info(); one plugin raising no longer breaks /api/v3/plugins/installed. - plugin_state.json and the operation history are written with atomic_write_text under their lock. - plugin_loader: module-level lock serialises pip installs across the parallel startup loaders. - store_manager._install_via_download: extract dir cleanup moved to finally. - Test doubles: draw_image() warns (DeprecationWarning; the real DisplayManager has none), MockDisplayManager.draw_text accepts the real signature's optional params, VisualTestDisplayManager logs draw errors at WARNING. - Docs/comments: compatibility.py method name, PluginState.LOADED meaning, brittle schema count, why _report_skip_once uses setdefault. - Remove unused PluginOperationQueue.get_active_operations(). Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -5,9 +5,21 @@ Provides mock implementations of display_manager, cache_manager, config_manager,
|
||||
and plugin_manager for use in plugin unit tests.
|
||||
"""
|
||||
|
||||
import warnings
|
||||
from typing import Dict, Any, Optional
|
||||
from PIL import Image
|
||||
|
||||
#: Why draw_image() warns. Kept (rather than removed) so existing plugin test
|
||||
#: suites that call it keep passing, but a plugin that calls it passes its
|
||||
#: tests and then crashes on the Pi.
|
||||
DRAW_IMAGE_DEPRECATION = (
|
||||
"display_manager.draw_image() exists only on the test doubles; the real "
|
||||
"DisplayManager has no such method, so this raises AttributeError on a "
|
||||
"device. Paste onto the canvas instead: "
|
||||
"display_manager.image.paste(img, (x, y)) (with the image as mask, "
|
||||
"image.paste(rgba, (x, y), rgba), for transparency)."
|
||||
)
|
||||
|
||||
|
||||
class MockDisplayManager:
|
||||
"""Mock display manager for testing."""
|
||||
@@ -31,8 +43,16 @@ class MockDisplayManager:
|
||||
"""Update the display."""
|
||||
self.update_called = True
|
||||
|
||||
def draw_text(self, text: str, x: int, y: int, color: tuple = (255, 255, 255), font=None):
|
||||
"""Draw text on the display."""
|
||||
def draw_text(self, text: str, x: int = None, y: int = None, color: tuple = (255, 255, 255),
|
||||
font=None, small_font: bool = False, centered: bool = False):
|
||||
"""Draw text on the display.
|
||||
|
||||
Accepts every argument the real ``DisplayManager.draw_text`` does, so
|
||||
a plugin passing ``small_font``/``centered`` (or leaving x/y to
|
||||
default) doesn't fail here while working on the device. ``font``
|
||||
stays fifth for callers of the old mock signature; pass the rest by
|
||||
keyword, as the real method's positional order differs.
|
||||
"""
|
||||
self.draw_calls.append({
|
||||
'type': 'text',
|
||||
'text': text,
|
||||
@@ -43,7 +63,8 @@ class MockDisplayManager:
|
||||
})
|
||||
|
||||
def draw_image(self, image: Image.Image, x: int, y: int):
|
||||
"""Draw an image on the display."""
|
||||
"""Draw an image on the display. Deprecated: see DRAW_IMAGE_DEPRECATION."""
|
||||
warnings.warn(DRAW_IMAGE_DEPRECATION, DeprecationWarning, stacklevel=2)
|
||||
self.draw_calls.append({
|
||||
'type': 'image',
|
||||
'image': image,
|
||||
|
||||
@@ -29,6 +29,7 @@ through src/common/bdf_font.py, so those pixels cannot drift.
|
||||
import math
|
||||
import os
|
||||
import time
|
||||
import warnings
|
||||
from contextlib import contextmanager
|
||||
from pathlib import Path
|
||||
from typing import Any, List, Optional, Tuple
|
||||
@@ -38,9 +39,12 @@ from src.common.bdf_font import draw_bdf_text, load_bdf_face
|
||||
from src.common.font_layout import crisp_size, load_truetype
|
||||
|
||||
from src.logging_config import get_logger
|
||||
from src.plugin_system.testing.mocks import DRAW_IMAGE_DEPRECATION
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
_draw_image_warning_logged = False
|
||||
|
||||
|
||||
class _MatrixProxy:
|
||||
"""Lightweight proxy so plugins can access display_manager.matrix.width/height."""
|
||||
@@ -321,17 +325,26 @@ class VisualTestDisplayManager:
|
||||
else:
|
||||
self.draw.text((x, y), text, font=current_font, fill=color)
|
||||
except Exception as e:
|
||||
logger.debug(f"Error drawing text: {e}")
|
||||
# WARNING, not DEBUG: the real DisplayManager logs this at ERROR,
|
||||
# and a test double that hides it lets a broken draw pass.
|
||||
logger.warning(f"Error drawing text: {e}")
|
||||
|
||||
def draw_image(self, image: Image.Image, x: int, y: int):
|
||||
"""Draw an image on the display."""
|
||||
"""Draw an image on the display. Deprecated: see DRAW_IMAGE_DEPRECATION."""
|
||||
warnings.warn(DRAW_IMAGE_DEPRECATION, DeprecationWarning, stacklevel=2)
|
||||
global _draw_image_warning_logged
|
||||
if not _draw_image_warning_logged:
|
||||
# Also logged once: the dev preview server drives this class
|
||||
# outside pytest, where DeprecationWarning is hidden by default.
|
||||
_draw_image_warning_logged = True
|
||||
logger.warning(DRAW_IMAGE_DEPRECATION)
|
||||
self.draw_calls.append({
|
||||
'type': 'image', 'image': image, 'x': x, 'y': y,
|
||||
})
|
||||
try:
|
||||
self.image.paste(image, (x, y))
|
||||
except Exception as e:
|
||||
logger.debug(f"Error drawing image: {e}")
|
||||
logger.warning(f"Error drawing image: {e}")
|
||||
|
||||
def _draw_bdf_text(self, text, x, y, color=(255, 255, 255), font=None):
|
||||
"""Draw text in a BDF ``freetype.Face`` with (x, y) as its top-left.
|
||||
|
||||
Reference in New Issue
Block a user