refactor(plugins): split PluginStoreManager into mixins (#659)

* refactor(plugins): split PluginStoreManager into mixins

src/plugin_system/store_manager.py (2,977 lines) keeps the class, its
shared state, locks, the uninstall registry, directory lookup and
uninstall; its methods are split by area into:
- store_registry.py (_RegistryMixin): registry, GitHub metadata, search,
  manifest validation
- store_install.py (_InstallMixin): install paths and dependencies
- store_update.py (_UpdateMixin): updates, rollback, local git state

Pure move: all 56 members are byte-identical (checked with ast) and the
assembled class has exactly the same attributes as before (checked at
runtime). PluginStoreManager is imported from store_manager.py as before.
Tests that patched shared modules (subprocess, requests, tempfile, shutil)
through store_manager now reach them through the module whose code they
exercise; a source-text contract test reads all store_*.py modules.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* chore: annotate findings the split moved into new store modules

subprocess imports and a list-form git clone (no shell), and the config
template's placeholder token string -- existing code that Codacy reported
as new because it moved. Annotated with the repo's nosec/nosemgrep style.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* chore: annotate the default-branch git clone the split moved

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Chuck
2026-09-28 15:00:45 -04:00
committed by GitHub
co-authored by Claude Opus 5.5
parent d469fe39d2
commit 989eae9405
15 changed files with 2627 additions and 2538 deletions
+5 -1
View File
@@ -210,7 +210,11 @@ class TestStandaloneBackupContract:
from src.plugin_system import plugin_dirs
assert plugin_dirs.BACKUP_MARKER == '.standalone-backup-'
root = Path(__file__).resolve().parents[1]
sm_text = (root / "src/plugin_system/store_manager.py").read_text(encoding="utf-8")
# PluginStoreManager's methods are spread over store_manager.py and
# its store_*.py mixins.
sm_text = "".join(
p.read_text(encoding="utf-8")
for p in sorted((root / "src/plugin_system").glob("store_*.py")))
assert "{BACKUP_MARKER}preinstall" in sm_text
assert "{BACKUP_MARKER}migrating" in sm_text
assert plugin_dirs.is_ignored_dir_name(
+2 -2
View File
@@ -42,8 +42,8 @@ def _run(tmp_path, move_side_effect=None):
return path
with patch.object(sm, '_http_get_with_retries', return_value=_response(_zip_bytes())), \
patch('src.plugin_system.store_manager.tempfile.mkdtemp', side_effect=tracking_mkdtemp), \
patch('src.plugin_system.store_manager.shutil.move', side_effect=move_side_effect):
patch('src.plugin_system.store_install.tempfile.mkdtemp', side_effect=tracking_mkdtemp), \
patch('src.plugin_system.store_install.shutil.move', side_effect=move_side_effect):
ok = sm._install_via_download('https://example.invalid/demo.zip', plugins_dir / 'demo')
return ok, created
+1 -1
View File
@@ -92,7 +92,7 @@ def test_github_network_failure_logs_a_warning_not_an_error(caplog):
from src.plugin_system.store_manager import PluginStoreManager
with TemporaryDirectory() as tmp:
sm = PluginStoreManager(plugins_dir=tmp)
with patch("src.plugin_system.store_manager.requests.get",
with patch("src.plugin_system.store_registry.requests.get",
side_effect=requests.ConnectionError("offline")), \
caplog.at_level(logging.WARNING):
info = sm._get_github_repo_info("https://github.com/owner/repo")
+2 -2
View File
@@ -468,7 +468,7 @@ class TestGitPullGate:
import src
monkeypatch.setattr(src, '__version__', '3.2.0')
monkeypatch.setattr(
'src.plugin_system.store_manager.subprocess.run', fail_stash)
'src.plugin_system.store_update.subprocess.run', fail_stash)
assert mgr.update_plugin('gitplug') is False
assert self._head(work) == before, "must not pull what it cannot undo"
@@ -496,7 +496,7 @@ class TestGitPullGate:
import src
monkeypatch.setattr(src, '__version__', '3.2.0')
monkeypatch.setattr(
'src.plugin_system.store_manager.subprocess.run', fail_reset)
'src.plugin_system.store_update.subprocess.run', fail_reset)
assert mgr.update_plugin('gitplug') is False
logged = ' '.join(str(c) for c in mgr.logger.error.call_args_list)
+9 -9
View File
@@ -157,7 +157,7 @@ class TestGitInfoCache(unittest.TestCase):
def test_cache_hits_avoid_subprocess_calls(self):
with patch(
"src.plugin_system.store_manager.subprocess.run",
"src.plugin_system.store_update.subprocess.run",
side_effect=self._fake_subprocess_run,
) as mock_run:
first = self.sm._get_local_git_info(self.plugin_path)
@@ -174,7 +174,7 @@ class TestGitInfoCache(unittest.TestCase):
def test_cache_invalidates_on_head_mtime_change(self):
with patch(
"src.plugin_system.store_manager.subprocess.run",
"src.plugin_system.store_update.subprocess.run",
side_effect=self._fake_subprocess_run,
) as mock_run:
self.sm._get_local_git_info(self.plugin_path)
@@ -229,7 +229,7 @@ class TestGitInfoCache(unittest.TestCase):
return result
with patch(
"src.plugin_system.store_manager.subprocess.run",
"src.plugin_system.store_update.subprocess.run",
side_effect=fake_subprocess_run,
):
first = self.sm._get_local_git_info(self.plugin_path)
@@ -284,7 +284,7 @@ class TestGitInfoCache(unittest.TestCase):
return result
with patch(
"src.plugin_system.store_manager.subprocess.run",
"src.plugin_system.store_update.subprocess.run",
side_effect=fake_subprocess_run,
):
first = self.sm._get_local_git_info(self.plugin_path)
@@ -442,7 +442,7 @@ class TestStaleOnErrorFallbacks(unittest.TestCase):
self.sm.cache_timeout = 1 # force re-fetch
import requests as real_requests
with patch("src.plugin_system.store_manager.requests.get",
with patch("src.plugin_system.store_registry.requests.get",
side_effect=real_requests.ConnectionError("boom")):
result = self.sm._get_github_repo_info("https://github.com/owner/repo")
self.assertEqual(result["stars"], 42)
@@ -472,7 +472,7 @@ class TestStaleOnErrorFallbacks(unittest.TestCase):
call_count["n"] += 1
raise real_requests.ConnectionError("boom")
with patch("src.plugin_system.store_manager.requests.get", side_effect=counting_get):
with patch("src.plugin_system.store_registry.requests.get", side_effect=counting_get):
first = self.sm._get_github_repo_info("https://github.com/owner/repo")
self.assertEqual(first["stars"], 99)
self.assertEqual(call_count["n"], 1)
@@ -505,7 +505,7 @@ class TestStaleOnErrorFallbacks(unittest.TestCase):
call_count["n"] += 1
return rate_limited
with patch("src.plugin_system.store_manager.requests.get", side_effect=counting_get):
with patch("src.plugin_system.store_registry.requests.get", side_effect=counting_get):
self.sm._get_github_repo_info("https://github.com/owner/repo")
self.assertEqual(call_count["n"], 1)
self.sm._get_github_repo_info("https://github.com/owner/repo")
@@ -523,7 +523,7 @@ class TestStaleOnErrorFallbacks(unittest.TestCase):
self.sm.commit_cache_timeout = 1 # force re-fetch
import requests as real_requests
with patch("src.plugin_system.store_manager.requests.get",
with patch("src.plugin_system.store_registry.requests.get",
side_effect=real_requests.ConnectionError("boom")):
result = self.sm._get_latest_commit_info(
"https://github.com/owner/repo", branch="main"
@@ -553,7 +553,7 @@ class TestStaleOnErrorFallbacks(unittest.TestCase):
not_found = MagicMock()
not_found.status_code = 404
not_found.text = "Not Found"
with patch("src.plugin_system.store_manager.requests.get", return_value=not_found):
with patch("src.plugin_system.store_registry.requests.get", return_value=not_found):
result = self.sm._get_latest_commit_info(
"https://github.com/owner/repo", branch="main"
)