feat: deprecate unused plugin-facing methods for removal in 3.7.0 (#610)

35 methods on CacheManager, DisplayManager, FontManager and PluginManager
have no caller in core, the ledmatrix-plugins monorepo or the registry's
third-party plugins, but plugins live elsewhere, so they stay for one
release. src.deprecation.deprecated logs a warning (and emits a
DeprecationWarning) the first time each is called in a process, naming the
release that removes it. The list and replacements are in CHANGELOG and
PLUGIN_API_REFERENCE's new Deprecated APIs section; a test pins the set.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Chuck
2026-09-23 12:44:55 -04:00
committed by GitHub
co-authored by Claude Opus 5.5
parent a8b3e86775
commit 604f58ff07
8 changed files with 210 additions and 0 deletions
+18
View File
@@ -26,6 +26,24 @@ accepts both, but the store flags the old spelling as deprecated
- `src.wifi_manager.get_wifi_status_path()` — where WiFi status messages for
the display are written (`config/wifi_status.json`).
Deprecated, removed in 3.7.0 (each logs a warning on first use; see
`docs/PLUGIN_API_REFERENCE.md#deprecated-apis` for replacements). Nothing in
core, the monorepo or the registry's third-party plugins calls them:
- `CacheManager`: `has_data_changed`, `update_cache`, `setup_persistent_cache`,
`get_sport_live_interval`, `get_sport_key_from_cache_key`,
`get_background_cached_data`, `is_background_data_available`,
`record_cache_hit`, `record_cache_miss`, `record_fetch_time`,
`get_cache_metrics`, `log_cache_metrics`, `get_memory_cache_stats`.
- `DisplayManager`: `draw_weather_icon`, `draw_sun`, `draw_cloud`, `draw_rain`,
`draw_snow`, `draw_text_with_icons`, `get_scrolling_stats`.
- `FontManager`: `set_override`, `remove_override`, `get_overrides`,
`add_font`, `remove_font`, `validate_font`, `get_font_catalog`,
`get_available_fonts`, `get_size_tokens`, `get_performance_stats`,
`get_manager_fonts`, `get_detected_fonts`, `get_plugin_fonts`,
`unregister_plugin_fonts`.
- `PluginManager.get_enabled_plugins`.
## 3.5.0
New modules a plugin may import via `src.*` (floor on 3.5.0):
+21
View File
@@ -13,6 +13,7 @@ Complete API reference for plugin developers. This document describes all method
- [Display Manager](#display-manager)
- [Cache Manager](#cache-manager)
- [Plugin Manager](#plugin-manager)
- [Deprecated APIs](#deprecated-apis)
---
@@ -1074,3 +1075,23 @@ if "weather" in enabled_plugins:
- [Plugin Development Guide](PLUGIN_DEVELOPMENT_GUIDE.md) - Complete development guide
- [Advanced Plugin Development](ADVANCED_PLUGIN_DEVELOPMENT.md) - Advanced patterns and examples
---
## Deprecated APIs
These still work in 3.6 but log a warning the first time they are called
(`journalctl -u ledmatrix` shows which one), and are **removed in 3.7.0**.
Nothing in core, the official plugins or the third-party plugins in the
registry calls them.
| Object | Methods | Instead |
|---|---|---|
| `cache_manager` | `update_cache` | `set()` |
| `cache_manager` | `get_background_cached_data`, `is_background_data_available` | `get()` |
| `cache_manager` | `has_data_changed`, `setup_persistent_cache`, `get_sport_live_interval`, `get_sport_key_from_cache_key`, `record_cache_hit`, `record_cache_miss`, `record_fetch_time`, `get_cache_metrics`, `log_cache_metrics`, `get_memory_cache_stats` | no replacement |
| `display_manager` | `draw_weather_icon`, `draw_sun`, `draw_cloud`, `draw_rain`, `draw_snow`, `draw_text_with_icons` | draw your own icons (the weather plugin ships `WeatherIcons`) |
| `display_manager` | `get_scrolling_stats` | no replacement |
| `font_manager` | `get_font_catalog`, `get_available_fonts` | read `font_catalog` |
| `font_manager` | `set_override`, `remove_override`, `get_overrides`, `add_font`, `remove_font`, `validate_font`, `get_size_tokens`, `get_performance_stats`, `get_manager_fonts`, `get_detected_fonts`, `get_plugin_fonts`, `unregister_plugin_fonts` | no replacement |
| `plugin_manager` | `get_enabled_plugins` | check `enabled` on the entries in `plugin_manager.plugins` |
+14
View File
@@ -38,6 +38,7 @@ from src.cache.disk_cache import DiskCache
from src.cache.cache_strategy import CacheStrategy
from src.cache.cache_metrics import CacheMetrics
from src.logging_config import get_logger
from src.deprecation import deprecated
# Canonical implementation lives in src.cache.disk_cache; re-exported here
# because this module's docstring documents it and external code may import
@@ -473,6 +474,7 @@ class CacheManager:
"""Get the cache directory path."""
return self.cache_dir
@deprecated("3.7.0")
def has_data_changed(self, data_type: str, new_data: Dict[str, Any]) -> bool:
"""Check if data has changed from cached version."""
cached_data = self.load_cache(data_type)
@@ -578,6 +580,7 @@ class CacheManager:
"""Check if the US stock market is currently open."""
return self._strategy_component.is_market_open()
@deprecated("3.7.0", "use set()")
def update_cache(self, data_type: str, data: Dict[str, Any]) -> bool:
"""Update cache with new data."""
cache_data = {
@@ -623,6 +626,7 @@ class CacheManager:
cache_data['ttl'] = ttl
self.save_cache(key, cache_data)
@deprecated("3.7.0")
def setup_persistent_cache(self) -> bool:
"""
Set up a persistent cache directory with proper permissions.
@@ -834,6 +838,7 @@ class CacheManager:
else:
self.logger.info("Disk cache cleanup thread stopped successfully")
@deprecated("3.7.0")
def get_sport_live_interval(self, sport_key: str) -> int:
"""
Get the live_update_interval for a specific sport from config.
@@ -855,6 +860,7 @@ class CacheManager:
"""
return self._strategy_component.get_data_type_from_key(key)
@deprecated("3.7.0")
def get_sport_key_from_cache_key(self, key: str) -> Optional[str]:
"""
Extract sport key from cache key to determine appropriate live_update_interval.
@@ -894,6 +900,7 @@ class CacheManager:
data_type = self.get_data_type_from_key(key)
return self.get_cached_data_with_strategy(key, data_type)
@deprecated("3.7.0", "use get()")
def get_background_cached_data(self, key: str, sport_key: Optional[str] = None) -> Optional[Dict[str, Any]]:
"""
Get data from background service cache with appropriate strategy.
@@ -931,6 +938,7 @@ class CacheManager:
self.record_cache_miss('background')
return None
@deprecated("3.7.0", "use get()")
def is_background_data_available(self, key: str, sport_key: Optional[str] = None) -> bool:
"""
Check if background service has fresh data available.
@@ -960,26 +968,32 @@ class CacheManager:
date_str = datetime.now(pytz.utc).strftime('%Y%m%d')
return f"{sport}_{date_str}"
@deprecated("3.7.0")
def record_cache_hit(self, cache_type: str = 'regular') -> None:
"""Record a cache hit for performance monitoring."""
self._metrics_component.record_hit(cache_type)
@deprecated("3.7.0")
def record_cache_miss(self, cache_type: str = 'regular') -> None:
"""Record a cache miss for performance monitoring."""
self._metrics_component.record_miss(cache_type)
@deprecated("3.7.0")
def record_fetch_time(self, duration: float) -> None:
"""Record fetch operation duration for performance monitoring."""
self._metrics_component.record_fetch_time(duration)
@deprecated("3.7.0")
def get_cache_metrics(self) -> Dict[str, Any]:
"""Get current cache performance metrics."""
return self._metrics_component.get_metrics()
@deprecated("3.7.0")
def log_cache_metrics(self) -> None:
"""Log current cache performance metrics."""
self._metrics_component.log_metrics()
@deprecated("3.7.0")
def get_memory_cache_stats(self) -> Dict[str, Any]:
"""
Get statistics about the memory cache.
+47
View File
@@ -0,0 +1,47 @@
"""Marking plugin-facing core APIs for removal.
Plugins live in other repositories, so a method nothing in core calls may
still be called by a plugin nobody has checked. Such methods get
``@deprecated`` for one release before they are removed: the first call in a
process logs a warning naming the method and the release that removes it
(visible in ``journalctl -u ledmatrix``), and emits a DeprecationWarning for
tooling.
"""
import functools
import threading
import warnings
from typing import Callable, Optional, TypeVar
from src.logging_config import get_logger
logger = get_logger(__name__)
F = TypeVar("F", bound=Callable)
_warned = set()
_warned_lock = threading.Lock()
def deprecated(removal: str, alternative: Optional[str] = None) -> Callable[[F], F]:
"""Decorate a function or method that will be removed in ``removal``."""
def decorate(func: F) -> F:
message = f"{func.__qualname__}() is deprecated and will be removed in LEDMatrix {removal}"
if alternative:
message += f"; {alternative}"
@functools.wraps(func)
def wrapper(*args, **kwargs):
with _warned_lock:
first = func.__qualname__ not in _warned
_warned.add(func.__qualname__)
if first:
logger.warning(message)
warnings.warn(message, DeprecationWarning, stacklevel=2)
return func(*args, **kwargs)
wrapper.__deprecated__ = message
return wrapper # type: ignore[return-value]
return decorate
+8
View File
@@ -52,6 +52,7 @@ import zlib
import freetype
from src.common import snapshot_policy
from src.deprecation import deprecated
from src.common.permission_utils import (
ensure_directory_permissions,
ensure_file_permissions,
@@ -1139,6 +1140,7 @@ class DisplayManager:
except Exception as e:
logger.error(f"Error drawing text: {e}", exc_info=True)
@deprecated("3.7.0")
def draw_sun(self, x: int, y: int, size: int = 16):
"""Draw a sun icon using yellow circles and lines."""
center = (x + size//2, y + size//2)
@@ -1159,6 +1161,7 @@ class DisplayManager:
end_y = center[1] + ((radius + ray_length) * math.sin(rad))
self.draw.line([start_x, start_y, end_x, end_y], fill=(255, 255, 0), width=2)
@deprecated("3.7.0")
def draw_cloud(self, x: int, y: int, size: int = 16, color=(200, 200, 200)):
"""Draw a cloud icon."""
# Draw multiple circles to form a cloud shape
@@ -1166,6 +1169,7 @@ class DisplayManager:
self.draw.ellipse([x+size//2, y+size//3, x+size//2+size//2, y+size//3+size//2], fill=color)
self.draw.ellipse([x+size//3, y+size//6, x+size//3+size//2, y+size//6+size//2], fill=color)
@deprecated("3.7.0")
def draw_rain(self, x: int, y: int, size: int = 16):
"""Draw rain icon with cloud and droplets."""
# Draw cloud
@@ -1180,6 +1184,7 @@ class DisplayManager:
self.draw.line([drop_x, drop_y, drop_x, drop_y+drop_size],
fill=drop_color, width=2)
@deprecated("3.7.0")
def draw_snow(self, x: int, y: int, size: int = 16):
"""Draw snow icon with cloud and snowflakes."""
# Draw cloud
@@ -1300,6 +1305,7 @@ class DisplayManager:
]
self.draw.polygon(bolt_points, fill=bolt_color)
@deprecated("3.7.0")
def draw_weather_icon(self, condition: str, x: int, y: int, size: int = 16) -> None:
"""Draw a weather icon based on the condition."""
if condition.lower() in ['clear', 'sunny']:
@@ -1316,6 +1322,7 @@ class DisplayManager:
self._draw_sun(x, y, size)
# Note: No update_display() here - let the caller handle the update
@deprecated("3.7.0")
def draw_text_with_icons(self, text: str, icons: List[tuple] = None, x: int = None, y: int = None,
color: tuple = (255, 255, 255)):
"""Draw text with weather icons at specified positions."""
@@ -1600,6 +1607,7 @@ class DisplayManager:
if removed_count > 0:
logger.debug(f"Cleaned up {removed_count} expired deferred updates")
@deprecated("3.7.0")
def get_scrolling_stats(self) -> dict:
"""Get current scrolling statistics for debugging."""
return {
+15
View File
@@ -40,6 +40,7 @@ from pathlib import Path
from PIL import ImageFont
from src.common.font_layout import load_truetype, resolve_asset_path
from typing import Dict, Tuple, Optional, Union, Any, List
from src.deprecation import deprecated
logger = logging.getLogger(__name__)
@@ -167,6 +168,7 @@ class FontManager:
logger.debug(f"Registered font for {manager_id}.{element_key}: {family}@{size_px}px")
@deprecated("3.7.0")
def get_manager_fonts(self, manager_id: Optional[str] = None) -> Dict[str, Any]:
"""
Get registered fonts for a specific manager or all managers.
@@ -181,6 +183,7 @@ class FontManager:
return self.manager_fonts.get(manager_id, {})
return self.manager_fonts.copy()
@deprecated("3.7.0")
def get_detected_fonts(self) -> Dict[str, Dict[str, Any]]:
"""Get all detected font usage across managers."""
return self.detected_fonts.copy()
@@ -357,6 +360,7 @@ class FontManager:
logger.error(f"Plugin font not found: {font_path}")
return None
@deprecated("3.7.0")
def unregister_plugin_fonts(self, plugin_id: str) -> bool:
"""Unregister all fonts for a plugin."""
try:
@@ -393,6 +397,7 @@ class FontManager:
for key in keys_to_remove:
del self.font_cache[key]
@deprecated("3.7.0")
def get_plugin_fonts(self, plugin_id: str) -> List[str]:
"""Get list of font families registered by a plugin."""
if plugin_id in self.plugin_font_catalogs:
@@ -637,6 +642,7 @@ class FontManager:
# ==================== Override Management ====================
@deprecated("3.7.0")
def set_override(self, element_key: str, family: str = None, size_px: int = None):
"""Set font override for a specific element."""
if element_key not in self.font_overrides:
@@ -656,6 +662,7 @@ class FontManager:
self.clear_cache()
logger.info(f"Font override set for {element_key}: {self.font_overrides.get(element_key, {})}")
@deprecated("3.7.0")
def remove_override(self, element_key: str):
"""Remove font override for a specific element."""
if element_key in self.font_overrides:
@@ -664,6 +671,7 @@ class FontManager:
self.clear_cache()
logger.info(f"Font override removed for {element_key}")
@deprecated("3.7.0")
def get_overrides(self) -> Dict[str, Dict[str, str]]:
"""Get current font overrides."""
return self.font_overrides.copy()
@@ -753,10 +761,12 @@ class FontManager:
self.metrics_cache.clear()
logger.info("Font cache cleared")
@deprecated("3.7.0", "read font_catalog")
def get_available_fonts(self) -> Dict[str, str]:
"""Get dictionary of available font families and their paths."""
return self.font_catalog.copy()
@deprecated("3.7.0")
def get_size_tokens(self) -> Dict[str, int]:
"""Get available size tokens."""
return self.size_tokens.copy()
@@ -767,6 +777,7 @@ class FontManager:
self.performance_stats[operation] = {}
self.performance_stats[operation][font_key] = duration
@deprecated("3.7.0")
def get_performance_stats(self) -> Dict[str, Any]:
"""Get performance statistics."""
uptime = time.time() - self.performance_stats["start_time"]
@@ -788,10 +799,12 @@ class FontManager:
"detected_fonts": len(self.detected_fonts)
}
@deprecated("3.7.0", "read font_catalog")
def get_font_catalog(self) -> Dict[str, str]:
"""Get the current font catalog."""
return self.font_catalog.copy()
@deprecated("3.7.0")
def add_font(self, font_file_path: str, family_name: str) -> bool:
"""Add a new font to the catalog."""
try:
@@ -824,6 +837,7 @@ class FontManager:
logger.error(f"Error adding font {family_name}: {e}")
return False
@deprecated("3.7.0")
def remove_font(self, family_name: str) -> bool:
"""Remove a font from the catalog."""
try:
@@ -851,6 +865,7 @@ class FontManager:
logger.error(f"Error removing font {family_name}: {e}")
return False
@deprecated("3.7.0")
def validate_font(self, font_path: str) -> Dict[str, Any]:
"""Validate a font file."""
try:
+2
View File
@@ -26,6 +26,7 @@ from src.plugin_system.schema_manager import (
CORE_VEGAS_TUNING_KEYS, SchemaManager, normalize_legacy_booleans,
)
from src.common.path_safety import safe_path_component
from src.deprecation import deprecated
from src.common.permission_utils import (
ensure_directory_permissions,
get_plugin_dir_mode
@@ -698,6 +699,7 @@ class PluginManager:
"""
return self.plugins.copy()
@deprecated("3.7.0", "check each plugin's enabled flag in plugins")
def get_enabled_plugins(self) -> List[str]:
"""
Get list of enabled plugin IDs.
+85
View File
@@ -0,0 +1,85 @@
"""@deprecated: plugin-facing APIs nothing in core, the monorepo or the
registry's third-party plugins calls, kept for one release with a warning."""
import logging
import os
import warnings
import pytest
os.environ.setdefault("EMULATOR", "true")
from src import deprecation
from src.deprecation import deprecated
#: Everything deprecated for removal in 3.7.0. Removing one of these, or
#: deprecating another, should be a deliberate edit here too.
DEPRECATED = {
"src.cache_manager.CacheManager": [
"has_data_changed", "update_cache", "setup_persistent_cache",
"get_sport_live_interval", "get_sport_key_from_cache_key",
"get_background_cached_data", "is_background_data_available",
"record_cache_hit", "record_cache_miss", "record_fetch_time",
"get_cache_metrics", "log_cache_metrics", "get_memory_cache_stats",
],
"src.display_manager.DisplayManager": [
"draw_sun", "draw_cloud", "draw_rain", "draw_snow", "draw_weather_icon",
"draw_text_with_icons", "get_scrolling_stats",
],
"src.font_manager.FontManager": [
"get_manager_fonts", "get_detected_fonts", "unregister_plugin_fonts",
"get_plugin_fonts", "set_override", "remove_override", "get_overrides",
"get_available_fonts", "get_size_tokens", "get_performance_stats",
"get_font_catalog", "add_font", "remove_font", "validate_font",
],
"src.plugin_system.plugin_manager.PluginManager": ["get_enabled_plugins"],
}
def _cls(path):
import importlib
module, name = path.rsplit(".", 1)
return getattr(importlib.import_module(module), name)
@pytest.mark.parametrize("path", sorted(DEPRECATED))
def test_exactly_these_methods_are_deprecated(path):
cls = _cls(path)
marked = sorted(name for name, value in vars(cls).items()
if hasattr(value, "__deprecated__"))
assert marked == sorted(DEPRECATED[path])
for name in marked:
assert "3.7.0" in getattr(cls, name).__deprecated__
@pytest.fixture
def fresh(monkeypatch):
monkeypatch.setattr(deprecation, "_warned", set())
def test_first_call_warns_and_logs_then_stays_quiet(fresh, caplog):
@deprecated("9.9.9", "use other()")
def old(x):
"""Doc."""
return x * 2
with warnings.catch_warnings(record=True) as caught, caplog.at_level(logging.WARNING):
warnings.simplefilter("always")
assert old(2) == 4
assert old(3) == 6
assert [str(w.message) for w in caught] == [
"test_first_call_warns_and_logs_then_stays_quiet.<locals>.old() is deprecated "
"and will be removed in LEDMatrix 9.9.9; use other()"]
assert caught[0].category is DeprecationWarning
assert caught[0].filename == __file__ # points at the caller
assert sum("will be removed in LEDMatrix 9.9.9" in r.message for r in caplog.records) == 1
assert old.__name__ == "old" and old.__doc__ == "Doc."
def test_decorated_methods_still_work(fresh):
from src.font_manager import FontManager
fm = FontManager({})
with warnings.catch_warnings():
warnings.simplefilter("ignore")
assert fm.get_font_catalog() == fm.font_catalog