v0.6.1: read version from manifest.json (single source of truth)

Read the plugin version at runtime from bal/manifest.json via
importlib.resources (zip-safe, works from inside a zip on Windows).
Removes the four-file duplication (VERSION, __init__.py, plugin_base.py,
manifest.json) and deletes the bal/VERSION file.

- bal/core/plugin_base.py: new get_version() function, BalPlugin.version
  is now a @property, removed hardcoded __version__
- bal/__init__.py: removed __version__, replaced with a comment
- bal/core/willexecutors.py: user-agent uses get_version()
- bal/gui/qt/dialogs.py, widgets.py: call sites use .version property
- tests/test_version_source.py: 7 tests incl. zip-safe subprocess check
- HANDOFF.md: updated to single-source-of-truth documentation
- CHANGELOG.md: entries 43-46
- bal/manifest.json: version bumped to 0.6.1

PR #4 (branch bitcoinafterlife-patch-5).
This commit is contained in:
2026-07-17 17:40:18 +00:00
committed by svatantrya
parent f72ed33ea0
commit 4b0af6f4bf
10 changed files with 338 additions and 32 deletions

View File

@@ -21,6 +21,7 @@ serialised together with the wallet file.
This module performs **no** GUI work and imports nothing from PyQt / electrum.gui.
"""
import json
import os
import platform
from datetime import date, datetime, timedelta
@@ -33,6 +34,45 @@ from electrum.transaction import tx_from_any
_logger = get_logger(__name__)
# --------------------------------------------------------------------------- #
# Plugin version - single source of truth
# --------------------------------------------------------------------------- #
# The version lives ONLY in bal/manifest.json (the file Electrum itself reads).
# We used to hardcode it in four files and keep them in sync with a pre-commit
# hook; reading it from the manifest removes that duplication.
#
# importlib.resources is used on purpose: it reads a data file bundled inside
# the ``bal`` package and works identically whether the plugin runs from an
# extracted directory or from INSIDE a zip (Electrum loads external plugins via
# zipimport). It never builds a path by hand, so there is no os.path.join
# backslash issue on Windows inside a zip.
_VERSION_CACHE = None
def get_version():
"""Return the plugin version from ``bal/manifest.json`` (cached).
Zip-safe and independent of the current working directory. Falls back to
``"unknown"`` if the manifest cannot be read, so importing the plugin never
fails just because of version lookup.
"""
global _VERSION_CACHE
if _VERSION_CACHE is None:
try:
import importlib.resources
data = (
importlib.resources.files("bal")
.joinpath("manifest.json")
.read_text(encoding="utf-8")
)
_VERSION_CACHE = json.loads(data)["version"]
except Exception as e: # noqa: BLE001 - never break import over version
_logger.error(f"failed to read version from manifest.json: {e}")
_VERSION_CACHE = "unknown"
return _VERSION_CACHE
# --------------------------------------------------------------------------- #
# Wallet-DB registration
# --------------------------------------------------------------------------- #
@@ -120,9 +160,6 @@ class BalPlugin(BasePlugin):
layer (or unit tests) can use the plugin logic without importing Qt.
"""
_version = None
__version__ = "0.5.18" # AUTOMATICALLY GENERATED DO NOT EDIT
# Command used to open an .ics calendar file, per operating system.
default_app = {
"Linux": "xdg-open",
@@ -138,18 +175,11 @@ class BalPlugin(BasePlugin):
# Default geometry hint for some dialogs (kept from the original code).
SIZE = (159, 97)
@property
def version(self):
"""Return the plugin version, read once from the ``VERSION`` file."""
if not self._version:
try:
f = ""
with open("{}/VERSION".format(self.plugin_dir), "r") as fi:
f = str(fi.read())
self._version = f.strip()
except Exception as e:
_logger.error(f"failed to get version: {e}")
self._version = "unknown"
return self._version
"""Plugin version, read from ``bal/manifest.json`` (single source of
truth). See :func:`get_version`."""
return get_version()
def __init__(self, parent, config, name):
self.logger = get_logger(__name__)

View File

@@ -23,7 +23,7 @@ from electrum.i18n import _
from electrum.logging import get_logger
from electrum.network import Network
from .plugin_base import BalPlugin
from .plugin_base import BalPlugin, get_version
# Per-request timeout (seconds) for interactive operations (ping / info /
# list download). These fail fast (no retries) so a dead server does not
@@ -272,7 +272,7 @@ class Willexecutors:
raise Exception("You are offline.")
_logger.debug(f"<-- {method} {url} {data}")
headers = {}
headers["user-agent"] = f"BalPlugin v:{BalPlugin.__version__}"
headers["user-agent"] = f"BalPlugin v:{get_version()}"
headers["Content-Type"] = "text/plain"
if not handle_response:
handle_response = Willexecutors.handle_response