mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-10-04 14:25:08 +00:00
chore(deprecation): retarget the 35 deprecations to 3.8.0 and add a usage scan (#681)
Moves the 35 @deprecated markers from 3.7.0 (already shipped with them in place) to 3.8.0, and adds scripts/plugin_api_usage.py plus the generated docs/DEPRECATIONS_3.8.md: who still calls or overrides each deprecated method across core, the monorepo and third-party plugins. Removes nothing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+136
-4
@@ -1,19 +1,29 @@
|
||||
"""@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 ast
|
||||
import importlib.util
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
import textwrap
|
||||
import warnings
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from packaging.version import Version
|
||||
|
||||
os.environ.setdefault("EMULATOR", "true")
|
||||
|
||||
from src import deprecation
|
||||
from src import __version__, 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.
|
||||
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",
|
||||
@@ -49,7 +59,129 @@ def test_exactly_these_methods_are_deprecated(path):
|
||||
if hasattr(value, "__deprecated__"))
|
||||
assert marked == sorted(DEPRECATED[path])
|
||||
for name in marked:
|
||||
assert "3.7.0" in getattr(cls, name).__deprecated__
|
||||
assert "3.8.0" in getattr(cls, name).__deprecated__
|
||||
|
||||
|
||||
def _markers():
|
||||
"""(file:line, removal) for every ``@deprecated(...)`` under src/."""
|
||||
found = []
|
||||
for path in sorted((REPO / "src").rglob("*.py")):
|
||||
tree = ast.parse(path.read_text(encoding="utf-8"), str(path))
|
||||
for fn in ast.walk(tree):
|
||||
if not isinstance(fn, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
||||
continue
|
||||
for dec in fn.decorator_list:
|
||||
target = dec.func if isinstance(dec, ast.Call) else dec
|
||||
name = getattr(target, "id", None) or getattr(target, "attr", None)
|
||||
if name != "deprecated":
|
||||
continue
|
||||
where = f"{path.relative_to(REPO).as_posix()}:{fn.lineno} {fn.name}"
|
||||
arg = dec.args[0] if isinstance(dec, ast.Call) and dec.args else None
|
||||
found.append((where, arg.value if isinstance(arg, ast.Constant) else None))
|
||||
return found
|
||||
|
||||
|
||||
def test_markers_are_found():
|
||||
assert len(_markers()) == sum(len(v) for v in DEPRECATED.values())
|
||||
|
||||
|
||||
def test_no_marker_names_a_release_already_shipped():
|
||||
"""3.7.0 shipped still warning that 35 methods are "removed in 3.7.0".
|
||||
|
||||
Once ``src.__version__`` reaches a marker's release, that release is here:
|
||||
remove the method (if scripts/plugin_api_usage.py reports it unused) or
|
||||
move the marker to a later release. Either way, never ship a warning that
|
||||
names a version the user is already running.
|
||||
"""
|
||||
current = Version(__version__)
|
||||
stale = [f"{where} -> {removal!r}" for where, removal in _markers()
|
||||
if not isinstance(removal, str) or Version(removal) <= current]
|
||||
assert not stale, (f"@deprecated markers at or below src.__version__ {__version__} "
|
||||
f"(or not a literal version): {stale}")
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def usage_script():
|
||||
path = REPO / "scripts" / "plugin_api_usage.py"
|
||||
spec = importlib.util.spec_from_file_location("plugin_api_usage_script", path)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
sys.modules[spec.name] = module # dataclasses resolve annotations through it
|
||||
try:
|
||||
spec.loader.exec_module(module)
|
||||
yield module
|
||||
finally:
|
||||
sys.modules.pop(spec.name, None)
|
||||
|
||||
|
||||
def test_usage_script_lists_exactly_the_pinned_markers(usage_script):
|
||||
found = {(f"{m.module}.{m.owner}", m.method, m.removal)
|
||||
for m in usage_script.find_markers(REPO)}
|
||||
assert found == {(path, name, "3.8.0")
|
||||
for path, names in DEPRECATED.items() for name in names}
|
||||
|
||||
|
||||
def test_usage_script_tells_uses_from_name_collisions(usage_script, 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"
|
||||
(plugin / "test").mkdir(parents=True)
|
||||
(plugin / "manager.py").write_text(textwrap.dedent("""\
|
||||
class Icons:
|
||||
@staticmethod
|
||||
def draw_sun(img):
|
||||
pass
|
||||
|
||||
class Plugin:
|
||||
def draw_cloud(self):
|
||||
return self.draw_cloud()
|
||||
|
||||
def display(self):
|
||||
Icons.draw_sun(None)
|
||||
self.cache_manager.update_cache('k', {})
|
||||
dm = self.display_manager
|
||||
dm.draw_rain(0, 0)
|
||||
thing.get_scrolling_stats()
|
||||
|
||||
class MyFonts(FontManager):
|
||||
def add_font(self, path, name):
|
||||
return super().add_font(path, name)
|
||||
"""), encoding="utf-8")
|
||||
(plugin / "test" / "test_manager.py").write_text(textwrap.dedent("""\
|
||||
def test_x(display_manager):
|
||||
display_manager.draw_snow.assert_not_called()
|
||||
"""), encoding="utf-8")
|
||||
|
||||
markers = usage_script.find_markers(REPO)
|
||||
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)
|
||||
for key, hits in source.hits.items()}
|
||||
|
||||
assert kinds == {
|
||||
"DisplayManager.draw_sun": ["unrelated", "unrelated"],
|
||||
"DisplayManager.draw_cloud": ["unrelated", "unrelated"],
|
||||
"CacheManager.update_cache": ["call"],
|
||||
"DisplayManager.draw_rain": ["call"],
|
||||
"DisplayManager.get_scrolling_stats": ["review"],
|
||||
"FontManager.add_font": ["call", "override"],
|
||||
"DisplayManager.draw_snow": ["test call"],
|
||||
}
|
||||
|
||||
status = usage_script.verdicts(markers, [source])
|
||||
assert status["CacheManager.update_cache"][0] == "used"
|
||||
assert status["DisplayManager.get_scrolling_stats"][0] == "review"
|
||||
assert status["DisplayManager.draw_sun"][0] == "unused" # a collision only
|
||||
assert status["DisplayManager.draw_snow"][0] == "unused" # a test mock only
|
||||
|
||||
|
||||
def test_usage_script_follows_calls_between_deprecated_core_methods(usage_script):
|
||||
"""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)
|
||||
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"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
||||
Reference in New Issue
Block a user