Fix "No module named 'swapserver_gui'" crash when installing the zip - #3
Merged
Merged
Conversation
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Installing the v.0.13 plugin zip in Electrum crashes with:
Cause
qt.pyandswapserver_gui.pyboth usedfrom . import pow as swap_pow. That import form is unsafe for an Electrum external zip plugin.electrum/plugin.py: maybe_load_plugin_init_methodregisters the package insys.modulesaselectrum_external_plugins.swapserver_gui, but builds its spec withzipimport.zipimporter(zip).find_spec('swapserver_gui')— so the package object's__name__stays the bareswapserver_gui:CPython's
importlib._bootstrap._handle_fromlistbuilds the submodule name as'{}.{}'.format(module.__name__, x), sofrom . import powresolved toswapserver_gui.pow, whose parent is not importable.Only v.0.13 is affected —
pow.pyand these imports arrived with the shutdown-hang fix.from .swapserver_gui import ...was never affected: a non-package module has no__path__, so_handle_fromlistskips 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 9swap_pow.call sites and the'swap_pow.PowState'annotations are unchanged.Tests
The existing suite could not catch this: it puts
plugins/onsys.pathand imports the package as a top-levelswapserver_gui, where__name__does match thesys.moduleskey.tests/test_zip_plugin_load.pyadds:from . import <submodule>in the plugin sources — coversqt.pywithout needing PyQt6 — plus a check that the guard actually fires;contrib/make_zip.shand replicates Electrum's loader in a subprocess withplugins/deliberately offsys.path, asserting the bare name is genuinely unimportable so the test is not vacuous (a fresh interpreter is required anyway, sinceConfigVarasserts each key registers exactly once);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