Compare commits
7 Commits
v0.6.0
...
31b5e9478a
| Author | SHA1 | Date | |
|---|---|---|---|
| 31b5e9478a | |||
| 0bcd9c5340 | |||
| bf89bd4217 | |||
| 7ed0f60c88 | |||
| 300fa89124 | |||
| 8aa2175f15 | |||
| e567676e0e |
181
CHANGELOG.md
181
CHANGELOG.md
@@ -2329,3 +2329,184 @@ The request was to make the failure message clearer (no timeout change).
|
||||
- `ruff`: no new errors.
|
||||
|
||||
**Outcome:** DONE (delivered as test ZIP v0.5.18; commit only after confirmation).
|
||||
|
||||
---
|
||||
|
||||
## 43. v0.6.0 - Version bump for the official repository release
|
||||
|
||||
**Date:** 2026-07-17
|
||||
|
||||
**Context:** the plugin content of this version is IDENTICAL to v0.5.18 - no code
|
||||
changes. A release was published on the official repository
|
||||
(bitcoinafterlife/bal-electrum-plugin) tagged "v0.6.0", but the internal version
|
||||
files still read "0.5.18" (a human oversight: the release tag was not matched by
|
||||
a version bump in the code), so Electrum displayed "0.5.18" after installing it.
|
||||
This entry aligns the internal version with the intended "0.6.0" release tag by
|
||||
bumping `bal/VERSION`, `bal/manifest.json`, `bal/__init__.py` and
|
||||
`bal/core/plugin_base.py` from 0.5.18 to 0.6.0. No functional changes.
|
||||
|
||||
**Verification:** full test suite against Electrum 4.7.2 and 4.8.0 (same 266
|
||||
passed / 2 pre-existing unrelated failures as v0.5.18); `ruff` clean.
|
||||
|
||||
**Outcome:** DONE.
|
||||
|
||||
---
|
||||
|
||||
## 44. Repo: inheritance-logic test suite + first BAL CLI (no plugin version change)
|
||||
|
||||
**Date:** 2026-07-20
|
||||
|
||||
**Scope note:** this entry adds REPOSITORY files only (`tests/`, `bal_cli.py`).
|
||||
The plugin package `bal/` is byte-for-byte unchanged, so the plugin version
|
||||
stays **0.6.0** on purpose (no zip rebuild, no version bump - avoiding any new
|
||||
tag/code mismatch).
|
||||
|
||||
**1) New logic test suite - `tests/test_inheritance_scenarios.py` (23 tests):**
|
||||
- Multi-heir distribution via `Heirs.prepare_lists`: 50/50 percent split of
|
||||
(balance - fees); percentages normalized to their SUM (30+30 behaves like
|
||||
50/50); fixed heirs paid first with percents sharing the remainder; fixed
|
||||
amounts exceeding the balance scaled down proportionally (`onlyfixed`).
|
||||
- Dust rules: below-dust heirs marked `DUST:` while valid heirs keep building;
|
||||
the leftover redistribution can LIFT dust fixed heirs above the threshold
|
||||
(pinned: 100:200 on 1M -> 333333/666666); all-dust wills refused
|
||||
(`HeirAmountIsDustException`); `BalanceTooLowException` when balance < fees.
|
||||
- Will-executor fees: one pseudo-heir per distinct locktime; fees larger than
|
||||
the balance raise `WillExecutorFeeException`.
|
||||
- Expired-heir exclusion vs `from_locktime`; grouping of heirs by locktime.
|
||||
- Heir add/remove/locktime-change all flag the will as changed
|
||||
(`Will._same_heirs`); the `Heirs` mapping persists on add/remove.
|
||||
- Will expiry (`Will.check_will_expired`): past locktime raises
|
||||
`WillExpiredException`, future/boundary (== now) does not, non-VALID items
|
||||
are ignored.
|
||||
- Locktime parsing: int passthrough, `<n>d` -> future midnight, `1y` == `365d`.
|
||||
|
||||
**2) First BAL CLI - `bal_cli.py` (offline iteration):**
|
||||
- Reuses `bal.core` directly with Electrum as a library (no Qt, headless).
|
||||
- Commands: `heirs list/add/remove/export/import`, `status`.
|
||||
- Safety: **testnet by default**, mainnet only with an explicit `--mainnet`
|
||||
flag (active network always printed); encrypted wallets via `--password` or
|
||||
`BAL_WALLET_PASSWORD` env var; address/locktime validation (rejects wrong
|
||||
network and past locktimes); guaranteed process termination (electrum leaves
|
||||
non-daemon threads even offline - the CLI stops the wallet and event loop,
|
||||
then hard-exits with the proper exit code, so machine callers never hang).
|
||||
- Planned next iteration: `will build/sign/push/check` with an explicit
|
||||
`--yes` confirmation flag for automation.
|
||||
- End-to-end smoke test `tests/test_cli_smoke.py`: creates a REAL testnet
|
||||
wallet via electrum-as-library in a subprocess, then drives
|
||||
add/list/reject-invalid-address/reject-past-locktime/export/remove/status.
|
||||
|
||||
**Verification:**
|
||||
- Full suite against **Electrum 4.7.2**: `290 passed`; against **4.8.0**:
|
||||
`290 passed` (same 2 pre-existing, unrelated `baltx_fees` failures in both).
|
||||
- `ruff`: no new errors on the three new files.
|
||||
|
||||
**Outcome:** DONE.
|
||||
|
||||
---
|
||||
|
||||
## 45. Repo: CLI will commands (build / sign / push / check) - no plugin version change
|
||||
|
||||
**Date:** 2026-07-21
|
||||
|
||||
**Goal:** complete the headless workflow started in entry #44, so a machine can
|
||||
run the whole inheritance cycle without the Qt GUI.
|
||||
|
||||
**What was added (`bal_cli.py`):**
|
||||
- `will build` - builds the will transactions offline through the same
|
||||
`bal.core` pipeline the GUI uses (`Heirs.get_transactions` -> `WillItem` ->
|
||||
`Will.update_will` / `normalize_will`), storing them as "New".
|
||||
- `will sign` - signs the valid, incomplete transactions with the wallet
|
||||
password (`--password` / `BAL_WALLET_PASSWORD`). Prints a summary and asks
|
||||
for confirmation; `--yes` skips it for automation. Chained will inputs
|
||||
(spending a previous will's change) are patched exactly as the GUI does.
|
||||
Watching-only wallets and hardware keystores are rejected with a clear
|
||||
message (hardware devices need physical confirmation).
|
||||
- `will push` - sends signed transactions to the selected will-executors via
|
||||
`Willexecutors.push_transactions_parallel`; `--yes` and `--force` supported;
|
||||
updates PUSHED / PUSH_FAIL statuses.
|
||||
- `will check` - read-only report with **exit codes for scripts**: 0 valid,
|
||||
complete and pushed; 2 no will stored; 3 EXPIRED; 4 not fully signed;
|
||||
5 signed but not pushed; 6 heirs changed since the will was built.
|
||||
- `_CliBalPlugin`: minimal stand-in exposing only what `bal.core` needs
|
||||
headless (`WILLEXECUTORS`, `NO_WILLEXECUTOR`, `get_decimal_point`).
|
||||
|
||||
**Three real bugs found and fixed while implementing this:**
|
||||
1. `Will.only_valid()` returns a GENERATOR - `len()` on it raised
|
||||
`TypeError`; now wrapped in `list()`.
|
||||
2. `copy.deepcopy(tx)` fails on Electrum 4.8 (`cannot pickle '_thread.RLock'`);
|
||||
signing now re-parses the transaction from its serialization instead.
|
||||
3. **Silent will loss on save**: values loaded from the wallet DB are
|
||||
`StoredDict`s carrying the DB lock, and `JsonDB.put` deep-copies its value
|
||||
and returns False *silently* when that fails - the will appeared saved but
|
||||
was never written. Persistence now normalizes through a
|
||||
`json.dumps`/`loads` round-trip (which also validates serializability) and
|
||||
exits with an explicit error if anything is non-serializable.
|
||||
|
||||
Also added: pre-build validation of the selected executor addresses for the
|
||||
active network (refreshed from the server when possible), and a guard that
|
||||
catches exception sentinels embedded by `heirs.py` in a heir entry when an
|
||||
output cannot be built.
|
||||
|
||||
**Verification:**
|
||||
- New `test_cli_will_cycle` in `tests/test_cli_smoke.py`: creates a REAL
|
||||
testnet wallet, funds it offline with a handmade transaction, then drives
|
||||
`build -> check(4) -> sign --yes -> check(5) -> push (aborted at the
|
||||
confirmation prompt) -> status`. No network access.
|
||||
- Full suite: **291 passed** against **Electrum 4.7.2** and **4.8.0** (same 2
|
||||
pre-existing, unrelated `baltx_fees` failures in both).
|
||||
- `ruff`: all checks passed.
|
||||
|
||||
**Note:** repository-only change (CLI + tests). The plugin version is
|
||||
unchanged; the Qt plugin code is untouched.
|
||||
|
||||
**Outcome:** DONE.
|
||||
|
||||
---
|
||||
|
||||
## 46. v0.6.1 - Version read from manifest.json (single source of truth) - PR #4
|
||||
|
||||
**Date:** 2026-07-22
|
||||
|
||||
**Goal (Truman):** stop keeping the version in four places (`bal/VERSION`,
|
||||
`bal/manifest.json`, `bal/__init__.py`, `bal/core/plugin_base.py`) synced by a
|
||||
pre-commit hook. The version must be read at runtime from `manifest.json` only.
|
||||
|
||||
**What changed:**
|
||||
- `bal/core/plugin_base.py`: new module function `get_version()` reads the
|
||||
`"version"` field from `bal/manifest.json` using `importlib.resources`
|
||||
(zip-safe: works both extracted and from inside a zip, which is how Electrum
|
||||
loads external plugins via `zipimport`; no hand-built paths, so no Windows
|
||||
backslash-in-zip issue). The value is cached. `BalPlugin` now exposes a
|
||||
`version` **property** returning `get_version()`; the hardcoded
|
||||
`__version__` class attribute and the old `version()` method that read the
|
||||
`VERSION` file are removed.
|
||||
- The three call sites that printed the version now use the property
|
||||
(`self.bal_window.bal_plugin.version` in `bal/gui/qt/widgets.py` and
|
||||
`bal/gui/qt/dialogs.py`) or `get_version()` where no plugin instance is
|
||||
available (`bal/core/willexecutors.py`, the HTTP user-agent, a static
|
||||
method).
|
||||
- **Removed the `bal/VERSION` file** (the plugin package now ships 36 files
|
||||
instead of 37).
|
||||
- `HANDOFF.md`: updated the four references that told maintainers to edit
|
||||
`bal/VERSION` / keep four files in sync - now they point to
|
||||
`manifest.json` as the single source of truth. (Historical version
|
||||
references in `CHANGELOG.md` and `.agent_memory_tasks.md` are left intact on
|
||||
purpose - they describe past events.)
|
||||
|
||||
**Verification:**
|
||||
- New `tests/test_version_source.py` (7 tests): version equals the manifest
|
||||
field; the `version` property matches `get_version()`; no hardcoded
|
||||
`__version__` remains; no `bal/VERSION` file remains; the value is cached;
|
||||
and - the key one - the version is read correctly when `bal` is imported
|
||||
FROM INSIDE A ZIP in a child interpreter (the real Electrum/Windows
|
||||
scenario).
|
||||
- Full suite: **298 passed** against **Electrum 4.7.2** and **4.8.0** (same 2
|
||||
pre-existing, unrelated `baltx_fees` failures in both).
|
||||
- `ruff`: clean.
|
||||
- Also verified the built distribution zip reports the manifest version via
|
||||
`get_version()` (bumped to 0.6.1 in this change).
|
||||
|
||||
**Note:** repository change targeting PR #4 (branch
|
||||
`bitcoinafterlife-patch-5`). No functional change to inheritance behavior.
|
||||
|
||||
**Outcome:** DONE.
|
||||
|
||||
17
HANDOFF.md
17
HANDOFF.md
@@ -19,7 +19,7 @@
|
||||
services) can be paid a fee to broadcast the inheritance when due. The owner
|
||||
periodically proves they are alive ("check-alive"); if the deadline passes,
|
||||
the inheritance becomes spendable.
|
||||
- **Current version:** see `bal/VERSION` (last shipped: **0.4.8**).
|
||||
- **Current version:** see the `"version"` field of `bal/manifest.json` (the single source of truth; read at runtime via `get_version()` in `bal/core/plugin_base.py`).
|
||||
|
||||
---
|
||||
|
||||
@@ -55,11 +55,10 @@ These are non-negotiable. They come from the owner directly.
|
||||
|
||||
```
|
||||
bal/ <- the plugin package (this is what ships in the ZIP)
|
||||
__init__.py <- __version__ (one of 4 version files)
|
||||
VERSION <- plain-text version (one of 4 version files)
|
||||
manifest.json <- plugin manifest, "version" field (one of 4)
|
||||
__init__.py <- package docstring (no version here anymore)
|
||||
manifest.json <- plugin manifest, "version" field (SINGLE SOURCE OF TRUTH for the version)
|
||||
core/
|
||||
plugin_base.py <- __version__ "AUTOMATICALLY GENERATED" (one of 4)
|
||||
plugin_base.py <- get_version() reads the version from manifest.json (zip-safe)
|
||||
heirs.py <- HEIRS + transaction building (prepare_lists,
|
||||
prepare_transactions, buildTransactions). CORE LOGIC.
|
||||
will.py <- Will/WillItem, validation (check_amounts, check_will),
|
||||
@@ -115,13 +114,11 @@ find bal -name "__pycache__" -type d -exec rm -rf {} + ; find bal -name "*.pyc"
|
||||
python3 build_zip.py bal-electrum-plugin-vX.Y.Z.zip # produces 37 files
|
||||
```
|
||||
|
||||
**Bump version — there are FOUR files, keep them in sync:**
|
||||
**Bump version — ONE file only (single source of truth):**
|
||||
```
|
||||
bal/core/plugin_base.py -> __version__ = "X.Y.Z" # AUTOMATICALLY GENERATED DO NOT EDIT
|
||||
bal/__init__.py -> __version__ = "X.Y.Z"
|
||||
bal/VERSION -> X.Y.Z
|
||||
bal/manifest.json -> "version": "X.Y.Z",
|
||||
```
|
||||
The code reads this at runtime via `get_version()` in `bal/core/plugin_base.py` (exposed as the `BalPlugin.version` property), so there is nothing else to keep in sync. There is no longer a `bal/VERSION` file nor a hardcoded `__version__`.
|
||||
|
||||
**IMPORTANT for the owner when testing:** after installing a ZIP, the owner
|
||||
must **fully restart Electrum** (not just reload the plugin) — Electrum's
|
||||
@@ -292,7 +289,7 @@ must **fully restart Electrum** (not just reload the plugin) — Electrum's
|
||||
## 7. How to resume (checklist for the next AI)
|
||||
|
||||
1. Read this file, then `CHANGELOG.md` (last entries) and `.agent_memory_tasks.md`.
|
||||
2. Confirm the environment: `git status`, current branch, `bal/VERSION`.
|
||||
2. Confirm the environment: `git status`, current branch, and the `"version"` field of `bal/manifest.json`.
|
||||
3. Run the full test suite (Section 3) — expect all green (266 as of v0.4.8).
|
||||
4. Talk to the owner in **Italian**, write everything else in **English**.
|
||||
5. For any change: present a PLAN, wait for "OK" (R4), then implement, test,
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
0.5.18
|
||||
@@ -36,4 +36,7 @@ The plugin supports Electrum 4.7.2 and 4.8.0 with PyQt6. Electrum 4.8.0 removed
|
||||
available and adapts, so both releases keep working.
|
||||
"""
|
||||
|
||||
__version__ = "0.5.18"
|
||||
# The plugin version is NOT defined here. It lives only in ``bal/manifest.json``
|
||||
# (the single source of truth) and is read at runtime via ``get_version()`` in
|
||||
# ``bal/core/plugin_base.py`` (exposed as the ``BalPlugin.version`` property).
|
||||
# Keeping a hardcoded ``__version__`` here would just be a stale duplicate.
|
||||
|
||||
@@ -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__)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1533,7 +1533,7 @@ class BalBuildWillDialog(BalDialog):
|
||||
"BEGIN:VCALENDAR",
|
||||
"VERSION:2.0",
|
||||
"PRODID:-//Bitcoin After Life//Electrum Plugin/"
|
||||
f"{BalPlugin.__version__}",
|
||||
f"{self.bal_window.bal_plugin.version}",
|
||||
]
|
||||
|
||||
total = len(offsets)
|
||||
|
||||
@@ -1153,7 +1153,7 @@ class WillSettingsWidget(QWidget):
|
||||
lines = [
|
||||
"BEGIN:VCALENDAR",
|
||||
"VERSION:2.0",
|
||||
f"PRODID:-//Bitcoin After Life//Electrum Plugin/{BalPlugin.__version__}",
|
||||
f"PRODID:-//Bitcoin After Life//Electrum Plugin/{self.bal_window.bal_plugin.version}",
|
||||
]
|
||||
|
||||
# One separate VEVENT per reminder offset (its own date in the calendar).
|
||||
@@ -1284,7 +1284,7 @@ class WillSettingsWidget(QWidget):
|
||||
"BEGIN:VCALENDAR",
|
||||
"VERSION:2.0",
|
||||
"PRODID:-//Bitcoin After Life//Electrum Plugin/"
|
||||
f"{BalPlugin.__version__}",
|
||||
f"{self.bal_window.bal_plugin.version}",
|
||||
]
|
||||
|
||||
total = len(offsets)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "bal",
|
||||
"fullname": "Bitcoin After Life",
|
||||
"version": "0.5.18",
|
||||
"version": "0.6.1",
|
||||
"description": "Provides free and decentralized Bitcoin inheritance support. Build time-locked 'will' transactions that transfer funds to your heirs if you stop refreshing them (dead-man's switch), optionally relayed by will-executor servers.",
|
||||
"author": "Svatantrya",
|
||||
"licence": "MIT",
|
||||
|
||||
116
tests/test_version_source.py
Normal file
116
tests/test_version_source.py
Normal file
@@ -0,0 +1,116 @@
|
||||
"""Tests for the single-source-of-truth plugin version.
|
||||
|
||||
The plugin version must come ONLY from ``bal/manifest.json`` (no hardcoded
|
||||
copies, no ``bal/VERSION`` file), and it must be readable both from an extracted
|
||||
package and from INSIDE a zip (the way Electrum loads external plugins via
|
||||
zipimport). These tests lock that behavior.
|
||||
|
||||
Run:
|
||||
QT_QPA_PLATFORM=offscreen PYTHONPATH=<electrum-src> \
|
||||
python3 -m pytest tests/test_version_source.py -q
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import zipfile
|
||||
|
||||
import bal
|
||||
from bal.core.plugin_base import BalPlugin, get_version
|
||||
|
||||
_REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
_MANIFEST = os.path.join(_REPO, "bal", "manifest.json")
|
||||
|
||||
|
||||
def _manifest_version():
|
||||
with open(_MANIFEST, encoding="utf-8") as f:
|
||||
return json.load(f)["version"]
|
||||
|
||||
|
||||
def test_get_version_matches_manifest():
|
||||
"""get_version() returns exactly the manifest 'version' field."""
|
||||
assert get_version() == _manifest_version()
|
||||
|
||||
|
||||
def test_get_version_is_not_unknown():
|
||||
"""The manifest must be readable - never the 'unknown' fallback here."""
|
||||
assert get_version() != "unknown"
|
||||
# a plausible semantic version, e.g. 0.6.0
|
||||
assert get_version()[0].isdigit()
|
||||
|
||||
|
||||
def test_version_property_matches_get_version():
|
||||
"""BalPlugin.version is a property returning the same value."""
|
||||
assert isinstance(BalPlugin.version, property)
|
||||
# A bare object works as ``self`` because the property ignores instance
|
||||
# state and simply delegates to get_version().
|
||||
class _Dummy:
|
||||
version = BalPlugin.version
|
||||
assert _Dummy().version == get_version()
|
||||
|
||||
|
||||
def test_no_hardcoded_class_version():
|
||||
"""The old hardcoded BalPlugin.__version__ constant is gone."""
|
||||
assert "__version__" not in vars(BalPlugin)
|
||||
|
||||
|
||||
def test_no_version_file_in_package():
|
||||
"""The bal/VERSION file has been removed (manifest is the only source)."""
|
||||
assert not os.path.exists(os.path.join(_REPO, "bal", "VERSION"))
|
||||
|
||||
|
||||
def test_get_version_cached():
|
||||
"""Second call returns the cached value (same object)."""
|
||||
v1 = get_version()
|
||||
v2 = get_version()
|
||||
assert v1 == v2
|
||||
assert v1 is v2 # cached string, identical object
|
||||
|
||||
|
||||
def test_version_readable_from_inside_zip(tmp_path):
|
||||
"""The version must be readable when 'bal' is imported from a zip.
|
||||
|
||||
Electrum loads external plugins via zipimport, so this is the real
|
||||
production scenario (and the one that used to break on Windows with
|
||||
os.path.join). We build a zip of the bal package and read the version in a
|
||||
fresh interpreter whose only path to 'bal' is that zip.
|
||||
"""
|
||||
bal_dir = os.path.dirname(os.path.abspath(bal.__file__))
|
||||
zip_path = tmp_path / "bal_plugin.zip"
|
||||
with zipfile.ZipFile(zip_path, "w") as z:
|
||||
for root, _dirs, files in os.walk(bal_dir):
|
||||
for fn in files:
|
||||
if fn.endswith(".pyc"):
|
||||
continue
|
||||
full = os.path.join(root, fn)
|
||||
# arcname keeps the leading 'bal/' package prefix
|
||||
arc = os.path.join(
|
||||
"bal", os.path.relpath(full, bal_dir)
|
||||
)
|
||||
z.write(full, arc)
|
||||
|
||||
# electrum must stay importable in the child, but 'bal' must resolve ONLY
|
||||
# from the zip. So: drop the on-disk repo (and this package's dir) from the
|
||||
# child's path, prepend the zip, and run from a neutral working directory.
|
||||
repo_real = os.path.realpath(_REPO)
|
||||
bal_parent_real = os.path.realpath(os.path.dirname(bal_dir))
|
||||
filtered = [
|
||||
p for p in sys.path
|
||||
if p and os.path.realpath(p) not in (repo_real, bal_parent_real)
|
||||
]
|
||||
child_path = os.pathsep.join([str(zip_path)] + filtered)
|
||||
env = dict(os.environ, PYTHONPATH=child_path, QT_QPA_PLATFORM="offscreen")
|
||||
code = (
|
||||
"import bal, os;"
|
||||
"assert 'bal_plugin.zip' in bal.__file__.replace(os.sep, '/'),"
|
||||
" 'bal not loaded from zip: ' + bal.__file__;"
|
||||
"from bal.core.plugin_base import get_version;"
|
||||
"print(get_version())"
|
||||
)
|
||||
out = subprocess.run(
|
||||
[sys.executable, "-c", code], env=env, capture_output=True, text=True,
|
||||
cwd=str(tmp_path),
|
||||
)
|
||||
assert out.returncode == 0, f"child failed: {out.stderr}"
|
||||
assert out.stdout.strip() == _manifest_version()
|
||||
Reference in New Issue
Block a user