Skip to content

feat(plugins): warn when plugin venv was built with a different Python#43

Closed
lukeocodes wants to merge 1 commit intomainfrom
plugin-abi-mismatch-warning
Closed

feat(plugins): warn when plugin venv was built with a different Python#43
lukeocodes wants to merge 1 commit intomainfrom
plugin-abi-mismatch-warning

Conversation

@lukeocodes
Copy link
Copy Markdown
Member

Summary

Closes the last edge case in our "Homebrew installs + plugins" story. When brew upgrade python@3.13 rebuilds deepctl against a newer underlying Python, the user's plugin venv at ~/.deepctl/plugins/venv/ keeps the old Python it was built with. C-extension plugins then fail to load with a confusing low-level ImportError and no breadcrumb to the fix.

This PR detects that mismatch and prints a one-line, actionable warning at plugin-load time:

⚠ Plugin environment was built with Python 3.12 but you're running Python 3.13.
  C-extension plugins may fail to load. To rebuild:
    rm -rf /Users/<you>/.deepctl/plugins/venv && deepctl plugin install <your-plugin>

Pure-Python plugins continue to load transparently via sys.path bridging — the warning fires alongside normal loading, not as a blocker.

Why now

Companion to:

The plugin system already routes Homebrew installs through IsolatedVenvStrategy, so plugins survive the install path by design. The Python-version-bump scenario is the only remaining failure mode that's hard to diagnose from a user's perspective. Closing it now means we ship the Homebrew install path with a complete plugin story.

What lands

File Change
packages/deepctl-core/src/deepctl_core/plugin_env.py New get_venv_python_version() reads pyvenv.cfg, returns (major, minor) or None on missing/malformed cfg
packages/deepctl-core/src/deepctl_core/plugin_manager.py New _warn_if_plugin_venv_python_mismatch() fires inside _load_plugin_venv_entries, before sys.path bridging
packages/deepctl-core/tests/unit/test_plugin_env.py 6 new test cases for get_venv_python_version
packages/deepctl-core/tests/unit/test_plugin_manager.py 4 new test cases for the warning logic

Design notes

  • pyvenv.cfg parsing handles both version (stdlib python -m venv) and version_info (uv-created venvs). Both formats use MAJOR.MINOR.PATCH... semantics so splitting on . and taking [0:2] works for both.
  • None from get_venv_python_version means "unknown, skip the check" — explicitly documented in the function's docstring and respected by _warn_if_plugin_venv_python_mismatch. Avoids noisy false-positive warnings when the cfg is missing on weird/legacy venvs.
  • Warning fires once per CLI invocation (process-level), and only when a tracked plugin exists (plugin venv exists + plugins.json non-empty), so users without plugins never see it.
  • Comparison is at major.minor granularity — patch-level differences (3.13.7 → 3.13.8) don't break ABI compatibility, so they don't warrant a warning.

Verification

Check Result
pytest packages/deepctl-core/tests/unit/test_plugin_*.py -v 48 passed (10 new, 38 existing)
ruff check (changed src files) clean
ruff format --check (changed src files) clean
mypy --strict (changed src files) clean
Existing _load_plugin_venv_entries behavior unchanged for users without plugins or with matching Python

The 21 ruff warnings and 5 mypy errors visible in a wider check are all pre-existing in files this PR doesn't touch (auth.py, deepctl_cmd_mcp, test_auth.py, etc.) — out of scope per repo policy.

Pre-existing comments / docstrings

A few docstrings in the new code were retained as Category 3 necessary:

  • get_venv_python_version — public API in deepctl_core, documents the dual version/version_info key support and the None contract that _warn_if_plugin_venv_python_mismatch depends on
  • _warn_if_plugin_venv_python_mismatch — documents when it fires (the Homebrew Python-bump scenario) and why we warn instead of block (so future "robustness" PRs don't break the pure-Python plugin case by raising/skipping)
  • Test class + method docstrings — match the file's strong existing convention (every test has a docstring) and are surfaced in pytest -v output

Each describes a behavioral contract that the code alone can't communicate; removing them would predictably lead to wrong-direction refactors.

Out of scope

This PR doesn't try to automatically recreate the plugin venv on mismatch. That would be a more invasive change with its own failure modes (what if the rebuild fails partway through? what about user-installed dev plugins via --editable?). The single-line warning with explicit remediation is a much smaller blast radius. If users frequently hit this, we can layer auto-recovery on top later.

When a user installs deepctl via Homebrew (or any path that ends up using
IsolatedVenvStrategy), then later upgrades the underlying interpreter
(e.g. brew upgrade python@3.13), the running interpreter no longer
matches the Python that built ~/.deepctl/plugins/venv/. Pure-Python
plugins keep loading because PluginManager bridges them via sys.path,
but C-extension plugins fail with a confusing low-level ImportError that
gives users no obvious remediation.

This adds a one-line warning at plugin-load time that surfaces the
mismatch and tells the user how to rebuild:

    Plugin environment was built with Python 3.12 but you're running
    Python 3.13. C-extension plugins may fail to load. To rebuild:
      rm -rf ~/.deepctl/plugins/venv && deepctl plugin install <plugin>

Implementation:
- New get_venv_python_version() in plugin_env.py reads pyvenv.cfg and
  returns (major, minor). Handles both stdlib's 'version' key and uv's
  'version_info' key. Returns None on missing/malformed cfg so callers
  can treat 'unknown' as 'no warning'.
- New _warn_if_plugin_venv_python_mismatch() in PluginManager fires
  before sys.path bridging in _load_plugin_venv_entries, only when
  there's a real mismatch (no warning for unknown / matching versions).

Tests:
- 6 cases for get_venv_python_version (missing venv, missing cfg,
  stdlib 'version' key, uv 'version_info' key, malformed, no version key)
- 4 cases for the warning logic (silent on unknown, silent on match,
  warns on minor diff, warns on major diff)

All 48 plugin tests pass. mypy + ruff clean on changed src files.
@lukeocodes
Copy link
Copy Markdown
Member Author

Folded into #42 so all the Homebrew-tap-related changes for deepgram/cli ship as a single review unit.

@lukeocodes lukeocodes closed this Apr 29, 2026
@lukeocodes lukeocodes deleted the plugin-abi-mismatch-warning branch April 29, 2026 20:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant