diff --git a/CHANGELOG.md b/CHANGELOG.md index db613e1f..feb9bc3f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -538,18 +538,52 @@ read any of them: - The 35 plugin-facing methods deprecated in 3.5.0 are now removed in 3.8.0, not 3.7.0: 3.7.0 shipped with all of them still in place, still warning "will be removed in LEDMatrix 3.7.0". The warning, the docs and - `test/test_deprecation.py` now say 3.8.0. Nothing is removed yet. + `test/test_deprecation.py` now say 3.8.0. They are removed in this + release (see Removed, below). - New `scripts/plugin_api_usage.py` lists every `@deprecated` core method and scans core, the plugin monorepo and the registry's third-party plugins for calls and overrides, telling real uses from unrelated methods of the same name. Its output is `docs/DEPRECATIONS_3.8.md` (linked from - `docs/PLUGIN_API_REFERENCE.md#deprecated-apis`): 34 of the 35 are unused; - `CacheManager.get_memory_cache_stats` is still called by core's own - `log_memory_cache_stats()`, so it stays until that call migrates. + `docs/PLUGIN_API_REFERENCE.md#deprecated-apis`); no plugin uses any of + the 35. - `test/test_deprecation.py` fails while any `@deprecated` marker names a release at or below `src.__version__`, so a release can no longer ship warning about a removal it has already passed. +### Removed + +The 35 plugin-facing methods deprecated in 3.5.0 (each has logged a warning +on first call since, announced for 3.7.0 and then moved to 3.8.0) are gone. +The usage scan (`docs/DEPRECATIONS_3.8.md`, re-run 2026-10-01) found no call +or override of any of them in the 46 monorepo plugins or the 8 third-party +plugins `plugins.json` lists, and core's own last callers went with them. A +plugin that still calls one gets an `AttributeError`; +`docs/PLUGIN_API_REFERENCE.md#deprecated-apis` lists what to use instead. + +- `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`. The + private change-detection helpers behind `has_data_changed` + (`_has_weather_changed` and friends, `_is_market_open`) went with it. +- `DisplayManager`: `draw_weather_icon`, `draw_sun`, `draw_cloud`, `draw_rain`, + `draw_snow`, `draw_text_with_icons`, `get_scrolling_stats`, and with them + the `WEATHER_COLORS` table and the private `_draw_sun`/`_draw_cloud`/ + `_draw_rain`/`_draw_snow`/`_draw_storm` helpers. + `VisualTestDisplayManager` (the plugin test harness) drops its copies of + the icon methods too, so a plugin's visual tests fail the way the real + display would instead of passing against methods that no longer exist. +- `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`, plus the `size_tokens` attribute and the private + `_save_overrides` and `_clear_plugin_font_cache`. `resolve_font()` still + applies `config/font_overrides.json`. +- `PluginManager.get_enabled_plugins` (check `enabled` on the entries in + `plugin_manager.plugins`). + ### Web UI styling: a real Tailwind build - The web UI's utility classes now come from a generated diff --git a/docs/ADVANCED_PLUGIN_DEVELOPMENT.md b/docs/ADVANCED_PLUGIN_DEVELOPMENT.md index 4c56650f..386652a6 100644 --- a/docs/ADVANCED_PLUGIN_DEVELOPMENT.md +++ b/docs/ADVANCED_PLUGIN_DEVELOPMENT.md @@ -27,7 +27,7 @@ Advanced patterns, examples, and best practices for developing LEDMatrix plugins The Display Manager's icon methods — `draw_weather_icon()`, `draw_sun()`, `draw_cloud()`, `draw_rain()`, `draw_snow()` and `draw_text_with_icons()` — -are deprecated, removed in 3.8.0. Draw your own icons instead: render them +were removed in 3.8.0. Draw your own icons instead: render them onto a PIL image and paste it onto `self.display_manager.image`, or ship icon images with the plugin. The weather plugin's `WeatherIcons` class is an example. See [Deprecated APIs](PLUGIN_API_REFERENCE.md#deprecated-apis). @@ -194,7 +194,7 @@ def update(self): sport_key = "nhl" cache_key = f"{self.plugin_id}_{sport_key}_games" - # get_background_cached_data() is deprecated, removed in 3.8.0 — use get() + # get_background_cached_data() was removed in 3.8.0 — use get() cached = self.cache_manager.get(cache_key, max_age=60) if cached: @@ -596,8 +596,8 @@ def update(self): ```python def update(self): - # get_enabled_plugins() is deprecated, removed in 3.8.0 — check the - # instance's `enabled` flag instead + # get_enabled_plugins() was removed in 3.8.0 — check the instance's + # `enabled` flag instead weather_plugin = self.plugin_manager.get_plugin("weather") if weather_plugin is not None and weather_plugin.enabled: # Use weather data diff --git a/docs/DEPRECATIONS_3.8.md b/docs/DEPRECATIONS_3.8.md index 828ef121..c07841f6 100644 --- a/docs/DEPRECATIONS_3.8.md +++ b/docs/DEPRECATIONS_3.8.md @@ -2,8 +2,8 @@ Generated by `scripts/plugin_api_usage.py` — do not edit by hand; re-run it (see [How to re-run](#how-to-re-run)). -- Scanned: 2026-09-30, core 3.7.0 -- Monorepo: [ChuckBuilds/ledmatrix-plugins](https://github.com/ChuckBuilds/ledmatrix-plugins) (main @ 4327c2e4), 46 plugins +- Scanned: 2026-10-01, core 3.7.0 +- Monorepo: [ChuckBuilds/ledmatrix-plugins](https://github.com/ChuckBuilds/ledmatrix-plugins) (main @ 4de1d134), 46 plugins - Third-party plugins: 8 with their own repo in `plugins.json` (f1-live, gif-player, pga-tour-leaderboard, plex-marquee, ledmatrix-dresden-departures, tidbyt-baseball-scoreboard, sleeper-fantasy, ledmatrix-nascar) **37 deprecated methods: 36 unused, 1 still used, 0 need review.** @@ -78,19 +78,19 @@ File paths are relative to the plugin's directory (core: the repo root). | `CacheManager.get_memory_cache_stats` | core tests | test/test_cache_manager_memory_tier.py:63 | test call | `assert cm.get_memory_cache_stats()["last_cleanup"] >= before` | | `CacheManager.get_memory_cache_stats` | core tests | test/test_cache_manager_memory_tier.py:68 | test call | `stats = cm.get_memory_cache_stats()` | | `DisplayManager.draw_sun` | core | src/plugin_system/testing/visual_display_manager.py:417 | unrelated | `def draw_sun(self, x: int, y: int, size: int = 16):` | -| `DisplayManager.draw_cloud` | core | src/display_manager.py:1356 | internal (in `DisplayManager.draw_rain`) | `self.draw_cloud(x, y, size)` | -| `DisplayManager.draw_cloud` | core | src/display_manager.py:1371 | internal (in `DisplayManager.draw_snow`) | `self.draw_cloud(x, y, size)` | +| `DisplayManager.draw_cloud` | core | src/display_manager.py:1359 | internal (in `DisplayManager.draw_rain`) | `self.draw_cloud(x, y, size)` | +| `DisplayManager.draw_cloud` | core | src/display_manager.py:1374 | internal (in `DisplayManager.draw_snow`) | `self.draw_cloud(x, y, size)` | | `DisplayManager.draw_cloud` | core | src/plugin_system/testing/visual_display_manager.py:421 | unrelated | `def draw_cloud(self, x: int, y: int, size: int = 16, color: Tuple[int, int, int] = (200, 200, 200)):` | | `DisplayManager.draw_cloud` | ledmatrix-weather | weather_icons.py:184 | unrelated | `def draw_cloud(draw: ImageDraw, x: int, y: int, size: int = 16, color: tuple = (200, 200, 200)):` | | `DisplayManager.draw_rain` | core | src/plugin_system/testing/visual_display_manager.py:425 | unrelated | `def draw_rain(self, x: int, y: int, size: int = 16):` | | `DisplayManager.draw_snow` | core | src/plugin_system/testing/visual_display_manager.py:429 | unrelated | `def draw_snow(self, x: int, y: int, size: int = 16):` | -| `DisplayManager.draw_weather_icon` | core | src/display_manager.py:1515 | internal (in `DisplayManager.draw_text_with_icons`) | `self.draw_weather_icon(icon_type, icon_x, icon_y)` | +| `DisplayManager.draw_weather_icon` | core | src/display_manager.py:1518 | internal (in `DisplayManager.draw_text_with_icons`) | `self.draw_weather_icon(icon_type, icon_x, icon_y)` | | `DisplayManager.draw_weather_icon` | core | src/plugin_system/testing/visual_display_manager.py:510 | unrelated | `def draw_weather_icon(self, condition: str, x: int, y: int, size: int = 16) -> None:` | | `DisplayManager.draw_weather_icon` | core | src/plugin_system/testing/visual_display_manager.py:533 | unrelated | `self.draw_weather_icon(icon_type, icon_x, icon_y)` | -| `DisplayManager.draw_weather_icon` | ledmatrix-weather | manager.py:76 | unrelated | `def draw_weather_icon(image, icon_code, x, y, size):` | -| `DisplayManager.draw_weather_icon` | ledmatrix-weather | manager.py:1265 | unrelated | `WeatherIcons.draw_weather_icon(img, icon_code, icon_x, icon_y,` | -| `DisplayManager.draw_weather_icon` | ledmatrix-weather | manager.py:1544 | unrelated | `WeatherIcons.draw_weather_icon(img, forecast['icon'], icon_x, icon_y, icon_size)` | -| `DisplayManager.draw_weather_icon` | ledmatrix-weather | manager.py:1635 | unrelated | `WeatherIcons.draw_weather_icon(img, forecast['icon'], icon_x, icon_y, icon_size)` | +| `DisplayManager.draw_weather_icon` | ledmatrix-weather | manager.py:84 | unrelated | `def draw_weather_icon(image, icon_code, x, y, size):` | +| `DisplayManager.draw_weather_icon` | ledmatrix-weather | manager.py:1280 | unrelated | `WeatherIcons.draw_weather_icon(img, icon_code, icon_x, icon_y,` | +| `DisplayManager.draw_weather_icon` | ledmatrix-weather | manager.py:1559 | unrelated | `WeatherIcons.draw_weather_icon(img, forecast['icon'], icon_x, icon_y, icon_size)` | +| `DisplayManager.draw_weather_icon` | ledmatrix-weather | manager.py:1650 | unrelated | `WeatherIcons.draw_weather_icon(img, forecast['icon'], icon_x, icon_y, icon_size)` | | `DisplayManager.draw_weather_icon` | ledmatrix-weather | weather_icons.py:168 | unrelated | `def draw_weather_icon(image: Image.Image, icon_code: str, x: int, y: int, size: int = DEFAULT_SIZE):` | | `DisplayManager.draw_text_with_icons` | core | src/plugin_system/testing/visual_display_manager.py:526 | unrelated | `def draw_text_with_icons(self, text: str, icons: List[tuple] = None,` | | `FontManager.get_font_catalog` | core tests | test/test_deprecation.py:229 | test call | `assert fm.get_font_catalog() == fm.font_catalog` | @@ -106,12 +106,12 @@ File paths are relative to the plugin's directory (core: the repo root). | Source | Group | Python files | Hits | |---|---|---|---| -| core | core | 164 | 20 | -| core tests | core-tests | 323 | 17 | +| core | core | 172 | 20 | +| core tests | core-tests | 347 | 17 | | 7-segment-clock | monorepo | 3 | 0 | -| afl-scoreboard | monorepo | 34 | 0 | -| baseball-scoreboard | monorepo | 60 | 0 | -| basketball-scoreboard | monorepo | 48 | 0 | +| afl-scoreboard | monorepo | 35 | 0 | +| baseball-scoreboard | monorepo | 61 | 0 | +| basketball-scoreboard | monorepo | 49 | 0 | | birdnet-go | monorepo | 2 | 0 | | blackjack | monorepo | 7 | 2 | | calendar | monorepo | 5 | 1 | @@ -121,37 +121,37 @@ File paths are relative to the plugin's directory (core: the repo root). | cricket-scoreboard | monorepo | 8 | 0 | | f1-scoreboard | monorepo | 15 | 0 | | fantasy-blitz | monorepo | 13 | 0 | -| football-scoreboard | monorepo | 73 | 0 | +| football-scoreboard | monorepo | 74 | 0 | | geochron | monorepo | 10 | 0 | | hello-world | monorepo | 2 | 0 | -| hockey-scoreboard | monorepo | 51 | 0 | +| hockey-scoreboard | monorepo | 52 | 0 | | incoming-packages | monorepo | 8 | 0 | | jellyfin-now-playing | monorepo | 4 | 0 | -| lacrosse-scoreboard | monorepo | 39 | 0 | +| lacrosse-scoreboard | monorepo | 40 | 0 | | ledmatrix-elections | monorepo | 12 | 0 | -| ledmatrix-flights | monorepo | 45 | 0 | +| ledmatrix-flights | monorepo | 48 | 0 | | ledmatrix-leaderboard | monorepo | 9 | 0 | | ledmatrix-music | monorepo | 11 | 0 | | ledmatrix-stocks | monorepo | 7 | 0 | -| ledmatrix-weather | monorepo | 14 | 6 | +| ledmatrix-weather | monorepo | 15 | 6 | | march-madness | monorepo | 4 | 0 | | masters-tournament | monorepo | 10 | 0 | | mqtt-notifications | monorepo | 4 | 0 | | news | monorepo | 6 | 0 | | nfl-draft | monorepo | 3 | 0 | | nfl-stat-leaders | monorepo | 8 | 0 | -| nrl-scoreboard | monorepo | 29 | 0 | +| nrl-scoreboard | monorepo | 30 | 0 | | odds-ticker | monorepo | 9 | 0 | | of-the-day | monorepo | 14 | 0 | | olympics | monorepo | 16 | 1 | | on-air | monorepo | 2 | 0 | | pomodoro-timer | monorepo | 3 | 0 | -| soccer-scoreboard | monorepo | 46 | 0 | +| soccer-scoreboard | monorepo | 47 | 0 | | static-image | monorepo | 3 | 0 | | stock-news | monorepo | 3 | 0 | | text-display | monorepo | 4 | 0 | | tide-display | monorepo | 3 | 0 | -| ufc-scoreboard | monorepo | 34 | 0 | +| ufc-scoreboard | monorepo | 38 | 0 | | web-ui-info | monorepo | 2 | 0 | | youtube-stats | monorepo | 5 | 0 | | f1-live | third-party | 10 | 0 | diff --git a/docs/DEVELOPER_QUICK_REFERENCE.md b/docs/DEVELOPER_QUICK_REFERENCE.md index 55092679..84bd72e9 100644 --- a/docs/DEVELOPER_QUICK_REFERENCE.md +++ b/docs/DEVELOPER_QUICK_REFERENCE.md @@ -54,8 +54,8 @@ rows = self.layout.bounds.inset(1).split_v(3, 1, gap=1) self.draw_fit("12:34", rows[0]) # largest crisp font that fits self.draw_image(logo, rows[1], mode="fill_height", crop_to_ink=True) -# Weather icons: draw_weather_icon() is deprecated, removed in 3.8.0 — -# draw your own icons (the weather plugin ships WeatherIcons) +# Weather icons: draw_weather_icon() was removed in 3.8.0 — draw your +# own icons (the weather plugin ships WeatherIcons) # Scrolling state display_manager.set_scrolling_state(True) @@ -78,7 +78,7 @@ strategy = cache_manager.get_cache_strategy("weather") ``` `get_background_cached_data()` (use `get()`) and `get_sport_live_interval()` -are deprecated, removed in 3.8.0. See +were removed in 3.8.0. See [Deprecated APIs](PLUGIN_API_REFERENCE.md#deprecated-apis). ## Plugin Manager Quick Methods @@ -87,8 +87,8 @@ are deprecated, removed in 3.8.0. See # Get plugins plugin = plugin_manager.get_plugin("plugin-id") all_plugins = plugin_manager.get_all_plugins() -# get_enabled_plugins() is deprecated, removed in 3.8.0 — check `enabled` -# on the entries in plugin_manager.plugins +# get_enabled_plugins() was removed in 3.8.0 — check `enabled` on the +# entries in plugin_manager.plugins # Get info info = plugin_manager.get_plugin_info("plugin-id") diff --git a/docs/FONT_MANAGER.md b/docs/FONT_MANAGER.md index efea04dc..6d2dc61f 100644 --- a/docs/FONT_MANAGER.md +++ b/docs/FONT_MANAGER.md @@ -13,10 +13,9 @@ BDF fonts in `assets/fonts/`, registers fonts that plugins ship, and records which plugin uses which font so the web UI can show it. -Several methods are deprecated and will be removed in LEDMatrix 3.8.0; they -log a warning on first call. They are listed in -[Deprecated methods](#deprecated-methods) below, and the full set is pinned in -[`test/test_deprecation.py`](../test/test_deprecation.py). +Several methods were removed in LEDMatrix 3.8.0 after a release of +deprecation warnings; [Removed methods](#removed-methods) below lists them +with what to use instead. ## Getting the FontManager @@ -128,8 +127,8 @@ font = self.font_manager.resolve_font( `resolve_font()` still honours `config/font_overrides.json` (a map of element key to `family` and/or `size_px`), which is read once at start-up. -The methods that edit it — `set_override()`, `remove_override()`, -`get_overrides()` — are deprecated, and there is no web UI or REST endpoint +The methods that edited it — `set_override()`, `remove_override()`, +`get_overrides()` — were removed in 3.8.0, and there is no web UI or REST endpoint for overrides (the override editor and `/api/v3/fonts/overrides` were removed). To let users choose a font, add a field to your plugin's config schema. @@ -207,9 +206,10 @@ Current methods: | `clear_cache()` | Drop cached fonts and metrics | | `font_catalog` (attribute) | Family name → file path | -### Deprecated methods +### Removed methods -Removed in 3.8.0. Each logs a warning on first call. +Removed in 3.8.0, after logging a deprecation warning on first call since +3.5.0. | Method | Use instead | |---|---| diff --git a/docs/PLUGIN_API_REFERENCE.md b/docs/PLUGIN_API_REFERENCE.md index f97c3a6d..f9f517b3 100644 --- a/docs/PLUGIN_API_REFERENCE.md +++ b/docs/PLUGIN_API_REFERENCE.md @@ -628,18 +628,6 @@ self.display_manager.update_display() This is the canonical way to render arbitrary images. -### Weather Icons (deprecated) - -> Deprecated, removed in 3.8.0 — draw your own icons (the weather plugin -> ships `WeatherIcons`). See [Deprecated APIs](#deprecated-apis). - -- `draw_weather_icon(condition, x, y, size=16)` — icon for a condition - string such as `"clear"`, `"clouds"`, `"rain"`, `"snow"`, `"storm"` -- `draw_sun(x, y, size=16)`, `draw_cloud(x, y, size=16, color=(200, 200, 200))`, - `draw_rain(x, y, size=16)`, `draw_snow(x, y, size=16)` -- `draw_text_with_icons(text, icons=None, x=None, y=None, color=(255, 255, 255))` - — text plus a list of `(icon_type, x, y)` icons; calls `update_display()` - ### Scrolling State Management For plugins that implement scrolling content, use these methods to coordinate with the display system. @@ -730,20 +718,6 @@ Process any deferred updates if not currently scrolling. Called automatically by **Note**: Plugins typically don't need to call this directly. -#### `get_scrolling_stats() -> dict` - -> Deprecated, removed in 3.8.0. See [Deprecated APIs](#deprecated-apis). - -Get current scrolling statistics for debugging. - -**Returns**: Dictionary with scrolling state information - -**Example**: -```python -stats = self.display_manager.get_scrolling_stats() -self.logger.debug(f"Scrolling: {stats['is_scrolling']}, Deferred: {stats['deferred_count']}") -``` - ### Available Fonts The Display Manager provides several pre-loaded fonts: @@ -873,27 +847,6 @@ Get data with automatic strategy detection from cache key. data = self.cache_manager.get_with_auto_strategy("nhl_live_scores") ``` -#### `get_background_cached_data(key: str, sport_key: Optional[str] = None) -> Optional[Dict[str, Any]]` - -> Deprecated, removed in 3.8.0 — use `get()`. See [Deprecated APIs](#deprecated-apis). - -Get background service cached data with sport-specific intervals. - -**Parameters**: -- `key` (str): Cache key -- `sport_key` (str, optional): Sport identifier (e.g., 'nhl', 'nba') for live interval lookup - -**Returns**: Cached data, or `None` if not found or stale - -**Example**: -```python -# Uses sport-specific live_update_interval from config -games = self.cache_manager.get_background_cached_data( - "nhl_games", - sport_key="nhl" -) -``` - ### Strategy Methods #### `get_cache_strategy(data_type: str, sport_key: Optional[str] = None) -> Dict[str, Any]` @@ -912,23 +865,6 @@ strategy = self.cache_manager.get_cache_strategy("sports_live", sport_key="nhl") max_age = strategy['max_age'] # Get configured max age ``` -#### `get_sport_live_interval(sport_key: str) -> int` - -> Deprecated, removed in 3.8.0. See [Deprecated APIs](#deprecated-apis). - -Get the live_update_interval for a specific sport from config. - -**Parameters**: -- `sport_key` (str): Sport identifier (e.g., 'nhl', 'nba') - -**Returns**: Live update interval in seconds - -**Example**: -```python -interval = self.cache_manager.get_sport_live_interval("nhl") -# Returns configured live_update_interval for NHL -``` - #### `get_data_type_from_key(key: str) -> str` Extract data type from cache key to determine appropriate cache strategy. @@ -938,17 +874,6 @@ Extract data type from cache key to determine appropriate cache strategy. **Returns**: Inferred data type string -#### `get_sport_key_from_cache_key(key: str) -> Optional[str]` - -> Deprecated, removed in 3.8.0. See [Deprecated APIs](#deprecated-apis). - -Extract sport key from cache key for sport-specific strategies. - -**Parameters**: -- `key` (str): Cache key - -**Returns**: Sport identifier, or `None` if not found - ### Utility Methods #### `clear_cache(key: Optional[str] = None) -> None` @@ -986,30 +911,6 @@ for file_info in files: self.logger.info(f"Cache: {file_info['key']}, Age: {file_info['age_display']}") ``` -### Metrics Methods (deprecated) - -#### `get_cache_metrics() -> Dict[str, Any]` - -> Deprecated, removed in 3.8.0. See [Deprecated APIs](#deprecated-apis). - -Get cache performance metrics. - -**Returns**: Dictionary with cache statistics (`total_requests`, `cache_hit_rate`, `background_hit_rate`, `api_calls_saved`, `average_fetch_time`, etc.) - -**Example**: -```python -metrics = self.cache_manager.get_cache_metrics() -self.logger.info(f"Cache hit rate: {metrics['cache_hit_rate']:.2%}") -``` - -#### `get_memory_cache_stats() -> Dict[str, Any]` - -> Deprecated, removed in 3.8.0. See [Deprecated APIs](#deprecated-apis). - -Get memory cache statistics. - -**Returns**: Dictionary with memory cache stats (size, max_size, etc.) - --- ## Plugin Manager @@ -1048,14 +949,6 @@ for plugin_id, plugin in all_plugins.items(): self.logger.info(f"Plugin {plugin_id} is loaded") ``` -#### `get_enabled_plugins() -> List[str]` - -> Deprecated, removed in 3.8.0 — check `enabled` on the instances in `plugin_manager.plugins`. See [Deprecated APIs](#deprecated-apis). - -Get list of enabled plugin IDs. - -**Returns**: List of plugin identifier strings - #### `get_plugin_info(plugin_id: str) -> Optional[Dict[str, Any]]` Get plugin information including manifest and runtime info. @@ -1221,13 +1114,19 @@ if weather is not None and weather.enabled: ## Deprecated APIs -These still work but log a warning the first time they are called -(`journalctl -u ledmatrix` shows which one), and are **removed in 3.8.0** -(first announced for 3.7.0, which shipped with them still in place). -[DEPRECATIONS_3.8.md](DEPRECATIONS_3.8.md) is the usage scan behind that -decision: which of these the official plugins, the registry's third-party -plugins and core still call or override. Only methods that scan reports unused -are removed in 3.8.0; the rest stay until their callers migrate. +A deprecated method still works but logs a warning the first time it is +called (`journalctl -u ledmatrix` shows which one), until the release that +removes it. [DEPRECATIONS_3.8.md](DEPRECATIONS_3.8.md) is the usage scan +behind each removal: which of the deprecated methods the official plugins, +the registry's third-party plugins and core still call or override. Only +methods that scan reports unused are removed; the rest stay until their +callers migrate. + +### Removed in 3.8.0 + +Deprecated in 3.5.0 with a warning on first call, and gone in 3.8.0: +the scan found no caller in any official or third-party plugin. Calling one +now raises `AttributeError`. | Object | Methods | Instead | |---|---|---| diff --git a/docs/PLUGIN_DEVELOPMENT_GUIDE.md b/docs/PLUGIN_DEVELOPMENT_GUIDE.md index 67c4d63a..25f5d561 100644 --- a/docs/PLUGIN_DEVELOPMENT_GUIDE.md +++ b/docs/PLUGIN_DEVELOPMENT_GUIDE.md @@ -519,15 +519,12 @@ When developing plugins, you'll need to use the APIs provided by the LEDMatrix s - `draw_text()` - Text rendering. For images, paste directly onto `display_manager.image` (a PIL Image) and call `update_display()`; there is no `draw_image()` helper method. -- `draw_weather_icon()`, `draw_sun()`, `draw_cloud()` - Weather icons - (deprecated, removed in 3.8.0 — draw your own icons) - `get_text_width()`, `get_font_height()` - Text utilities - `set_scrolling_state()`, `defer_update()` - Scrolling state management **Cache Manager** (`self.cache_manager`): - `get()`, `set()`, `delete()` - Basic caching - `get_cached_data_with_strategy()` - Advanced caching with strategies -- `get_background_cached_data()` - deprecated, removed in 3.8.0 — use `get()` **Plugin Manager** (`self.plugin_manager`): - `get_plugin()`, `get_all_plugins()` - Access other plugins diff --git a/src/cache_manager.py b/src/cache_manager.py index 4068d50a..48dfb726 100644 --- a/src/cache_manager.py +++ b/src/cache_manager.py @@ -37,7 +37,6 @@ 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 @@ -408,122 +407,6 @@ class CacheManager: """Get the cache directory path.""" return self.cache_dir - @deprecated("3.8.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) - if not cached_data: - return True - - if data_type == 'weather': - return self._has_weather_changed(cached_data, new_data) - elif data_type == 'stocks': - return self._has_stocks_changed(cached_data, new_data) - elif data_type == 'stock_news': - return self._has_news_changed(cached_data, new_data) - elif data_type == 'nhl': - return self._has_nhl_changed(cached_data, new_data) - elif data_type == 'mlb': - return self._has_mlb_changed(cached_data, new_data) - - return True - - def _has_weather_changed(self, cached: Dict[str, Any], new: Dict[str, Any]) -> bool: - """Check if weather data has changed.""" - # Handle new cache structure where data is nested under 'data' key - if 'data' in cached: - cached = cached['data'] - - # Handle case where cached data might be the weather data directly - if 'current' in cached: - # This is the new structure with 'current' and 'forecast' keys - current_weather = cached.get('current', {}) - if current_weather and 'main' in current_weather and 'weather' in current_weather: - cached_temp = round(current_weather['main']['temp']) - cached_condition = current_weather['weather'][0]['main'] - return (cached_temp != new.get('temp') or - cached_condition != new.get('condition')) - - # Handle old structure where temp and condition are directly accessible - return (cached.get('temp') != new.get('temp') or - cached.get('condition') != new.get('condition')) - - def _has_stocks_changed(self, cached: Dict[str, Any], new: Dict[str, Any]) -> bool: - """Check if stock data has changed.""" - if not self._is_market_open(): - return False - return cached.get('price') != new.get('price') - - def _has_news_changed(self, cached: Dict[str, Any], new: Dict[str, Any]) -> bool: - """Check if news data has changed.""" - # Handle both dictionary and list formats - if isinstance(new, list): - # If new data is a list, cached data should also be a list - if not isinstance(cached, list): - return True - # Compare lengths and content - if len(cached) != len(new): - return True - # Compare titles since they're unique enough for our purposes - cached_titles = set(item.get('title', '') for item in cached) - new_titles = set(item.get('title', '') for item in new) - return cached_titles != new_titles - else: - # Original dictionary format handling - cached_headlines = set(h.get('id') for h in cached.get('headlines', [])) - new_headlines = set(h.get('id') for h in new.get('headlines', [])) - return not cached_headlines.issuperset(new_headlines) - - def _has_nhl_changed(self, cached: Dict[str, Any], new: Dict[str, Any]) -> bool: - """Check if NHL data has changed.""" - return (cached.get('game_status') != new.get('game_status') or - cached.get('score') != new.get('score')) - - def _has_mlb_changed(self, cached: Dict[str, Any], new: Dict[str, Any]) -> bool: - """Check if MLB game data has changed.""" - if not cached or not new: - return True - - # Check if any games have changed status or score - for game_id, new_game in new.items(): - cached_game = cached.get(game_id) - if not cached_game: - return True - - # Check for score changes - if (new_game['away_score'] != cached_game['away_score'] or - new_game['home_score'] != cached_game['home_score']): - return True - - # Check for status changes - if new_game['status'] != cached_game['status']: - return True - - # For live games, check inning and count - if new_game['status'] == 'in': - if (new_game['inning'] != cached_game['inning'] or - new_game['inning_half'] != cached_game['inning_half'] or - new_game['balls'] != cached_game['balls'] or - new_game['strikes'] != cached_game['strikes'] or - new_game['bases_occupied'] != cached_game['bases_occupied']): - return True - - return False - - def _is_market_open(self) -> bool: - """Check if the US stock market is currently open.""" - return self._strategy_component.is_market_open() - - @deprecated("3.8.0", "use set()") - def update_cache(self, data_type: str, data: Dict[str, Any]) -> bool: - """Update cache with new data.""" - cache_data = { - # Header first; see DiskCache's stale check. - 'timestamp': time.time(), - 'data': data, - } - return self.save_cache(data_type, cache_data) - def get(self, key: str, max_age: Optional[int] = 300, memory_ttl: Optional[int] = None) -> Optional[Dict[str, Any]]: """Get data from cache if it exists and is not stale. @@ -564,42 +447,6 @@ class CacheManager: cache_data['data'] = data self.save_cache(key, cache_data) - @deprecated("3.8.0") - def setup_persistent_cache(self) -> bool: - """ - Set up a persistent cache directory with proper permissions. - This should be run once with sudo to create the directory. - """ - try: - # Try to create /var/cache/ledmatrix with proper permissions - from pathlib import Path - from src.common.permission_utils import ( - ensure_directory_permissions, - get_cache_dir_mode - ) - cache_dir = '/var/cache/ledmatrix' - cache_dir_path = Path(cache_dir) - ensure_directory_permissions(cache_dir_path, get_cache_dir_mode()) - - # Set ownership to the real user (not root) - real_user = os.environ.get('SUDO_USER') - if real_user: - import pwd - try: - uid = pwd.getpwnam(real_user).pw_uid - gid = pwd.getpwnam(real_user).pw_gid - os.chown(cache_dir, uid, gid) - self.logger.info(f"Set ownership of {cache_dir} to {real_user}") - except (OSError, KeyError) as e: - self.logger.warning(f"Could not set ownership for {cache_dir}: {e}", exc_info=True) - - self.logger.info(f"Successfully set up persistent cache directory: {cache_dir}") - return True - - except (OSError, IOError, PermissionError) as e: - self.logger.error(f"Failed to set up persistent cache directory {cache_dir}: {e}", exc_info=True) - return False - def cleanup_disk_cache(self, force: bool = False) -> Dict[str, Any]: """ Clean up expired disk cache files based on retention policies. @@ -776,14 +623,6 @@ class CacheManager: else: self.logger.info("Disk cache cleanup thread stopped successfully") - @deprecated("3.8.0") - def get_sport_live_interval(self, sport_key: str) -> int: - """ - Get the live_update_interval for a specific sport from config. - Falls back to default values if config is not available. - """ - return self._strategy_component.get_sport_live_interval(sport_key) - def get_cache_strategy(self, data_type: str, sport_key: Optional[str] = None) -> Dict[str, Any]: """ Get cache strategy for different data types. @@ -798,13 +637,6 @@ class CacheManager: """ return self._strategy_component.get_data_type_from_key(key) - @deprecated("3.8.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. - """ - return self._strategy_component.get_sport_key_from_cache_key(key) - def get_cached_data_with_strategy(self, key: str, data_type: str = 'default') -> Optional[Dict[str, Any]]: """ Get data from cache using data-type-specific strategy. @@ -838,58 +670,6 @@ class CacheManager: data_type = self.get_data_type_from_key(key) return self.get_cached_data_with_strategy(key, data_type) - @deprecated("3.8.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. - This method is specifically designed for Recent/Upcoming managers - to use data cached by the background service. - - Args: - key: Cache key to retrieve - sport_key: Sport key for determining appropriate cache strategy - - Returns: - Cached data if available and fresh, None otherwise - """ - # Determine the appropriate cache strategy - data_type = self.get_data_type_from_key(key) - strategy = self.get_cache_strategy(data_type, sport_key) - - # For Recent/Upcoming managers, we want to use the background service cache - # which should have longer TTLs than the individual manager caches - max_age = strategy['max_age'] - memory_ttl = strategy.get('memory_ttl', max_age) - - # Get the cached data - cached_data = self.get_cached_data(key, max_age, memory_ttl) - - if cached_data: - # Record cache hit for performance monitoring - self.record_cache_hit('background') - # Unwrap if stored in { 'data': ..., 'timestamp': ... } format - if isinstance(cached_data, dict) and 'data' in cached_data: - return cached_data['data'] - return cached_data - - # Record cache miss for performance monitoring - self.record_cache_miss('background') - return None - - @deprecated("3.8.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. - This helps Recent/Upcoming managers determine if they should - wait for background data or fetch immediately. - """ - data_type = self.get_data_type_from_key(key) - strategy = self.get_cache_strategy(data_type, sport_key) - - # Check if we have data that's still fresh according to background service TTL - cached_data = self.get_cached_data(key, strategy['max_age']) - return cached_data is not None - def generate_sport_cache_key(self, sport: str, date_str: Optional[str] = None) -> str: """ Centralized cache key generation for sports data. @@ -906,45 +686,8 @@ class CacheManager: date_str = datetime.now(pytz.utc).strftime('%Y%m%d') return f"{sport}_{date_str}" - @deprecated("3.8.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.8.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.8.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.8.0") - def get_cache_metrics(self) -> Dict[str, Any]: - """Get current cache performance metrics.""" - return self._metrics_component.get_metrics() - - @deprecated("3.8.0") - def log_cache_metrics(self) -> None: - """Log current cache performance metrics.""" - self._metrics_component.log_metrics() - - @deprecated("3.8.0") - def get_memory_cache_stats(self) -> Dict[str, Any]: - """ - Get statistics about the memory cache. - - Returns: - Dictionary with memory cache statistics - """ - return self._memory_cache_component.get_stats() - def log_memory_cache_stats(self) -> None: """Log current memory cache statistics.""" - # Not get_memory_cache_stats(): that is deprecated, and core must not - # trip its own deprecation warning every time memory logging runs. stats = self._memory_cache_component.get_stats() self.logger.info(f"Memory Cache - Size: {stats['size']}/{stats['max_size']} " f"({stats['usage_percent']:.1f}%), " diff --git a/src/display_manager.py b/src/display_manager.py index 4b910ef9..cb0eae7e 100644 --- a/src/display_manager.py +++ b/src/display_manager.py @@ -51,8 +51,7 @@ from src.pi5_matrix_support import is_raspberry_pi_5 import threading import time from collections import OrderedDict, deque -from typing import Dict, Any, List, Optional, Tuple, TYPE_CHECKING -import math +from typing import Dict, Any, Optional, Tuple, TYPE_CHECKING import zlib import freetype @@ -62,7 +61,6 @@ from src.common.frame_timing import FrameTimingRecorder if TYPE_CHECKING: from src.common.render_gate import RenderGate -from src.deprecation import deprecated from src.logging_config import get_logger from src.common.permission_utils import ( ensure_directory_permissions, @@ -1323,203 +1321,6 @@ class DisplayManager: except Exception as e: logger.error(f"Error drawing text: {e}", exc_info=True) - @deprecated("3.8.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) - radius = size//3 - - # Draw the center circle - self.draw.ellipse([center[0]-radius, center[1]-radius, - center[0]+radius, center[1]+radius], - fill=(255, 255, 0)) # Yellow - - # Draw the rays - ray_length = size//4 - for angle in range(0, 360, 45): - rad = math.radians(angle) - start_x = center[0] + (radius * math.cos(rad)) - start_y = center[1] + (radius * math.sin(rad)) - end_x = center[0] + ((radius + ray_length) * math.cos(rad)) - 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.8.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 - self.draw.ellipse([x+size//4, y+size//3, x+size//4+size//2, y+size//3+size//2], fill=color) - 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.8.0") - def draw_rain(self, x: int, y: int, size: int = 16): - """Draw rain icon with cloud and droplets.""" - # Draw cloud - self.draw_cloud(x, y, size) - - # Draw rain drops - drop_color = (0, 0, 255) # Blue - drop_size = size//6 - for i in range(3): - drop_x = x + size//4 + (i * size//3) - drop_y = y + size//2 - self.draw.line([drop_x, drop_y, drop_x, drop_y+drop_size], - fill=drop_color, width=2) - - @deprecated("3.8.0") - def draw_snow(self, x: int, y: int, size: int = 16): - """Draw snow icon with cloud and snowflakes.""" - # Draw cloud - self.draw_cloud(x, y, size) - - # Draw snowflakes - snow_color = (200, 200, 255) # Light blue - for i in range(3): - center_x = x + size//4 + (i * size//3) - center_y = y + size//2 + size//4 - # Draw a small star shape - for angle in range(0, 360, 60): - rad = math.radians(angle) - end_x = center_x + (size//8 * math.cos(rad)) - end_y = center_y + (size//8 * math.sin(rad)) - self.draw.line([center_x, center_y, end_x, end_y], - fill=snow_color, width=1) - - # Weather icon color constants - WEATHER_COLORS = { - 'sun': (255, 200, 0), # Bright yellow - 'cloud': (200, 200, 200), # Light gray - 'rain': (0, 100, 255), # Light blue - 'snow': (220, 220, 255), # Ice blue - 'storm': (255, 255, 0) # Lightning yellow - } - - def _draw_sun(self, x: int, y: int, size: int) -> None: - """Draw a sun icon with rays.""" - center_x, center_y = x + size//2, y + size//2 - radius = size//4 - ray_length = size//3 - - # Draw the main sun circle - self.draw.ellipse([center_x - radius, center_y - radius, - center_x + radius, center_y + radius], - fill=self.WEATHER_COLORS['sun']) - - # Draw sun rays - for angle in range(0, 360, 45): - rad = math.radians(angle) - start_x = center_x + int((radius + 2) * math.cos(rad)) - start_y = center_y + int((radius + 2) * math.sin(rad)) - end_x = center_x + int((radius + ray_length) * math.cos(rad)) - end_y = center_y + int((radius + ray_length) * math.sin(rad)) - self.draw.line([start_x, start_y, end_x, end_y], - fill=self.WEATHER_COLORS['sun'], width=2) - - def _draw_cloud(self, x: int, y: int, size: int) -> None: - """Draw a cloud using multiple circles.""" - cloud_color = self.WEATHER_COLORS['cloud'] - base_y = y + size//2 - - # Draw main cloud body (3 overlapping circles) - circle_radius = size//4 - positions = [ - (x + size//3, base_y), # Left circle - (x + size//2, base_y - size//6), # Top circle - (x + 2*size//3, base_y) # Right circle - ] - - for cx, cy in positions: - self.draw.ellipse([cx - circle_radius, cy - circle_radius, - cx + circle_radius, cy + circle_radius], - fill=cloud_color) - - def _draw_rain(self, x: int, y: int, size: int) -> None: - """Draw rain drops falling from a cloud.""" - self._draw_cloud(x, y, size) - rain_color = self.WEATHER_COLORS['rain'] - - # Draw rain drops at an angle - drop_size = size//8 - drops = [ - (x + size//4, y + 2*size//3), - (x + size//2, y + 3*size//4), - (x + 3*size//4, y + 2*size//3) - ] - - for dx, dy in drops: - # Draw angled rain drops - self.draw.line([dx, dy, dx - drop_size//2, dy + drop_size], - fill=rain_color, width=2) - - def _draw_snow(self, x: int, y: int, size: int) -> None: - """Draw snowflakes falling from a cloud.""" - self._draw_cloud(x, y, size) - snow_color = self.WEATHER_COLORS['snow'] - - # Draw snowflakes - flake_size = size//6 - flakes = [ - (x + size//4, y + 2*size//3), - (x + size//2, y + 3*size//4), - (x + 3*size//4, y + 2*size//3) - ] - - for fx, fy in flakes: - # Draw a snowflake (six-pointed star) - for angle in range(0, 360, 60): - rad = math.radians(angle) - end_x = fx + int(flake_size * math.cos(rad)) - end_y = fy + int(flake_size * math.sin(rad)) - self.draw.line([fx, fy, end_x, end_y], - fill=snow_color, width=1) - - def _draw_storm(self, x: int, y: int, size: int) -> None: - """Draw a storm cloud with lightning bolt.""" - self._draw_cloud(x, y, size) - - # Draw lightning bolt - bolt_color = self.WEATHER_COLORS['storm'] - bolt_points = [ - (x + size//2, y + size//2), # Top - (x + 3*size//5, y + 2*size//3), # Middle right - (x + 2*size//5, y + 2*size//3), # Middle left - (x + size//2, y + 5*size//6) # Bottom - ] - self.draw.polygon(bolt_points, fill=bolt_color) - - @deprecated("3.8.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']: - self._draw_sun(x, y, size) - elif condition.lower() in ['clouds', 'cloudy', 'partly cloudy']: - self._draw_cloud(x, y, size) - elif condition.lower() in ['rain', 'drizzle', 'shower']: - self._draw_rain(x, y, size) - elif condition.lower() in ['snow', 'sleet', 'hail']: - self._draw_snow(x, y, size) - elif condition.lower() in ['thunderstorm', 'storm']: - self._draw_storm(x, y, size) - else: - self._draw_sun(x, y, size) - # Note: No update_display() here - let the caller handle the update - - @deprecated("3.8.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.""" - # Draw the text - self.draw_text(text, x, y, color) - - # Draw any icons - if icons: - for icon_type, icon_x, icon_y in icons: - self.draw_weather_icon(icon_type, icon_x, icon_y) - - # Update the display once after everything is drawn - self.update_display() - def cleanup(self): """Clean up resources.""" if hasattr(self, '_snapshot_cond'): @@ -1831,18 +1632,6 @@ class DisplayManager: if removed_count > 0: logger.debug(f"Cleaned up {removed_count} expired deferred updates") - @deprecated("3.8.0") - def get_scrolling_stats(self) -> dict: - """Get current scrolling statistics for debugging.""" - return { - 'is_scrolling': self._scrolling_state['is_scrolling'], - 'last_activity': self._scrolling_state['last_scroll_activity'], - 'deferred_count': len(self._scrolling_state['deferred_updates']), - 'inactivity_threshold': self._scrolling_state['scroll_inactivity_threshold'], - 'max_deferred_updates': self._scrolling_state['max_deferred_updates'], - 'deferred_update_ttl': self._scrolling_state['deferred_update_ttl'] - } - def _viewer_is_fresh(self, now: float) -> bool: """True when a browser preview is watching (marker file touched by the web SSE broadcaster). The marker is stat'd at most once per diff --git a/src/font_manager.py b/src/font_manager.py index 80ecd6a4..c722c712 100644 --- a/src/font_manager.py +++ b/src/font_manager.py @@ -40,13 +40,7 @@ from pathlib import Path from PIL import ImageFont from src.common.bdf_font import load_bdf_face, read_bdf_native_size from src.common.font_layout import load_truetype, resolve_asset_path -from src.common.permission_utils import ( - ensure_directory_permissions, - get_assets_dir_mode, - get_config_dir_mode, -) -from typing import Dict, Tuple, Optional, Union, Any, List -from src.deprecation import deprecated +from typing import Dict, Tuple, Optional, Union, Any logger = logging.getLogger(__name__) @@ -93,7 +87,7 @@ class FontManager: self.temp_font_dir = Path(tempfile.gettempdir()) / "ledmatrix_fonts" self.temp_font_dir.mkdir(exist_ok=True) - # Counters behind get_performance_stats(). + # Font-load counters, kept up by get_font(). self.performance_stats = { "cache_hits": 0, "cache_misses": 0, @@ -109,12 +103,8 @@ class FontManager: "tom_thumb": "assets/fonts/tom-thumb.bdf" } - # Size tokens for convenience - self.size_tokens = { - "xs": 6, "sm": 8, "md": 10, "lg": 12, "xl": 14, "xxl": 16 - } - - # Font overrides storage (for manual overrides) + # Per-element overrides read from config/font_overrides.json; + # resolve_font applies them. # Under the install root's config/ (which always exists), not the # cwd: the file itself may not exist yet, and resolve_asset_path # hands back a missing path unchanged. @@ -187,26 +177,6 @@ class FontManager: if removed: self.manager_fonts_version += 1 - @deprecated("3.8.0") - def get_manager_fonts(self, manager_id: Optional[str] = None) -> Dict[str, Any]: - """ - Get registered fonts for a specific manager or all managers. - - Args: - manager_id: Optional manager ID, if None returns all - - Returns: - Dictionary of registered fonts - """ - if manager_id: - return self.manager_fonts.get(manager_id, {}) - return self.manager_fonts.copy() - - @deprecated("3.8.0") - def get_detected_fonts(self) -> Dict[str, Dict[str, Any]]: - """Get all detected font usage across managers.""" - return self.detected_fonts.copy() - # ==================== Plugin Font Management ==================== def register_plugin_fonts(self, plugin_id: str, font_manifest: Dict[str, Any], @@ -433,51 +403,6 @@ class FontManager: search_dirs = [Path(resolve_asset_path(configured)), Path(resolve_asset_path("plugins"))] return resolve_plugin_dir(plugin_id, search_dirs, prefix=True) - @deprecated("3.8.0") - def unregister_plugin_fonts(self, plugin_id: str) -> bool: - """Unregister all fonts for a plugin.""" - try: - if plugin_id in self.plugin_fonts: - # Remove from plugin catalogs - if plugin_id in self.plugin_font_catalogs: - for family in self.plugin_font_catalogs[plugin_id]: - namespaced_family = f"{plugin_id}::{family}" - if namespaced_family in self.font_catalog: - del self.font_catalog[namespaced_family] - - del self.plugin_font_catalogs[plugin_id] - - # Remove plugin manifest - del self.plugin_fonts[plugin_id] - - # Clear related cache entries - self._clear_plugin_font_cache(plugin_id) - - logger.info(f"Unregistered fonts for plugin {plugin_id}") - return True - - return False - - except Exception as e: - logger.error(f"Error unregistering plugin fonts: {e}") - return False - - def _clear_plugin_font_cache(self, plugin_id: str): - """Clear font cache entries for a specific plugin.""" - keys_to_remove = [key for key in self.font_cache.keys() if key.startswith(f"{plugin_id}::")] - for key in keys_to_remove: - del self.font_cache[key] - if keys_to_remove: - # Font objects someone may hold were dropped; see cache_generation. - self.cache_generation += 1 - - @deprecated("3.8.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: - return list(self.plugin_font_catalogs[plugin_id].keys()) - return [] - # ==================== Font Resolution ==================== def resolve_font(self, element_key: str, family: str, size_px: int, @@ -668,42 +593,6 @@ class FontManager: logger.error(f"Error getting font height: {e}", exc_info=True) return 12 # Default height - # ==================== Override Management ==================== - - @deprecated("3.8.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: - self.font_overrides[element_key] = {} - - if family is not None: - self.font_overrides[element_key]["family"] = family - if size_px is not None: - self.font_overrides[element_key]["size_px"] = size_px - - # Remove empty overrides - if not self.font_overrides[element_key]: - del self.font_overrides[element_key] - else: - self._save_overrides() - - self.clear_cache() - logger.info(f"Font override set for {element_key}: {self.font_overrides.get(element_key, {})}") - - @deprecated("3.8.0") - def remove_override(self, element_key: str): - """Remove font override for a specific element.""" - if element_key in self.font_overrides: - del self.font_overrides[element_key] - self._save_overrides() - self.clear_cache() - logger.info(f"Font override removed for {element_key}") - - @deprecated("3.8.0") - def get_overrides(self) -> Dict[str, Dict[str, str]]: - """Get current font overrides.""" - return self.font_overrides.copy() - # ==================== Font Discovery ==================== @staticmethod @@ -765,17 +654,6 @@ class FontManager: logger.warning(f"Could not load font overrides: {e}") self.font_overrides = {} - def _save_overrides(self): - """Save current font overrides to file.""" - try: - font_overrides_path = Path(self.font_overrides_file) - ensure_directory_permissions(font_overrides_path.parent, get_config_dir_mode()) - with open(self.font_overrides_file, 'w') as f: - json.dump(self.font_overrides, f, indent=2) - logger.info(f"Saved {len(self.font_overrides)} font overrides") - except Exception as e: - logger.error(f"Could not save font overrides: {e}") - # ==================== Utility Methods ==================== def clear_cache(self): @@ -786,117 +664,3 @@ class FontManager: # without the bump they kept serving results for the dropped fonts. self.cache_generation += 1 logger.info("Font cache cleared") - - @deprecated("3.8.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.8.0") - def get_size_tokens(self) -> Dict[str, int]: - """Get available size tokens.""" - return self.size_tokens.copy() - - @deprecated("3.8.0") - def get_performance_stats(self) -> Dict[str, Any]: - """Get performance statistics.""" - uptime = time.time() - self.performance_stats["start_time"] - return { - "uptime_seconds": uptime, - "cache_hits": self.performance_stats["cache_hits"], - "cache_misses": self.performance_stats["cache_misses"], - "cache_hit_rate": ( - self.performance_stats["cache_hits"] / - (self.performance_stats["cache_hits"] + self.performance_stats["cache_misses"]) - if (self.performance_stats["cache_hits"] + self.performance_stats["cache_misses"]) > 0 else 0 - ), - "total_fonts_cached": len(self.font_cache), - "total_metrics_cached": len(self.metrics_cache), - "failed_loads": self.performance_stats["failed_loads"], - "total_fonts_available": len(self.font_catalog), - "plugin_fonts": len(self.plugin_fonts), - "manager_fonts": len(self.manager_fonts), - "detected_fonts": len(self.detected_fonts) - } - - @deprecated("3.8.0", "read font_catalog") - def get_font_catalog(self) -> Dict[str, str]: - """Get the current font catalog.""" - return self.font_catalog.copy() - - @deprecated("3.8.0") - def add_font(self, font_file_path: str, family_name: str) -> bool: - """Add ``font_file_path`` to the catalog as ``family_name``. The file - stays where it is; only assets/fonts is created if it is missing.""" - try: - # Validate font file - if not os.path.exists(font_file_path): - logger.error(f"Font file not found: {font_file_path}") - return False - - # Check if family name already exists - if family_name in self.font_catalog: - logger.warning(f"Font family '{family_name}' already exists") - return False - - fonts_dir = Path(resolve_asset_path("assets/fonts")) - ensure_directory_permissions(fonts_dir, get_assets_dir_mode()) - - # Add to catalog - self.font_catalog[family_name] = font_file_path - self.clear_cache() - logger.info(f"Added font {family_name}: {font_file_path}") - return True - - except Exception as e: - logger.error(f"Error adding font {family_name}: {e}") - return False - - @deprecated("3.8.0") - def remove_font(self, family_name: str) -> bool: - """Remove a font from the catalog.""" - try: - if family_name not in self.font_catalog: - logger.warning(f"Font family '{family_name}' not found") - return False - - # Check if font is currently in use - in_use = False - for override in self.font_overrides.values(): - if override.get("family") == family_name: - in_use = True - break - - if in_use: - logger.error(f"Cannot remove font '{family_name}' - it is currently in use") - return False - - del self.font_catalog[family_name] - self.clear_cache() - logger.info(f"Removed font {family_name}") - return True - - except Exception as e: - logger.error(f"Error removing font {family_name}: {e}") - return False - - @deprecated("3.8.0") - def validate_font(self, font_path: str) -> Dict[str, Any]: - """Validate a font file.""" - try: - if not os.path.exists(font_path): - return {"valid": False, "error": "Font file not found"} - - if font_path.endswith('.bdf'): - # Try to load BDF font - freetype.Face(font_path) - return {"valid": True, "type": "bdf", "family": "unknown"} - elif font_path.endswith('.ttf'): - # Try to load TTF font - load_truetype(font_path, 12) - return {"valid": True, "type": "ttf", "family": "unknown"} - else: - return {"valid": False, "error": "Unsupported font format"} - - except Exception as e: - return {"valid": False, "error": str(e)} diff --git a/src/plugin_system/plugin_manager.py b/src/plugin_system/plugin_manager.py index e31b77fe..c59ce065 100644 --- a/src/plugin_system/plugin_manager.py +++ b/src/plugin_system/plugin_manager.py @@ -32,7 +32,6 @@ from src.plugin_system.schema_manager import ( from src.plugin_system.plugin_dirs import ( ManifestStatus, PluginDirectoryIndex, resolve_plugin_dir, ) -from src.deprecation import deprecated from src.common.permission_utils import ( ensure_directory_permissions, get_plugin_dir_mode @@ -873,16 +872,6 @@ class PluginManager: """ return self.plugins.copy() - @deprecated("3.8.0", "check each plugin's enabled flag in plugins") - def get_enabled_plugins(self) -> List[str]: - """ - Get list of enabled plugin IDs. - - Returns: - List of plugin IDs that are currently enabled - """ - return [pid for pid, plugin in self.plugins.items() if plugin.enabled] - def get_plugin_info(self, plugin_id: str) -> Optional[Dict[str, Any]]: """ Get information about a plugin (manifest + runtime info). diff --git a/src/plugin_system/testing/visual_display_manager.py b/src/plugin_system/testing/visual_display_manager.py index 01536514..bf3096fb 100644 --- a/src/plugin_system/testing/visual_display_manager.py +++ b/src/plugin_system/testing/visual_display_manager.py @@ -14,9 +14,7 @@ PIL Image canvas and draws text using the actual project fonts. MAINTENANCE WARNING: this class is a deliberate fork of src/display_manager.py so it can run without hardware. It mirrors these DisplayManager methods by name and behavior: _load_fonts, -get_font_height, get_text_width, draw_text, -draw_text_with_icons, draw_weather_icon (and the _draw_sun/_draw_cloud/ -_draw_rain/_draw_snow/_draw_storm family), format_date_with_ordinal, +get_font_height, get_text_width, draw_text, format_date_with_ordinal, capture_mode, set_scrolling_state, is_currently_scrolling, process_deferred_updates, update_display, render_size, offscreen. A behavior change to any of those in DisplayManager must be mirrored here, or @@ -26,13 +24,12 @@ BDF text is not mirrored: both classes load BDF faces and draw BDF glyphs 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 +from typing import Any, Optional, Tuple from PIL import Image, ImageDraw, ImageFont from src.common.bdf_font import draw_bdf_text, load_bdf_face @@ -63,15 +60,6 @@ class VisualTestDisplayManager: no emulator dependency. """ - # Weather icon color constants (same as DisplayManager) - WEATHER_COLORS = { - 'sun': (255, 200, 0), - 'cloud': (200, 200, 200), - 'rain': (0, 100, 255), - 'snow': (220, 220, 255), - 'storm': (255, 255, 0), - } - def __init__(self, width: int = 128, height: int = 32): self._width = width self._height = height @@ -410,129 +398,6 @@ class VisualTestDisplayManager: return font.size return 8 - # ------------------------------------------------------------------ - # Weather drawing helpers - # ------------------------------------------------------------------ - - def draw_sun(self, x: int, y: int, size: int = 16): - """Draw a sun icon using yellow circles and lines.""" - self._draw_sun(x, y, size) - - def draw_cloud(self, x: int, y: int, size: int = 16, color: Tuple[int, int, int] = (200, 200, 200)): - """Draw a cloud icon.""" - self._draw_cloud(x, y, size, color) - - def draw_rain(self, x: int, y: int, size: int = 16): - """Draw rain icon with cloud and droplets.""" - self._draw_rain(x, y, size) - - def draw_snow(self, x: int, y: int, size: int = 16): - """Draw snow icon with cloud and snowflakes.""" - self._draw_snow(x, y, size) - - def _draw_sun(self, x: int, y: int, size: int) -> None: - """Draw a sun icon with rays (internal weather icon version).""" - center_x, center_y = x + size // 2, y + size // 2 - radius = size // 4 - ray_length = size // 3 - self.draw.ellipse( - [center_x - radius, center_y - radius, - center_x + radius, center_y + radius], - fill=self.WEATHER_COLORS['sun'], - ) - for angle in range(0, 360, 45): - rad = math.radians(angle) - start_x = center_x + int((radius + 2) * math.cos(rad)) - start_y = center_y + int((radius + 2) * math.sin(rad)) - end_x = center_x + int((radius + ray_length) * math.cos(rad)) - end_y = center_y + int((radius + ray_length) * math.sin(rad)) - self.draw.line([start_x, start_y, end_x, end_y], fill=self.WEATHER_COLORS['sun'], width=2) - - def _draw_cloud(self, x: int, y: int, size: int, color: Optional[Tuple[int, int, int]] = None) -> None: - """Draw a cloud using multiple circles (internal weather icon version).""" - cloud_color = color if color is not None else self.WEATHER_COLORS['cloud'] - base_y = y + size // 2 - circle_radius = size // 4 - positions = [ - (x + size // 3, base_y), - (x + size // 2, base_y - size // 6), - (x + 2 * size // 3, base_y), - ] - for cx, cy in positions: - self.draw.ellipse( - [cx - circle_radius, cy - circle_radius, - cx + circle_radius, cy + circle_radius], - fill=cloud_color, - ) - - def _draw_rain(self, x: int, y: int, size: int) -> None: - """Draw rain drops falling from a cloud.""" - self._draw_cloud(x, y, size) - rain_color = self.WEATHER_COLORS['rain'] - drop_size = size // 8 - drops = [ - (x + size // 4, y + 2 * size // 3), - (x + size // 2, y + 3 * size // 4), - (x + 3 * size // 4, y + 2 * size // 3), - ] - for dx, dy in drops: - self.draw.line([dx, dy, dx - drop_size // 2, dy + drop_size], fill=rain_color, width=2) - - def _draw_snow(self, x: int, y: int, size: int) -> None: - """Draw snowflakes falling from a cloud.""" - self._draw_cloud(x, y, size) - snow_color = self.WEATHER_COLORS['snow'] - flake_size = size // 6 - flakes = [ - (x + size // 4, y + 2 * size // 3), - (x + size // 2, y + 3 * size // 4), - (x + 3 * size // 4, y + 2 * size // 3), - ] - for fx, fy in flakes: - for angle in range(0, 360, 60): - rad = math.radians(angle) - end_x = fx + int(flake_size * math.cos(rad)) - end_y = fy + int(flake_size * math.sin(rad)) - self.draw.line([fx, fy, end_x, end_y], fill=snow_color, width=1) - - def _draw_storm(self, x: int, y: int, size: int) -> None: - """Draw a storm cloud with lightning bolt.""" - self._draw_cloud(x, y, size) - bolt_color = self.WEATHER_COLORS['storm'] - bolt_points = [ - (x + size // 2, y + size // 2), - (x + 3 * size // 5, y + 2 * size // 3), - (x + 2 * size // 5, y + 2 * size // 3), - (x + size // 2, y + 5 * size // 6), - ] - self.draw.polygon(bolt_points, fill=bolt_color) - - def draw_weather_icon(self, condition: str, x: int, y: int, size: int = 16) -> None: - """Draw a weather icon based on the condition.""" - cond = condition.lower() - if cond in ('clear', 'sunny'): - self._draw_sun(x, y, size) - elif cond in ('clouds', 'cloudy', 'partly cloudy'): - self._draw_cloud(x, y, size) - elif cond in ('rain', 'drizzle', 'shower'): - self._draw_rain(x, y, size) - elif cond in ('snow', 'sleet', 'hail'): - self._draw_snow(x, y, size) - elif cond in ('thunderstorm', 'storm'): - self._draw_storm(x, y, size) - else: - self._draw_sun(x, y, size) - - 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.""" - self.draw_text(text, x, y, color) - if icons: - for icon_type, icon_x, icon_y in icons: - self.draw_weather_icon(icon_type, icon_x, icon_y) - self.update_display() - # ------------------------------------------------------------------ # Scrolling state (no-op interface compat) # ------------------------------------------------------------------ diff --git a/test/plugins/test_visual_rendering.py b/test/plugins/test_visual_rendering.py index 0bb77d0c..be970b4b 100644 --- a/test/plugins/test_visual_rendering.py +++ b/test/plugins/test_visual_rendering.py @@ -196,42 +196,3 @@ class TestVisualDisplayManager: assert '11th' in result -class TestWeatherDrawing: - """Test weather icon rendering.""" - - def test_draw_sun(self): - vdm = VisualTestDisplayManager(width=128, height=32) - vdm.draw_sun(0, 0, 16) - pixels = list(vdm.image.getdata()) - non_black = [p for p in pixels if p != (0, 0, 0)] - assert len(non_black) > 0 - - def test_draw_cloud(self): - vdm = VisualTestDisplayManager(width=128, height=32) - vdm.draw_cloud(0, 0, 16) - pixels = list(vdm.image.getdata()) - non_black = [p for p in pixels if p != (0, 0, 0)] - assert len(non_black) > 0 - - def test_draw_rain(self): - vdm = VisualTestDisplayManager(width=128, height=32) - vdm.draw_rain(0, 0, 16) - pixels = list(vdm.image.getdata()) - non_black = [p for p in pixels if p != (0, 0, 0)] - assert len(non_black) > 0 - - def test_draw_snow(self): - vdm = VisualTestDisplayManager(width=128, height=32) - vdm.draw_snow(0, 0, 16) - pixels = list(vdm.image.getdata()) - non_black = [p for p in pixels if p != (0, 0, 0)] - assert len(non_black) > 0 - - def test_draw_weather_icon_dispatches(self): - vdm = VisualTestDisplayManager(width=128, height=32) - for condition in ['clear', 'cloudy', 'rain', 'snow', 'storm', 'unknown']: - vdm.clear() - vdm.draw_weather_icon(condition, 0, 0, 16) - pixels = list(vdm.image.getdata()) - non_black = [p for p in pixels if p != (0, 0, 0)] - assert len(non_black) > 0, f"draw_weather_icon('{condition}') should render pixels" diff --git a/test/test_cache_manager_memory_tier.py b/test/test_cache_manager_memory_tier.py index af6f2f20..1ee1934b 100644 --- a/test/test_cache_manager_memory_tier.py +++ b/test/test_cache_manager_memory_tier.py @@ -40,11 +40,9 @@ def test_cleanup_and_stats_follow_a_replaced_component(cm): assert cm._memory_cache_component.get("stale") is None assert cm._memory_cache_component.get("fresh") == {"v": 1} - stats = cm.get_memory_cache_stats() - assert stats["size"] == 1 - assert stats["max_size"] == 7 - assert stats["cleanup_interval"] == 11.0 - assert stats["usage_percent"] == pytest.approx(100 / 7) + with patch.object(cm.logger, "info") as info: + cm.log_memory_cache_stats() + assert "Size: 1/7 (14.3%)" in info.call_args[0][0] def test_periodic_cleanup_is_throttled_and_records_its_run(cm): @@ -60,16 +58,16 @@ def test_periodic_cleanup_is_throttled_and_records_its_run(cm): before = time.time() cm.get_cached_data("missing") # triggers the periodic sweep assert mem.size() == 0 - assert cm.get_memory_cache_stats()["last_cleanup"] >= before + assert mem.get_stats()["last_cleanup"] >= before -def test_stats_have_the_documented_shape(cm): +def test_memory_stats_log_reads_the_live_tier(cm): cm.set("k", {"v": 1}) - stats = cm.get_memory_cache_stats() - assert set(stats) == {"size", "max_size", "usage_percent", - "last_cleanup", "cleanup_interval"} - assert stats["size"] == 1 - assert stats["max_size"] == cm._memory_cache_component.max_size() + with patch.object(cm.logger, "info") as info: + cm.log_memory_cache_stats() + message = info.call_args[0][0] + assert f"Size: 1/{cm._memory_cache_component.max_size()}" in message + assert "Last cleanup:" in message def test_listing_the_cache_dir_does_not_hold_the_memory_lock(cm, tmp_path): diff --git a/test/test_deprecation.py b/test/test_deprecation.py index 55e5a0ef..82469291 100644 --- a/test/test_deprecation.py +++ b/test/test_deprecation.py @@ -20,31 +20,10 @@ from src.deprecation import deprecated REPO = Path(__file__).resolve().parents[1] -#: Everything deprecated for removal in 3.8.0 (first announced for 3.7.0, -#: which shipped with all of them still in place). docs/DEPRECATIONS_3.8.md -#: says which are unused. 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"], -} - +#: Everything still deprecated. (The 35 methods deprecated for 3.8.0 were +#: removed in it: docs/DEPRECATIONS_3.8.md found them unused.) Removing one +#: of these, or deprecating another, should be a deliberate edit here too. +#: #: Deprecated with Vegas participation, for removal in 3.9.0: core never read #: them (src.plugin_system.base_plugin.VEGAS_LEGACY_REMOVAL). DEPRECATED_3_9 = { @@ -55,7 +34,7 @@ DEPRECATED_3_9 = { #: Every pinned marker: (class path, method) -> the release that removes it. PINNED = {(path, name): removal - for removal, table in (("3.8.0", DEPRECATED), ("3.9.0", DEPRECATED_3_9)) + for removal, table in (("3.9.0", DEPRECATED_3_9),) for path, names in table.items() for name in names} @@ -132,7 +111,57 @@ def test_usage_script_lists_exactly_the_pinned_markers(usage_script): assert found == {(path, name, removal) for (path, name), removal in PINNED.items()} -def test_usage_script_tells_uses_from_name_collisions(usage_script, tmp_path): +#: A stand-in core for the scanner tests below, so they keep working whichever +#: real markers exist (the 3.8.0 ones they were written against are gone). +FAKE_CORE = { + "src/cache_manager.py": """\ + class CacheManager: + @deprecated("9.9.0", "use set()") + def update_cache(self, key, data): + pass + """, + "src/display_manager.py": """\ + class DisplayManager: + @deprecated("9.9.0") + def draw_sun(self, x, y): + pass + + @deprecated("9.9.0") + def draw_cloud(self, x, y): + pass + + @deprecated("9.9.0") + def draw_rain(self, x, y): + self.draw_cloud(x, y) + + @deprecated("9.9.0") + def draw_snow(self, x, y): + pass + + @deprecated("9.9.0") + def get_scrolling_stats(self): + return {} + """, + "src/font_manager.py": """\ + class FontManager: + @deprecated("9.9.0") + def add_font(self, path, name): + return True + """, +} + + +@pytest.fixture +def fake_core(tmp_path): + root = tmp_path / "core" + for rel, source in FAKE_CORE.items(): + path = root / rel + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(textwrap.dedent(source), encoding="utf-8") + return root + + +def test_usage_script_tells_uses_from_name_collisions(usage_script, fake_core, tmp_path): """Calls on the owning object and overrides count; same-named methods of unrelated classes and hits in test files do not.""" plugin = tmp_path / "demo" @@ -163,7 +192,7 @@ def test_usage_script_tells_uses_from_name_collisions(usage_script, tmp_path): display_manager.draw_snow.assert_not_called() """), encoding="utf-8") - markers = usage_script.find_markers(REPO) + markers = usage_script.find_markers(fake_core) source = usage_script.Source("demo", "monorepo", plugin) usage_script.scan_tree(source, [plugin], plugin, markers, core=False) kinds = {key: sorted(("test " if h.test else "") + h.kind for h in hits) @@ -186,11 +215,12 @@ def test_usage_script_tells_uses_from_name_collisions(usage_script, tmp_path): assert status["DisplayManager.draw_snow"][0] == "unused" # a test mock only -def test_usage_script_follows_calls_between_deprecated_core_methods(usage_script): +def test_usage_script_follows_calls_between_deprecated_core_methods(usage_script, fake_core): """draw_rain calls draw_cloud; with no outside callers both are unused.""" - markers = usage_script.find_markers(REPO) - core = usage_script.Source("core", "core", REPO) - usage_script.scan_tree(core, [REPO / "src" / "display_manager.py"], REPO, markers, core=True) + markers = usage_script.find_markers(fake_core) + core = usage_script.Source("core", "core", fake_core) + usage_script.scan_tree(core, [fake_core / "src" / "display_manager.py"], fake_core, + markers, core=True) kinds = {h.kind for h in core.hits["DisplayManager.draw_cloud"]} assert kinds == {"internal"} assert usage_script.verdicts(markers, [core])["DisplayManager.draw_cloud"][0] == "unused" @@ -219,11 +249,3 @@ def test_first_call_warns_and_logs_then_stays_quiet(fresh, caplog): 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 diff --git a/test/test_font_manager.py b/test/test_font_manager.py index 0bd4f903..b377c616 100644 --- a/test/test_font_manager.py +++ b/test/test_font_manager.py @@ -150,16 +150,6 @@ class TestCacheLifecycle: fm.clear_cache() assert fm.cache_generation == gen_before + 1 - def test_clearing_a_plugins_cached_fonts_bumps_generation(self, fm): - fm.font_cache["demo::tiny_8"] = object() - gen_before = fm.cache_generation - fm._clear_plugin_font_cache("demo") - assert "demo::tiny_8" not in fm.font_cache - assert fm.cache_generation == gen_before + 1 - # Nothing to drop, nothing to rebuild. - fm._clear_plugin_font_cache("demo") - assert fm.cache_generation == gen_before + 1 - class TestPluginFonts: """plugin:// sources resolve against the plugin's own directory, which