Skip to content

Fix "No module named 'swapserver_gui'" crash when installing the zip - #3

Merged
nothing-stops-this-train merged 1 commit into
mainfrom
fix/zip-plugin-relative-import
Jul 28, 2026
Merged

Fix "No module named 'swapserver_gui'" crash when installing the zip#3
nothing-stops-this-train merged 1 commit into
mainfrom
fix/zip-plugin-relative-import

Conversation

@nothing-stops-this-train

Copy link
Copy Markdown
Owner

Installing the v.0.13 plugin zip in Electrum crashes with:

ModuleNotFoundError: No module named 'swapserver_gui'

Cause

qt.py and swapserver_gui.py both used from . import pow as swap_pow. That import form is unsafe for an Electrum external zip plugin.

electrum/plugin.py: maybe_load_plugin_init_method registers the package in sys.modules as electrum_external_plugins.swapserver_gui, but builds its spec with zipimport.zipimporter(zip).find_spec('swapserver_gui') — so the package object's __name__ stays the bare swapserver_gui:

sys.modules key   : electrum_external_plugins.swapserver_gui
module.__name__   : swapserver_gui          <-- what _handle_fromlist uses
'swapserver_gui' in sys.modules: False

CPython's importlib._bootstrap._handle_fromlist builds the submodule name as '{}.{}'.format(module.__name__, x), so from . import pow resolved to swapserver_gui.pow, whose parent is not importable.

Only v.0.13 is affected — pow.py and these imports arrived with the shutdown-hang fix. from .swapserver_gui import ... was never affected: a non-package module has no __path__, so _handle_fromlist skips that branch and resolution goes through __package__.

Fix

swap_pow = importlib.import_module('.pow', __package__). __package__ comes from the submodule's own correctly-dotted spec, so it works for both the zip and the directory layout. All 9 swap_pow. call sites and the 'swap_pow.PowState' annotations are unchanged.

Tests

The existing suite could not catch this: it puts plugins/ on sys.path and imports the package as a top-level swapserver_gui, where __name__ does match the sys.modules key.

tests/test_zip_plugin_load.py adds:

  • an AST guard rejecting from . import <submodule> in the plugin sources — covers qt.py without needing PyQt6 — plus a check that the guard actually fires;
  • an end-to-end test that builds the real zip via contrib/make_zip.sh and replicates Electrum's loader in a subprocess with plugins/ deliberately off sys.path, asserting the bare name is genuinely unimportable so the test is not vacuous (a fresh interpreter is required anyway, since ConfigVar asserts each key registers exactly once);
  • a test that the directory layout still imports as a top-level package.

Reverting the two import lines fails the new tests. With the fix: 59 tests, OK.

🤖 Generated with Claude Code

https://claude.ai/code/session_013zETFC6MhnsCuxvQDGxqV5

Installing the plugin zip in Electrum crashed with

    ModuleNotFoundError: No module named 'swapserver_gui'

qt.py and swapserver_gui.py both did `from . import pow as swap_pow`, which
is unsafe for an Electrum *external zip* plugin.

electrum/plugin.py (maybe_load_plugin_init_method) registers the package in
sys.modules as 'electrum_external_plugins.swapserver_gui', but builds its spec
with zipimport.zipimporter(zip).find_spec('swapserver_gui') -- so the package
object's __name__ stays the bare 'swapserver_gui'. CPython's
importlib._bootstrap._handle_fromlist resolves a submodule fromlist against
__name__ rather than the sys.modules key, so `from . import pow` resolved to
'swapserver_gui.pow', whose parent is not importable.

Use importlib.import_module('.pow', __package__) instead: __package__ comes
from the submodule's own correctly-dotted spec, so it works for both the zip
and the directory layout. All swap_pow call sites are unchanged.

Only v.0.13 was affected -- pow.py and these imports arrived with the
shutdown-hang fix. `from .swapserver_gui import ...` was never affected: a
non-package module has no __path__, so _handle_fromlist skips that branch.

The existing tests could not catch this because they put plugins/ on sys.path
and import the package as a top-level 'swapserver_gui', where __name__ does
match the sys.modules key. tests/test_zip_plugin_load.py therefore adds:

* an AST guard rejecting `from . import <submodule>` in the plugin sources
  (covers qt.py without needing PyQt6), plus a check that the guard fires;
* an end-to-end test that builds the real zip via contrib/make_zip.sh and
  replicates Electrum's loader in a subprocess with plugins/ deliberately off
  sys.path, asserting the bare name is genuinely unimportable so the test is
  not vacuous;
* a test that the directory layout still imports as a top-level package.

Reverting the two import lines fails the new tests; with the fix the suite is
59 tests, OK.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013zETFC6MhnsCuxvQDGxqV5
@nothing-stops-this-train
nothing-stops-this-train merged commit 1db28d2 into main Jul 28, 2026
1 check passed
@nothing-stops-this-train
nothing-stops-this-train deleted the fix/zip-plugin-relative-import branch July 28, 2026 18:32
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