lint: ruff cleanup pass across bal/ and tests/

- Sort imports and fix pyproject ruff config (per-file ignores for
  intentional Qt/core exceptions)
- Mark Heirs.validate_* helpers as @staticmethod
- Clean up dead code, rename shadowing vars, use raise ... from
- Add AGENTS.md with env/lint/test/release guidance
This commit is contained in:
2026-07-31 16:03:17 -04:00
parent 649910e599
commit 08394f4868
48 changed files with 494 additions and 292 deletions

View File

@@ -33,7 +33,8 @@ def _active_source_without_strings(module) -> str:
if isinstance(node.value, str) and hasattr(node, "end_lineno"):
self.spans.append((node.lineno, node.end_lineno))
self.generic_visit(node)
s = _S(); s.visit(tree)
s = _S()
s.visit(tree)
drop = set()
for a, b in s.spans:
drop.update(range(a, b + 1))
@@ -45,12 +46,13 @@ def _active_source_without_strings(module) -> str:
def main(pkg: str) -> int:
from PyQt6.QtWidgets import QApplication, QDialog, QWidget
app = QApplication.instance() or QApplication(sys.argv)
_app = QApplication.instance() or QApplication(sys.argv)
wu = importlib.import_module(pkg + ".gui.qt.window_utils")
# top_level_of: returns the top-level container of a child widget
w = QWidget(); child = QWidget(w)
w = QWidget()
child = QWidget(w)
assert wu.top_level_of(child) is w
assert wu.top_level_of(None) is None
print("[OK] top_level_of")

View File

@@ -31,7 +31,7 @@ N = 8 # number of servers
def main():
we_mod = importlib.import_module(f"{PKG}.core.willexecutors")
W = we_mod.Willexecutors
we_cls = we_mod.Willexecutors
# ---- 1) ping_servers_parallel: time ~= slowest, not sum ----
def slow_get_info(url, we, **kwargs):
@@ -43,8 +43,8 @@ def main():
we["status"] = 200
return we
orig_get_info = W.get_info_task
W.get_info_task = staticmethod(slow_get_info)
orig_get_info = we_cls.get_info_task
we_cls.get_info_task = staticmethod(slow_get_info)
try:
wes = {}
for i in range(N):
@@ -57,7 +57,7 @@ def main():
seen.append((url, ok))
start = time.time()
W.ping_servers_parallel(wes, on_each=on_each, max_workers=N)
we_cls.ping_servers_parallel(wes, on_each=on_each, max_workers=N)
elapsed = time.time() - start
# Sequential would take ~ N * SLOW. Parallel must be far less.
@@ -81,15 +81,15 @@ def main():
assert we["status"] == "KO", (url, we)
print("[OK] ping results written back into the willexecutors mapping")
finally:
W.get_info_task = orig_get_info
we_cls.get_info_task = orig_get_info
# ---- 2) push_transactions_parallel: time ~= slowest, not sum ----
def slow_push(we, **kwargs):
time.sleep(SLOW)
return "fail" not in we["url"]
orig_push = W.push_transactions_to_willexecutor
W.push_transactions_to_willexecutor = staticmethod(slow_push)
orig_push = we_cls.push_transactions_to_willexecutor
we_cls.push_transactions_to_willexecutor = staticmethod(slow_push)
try:
wes = {}
for i in range(N):
@@ -106,7 +106,7 @@ def main():
pushed.append((url, ok))
start = time.time()
results = W.push_transactions_parallel(wes, on_each=on_each_push,
results = we_cls.push_transactions_parallel(wes, on_each=on_each_push,
max_workers=N)
elapsed = time.time() - start
@@ -117,11 +117,11 @@ def main():
f"(sequential would be ~{sequential:.2f}s)")
assert len(results) == N, results
for url, (ok, exc) in results.items():
for url, (ok, _exc) in results.items():
assert ok == ("good" in url), (url, ok)
print("[OK] push results correct for every server")
finally:
W.push_transactions_to_willexecutor = orig_push
we_cls.push_transactions_to_willexecutor = orig_push
# ---- 2b) global deadline: a hung server must not block past `deadline` ----
def hanging_push(we, **kwargs):
@@ -129,8 +129,8 @@ def main():
time.sleep(10)
return True
orig_push2 = W.push_transactions_to_willexecutor
W.push_transactions_to_willexecutor = staticmethod(hanging_push)
orig_push2 = we_cls.push_transactions_to_willexecutor
we_cls.push_transactions_to_willexecutor = staticmethod(hanging_push)
try:
wes = {
"https://fast.example": {
@@ -146,7 +146,7 @@ def main():
return True
time.sleep(10)
return True
W.push_transactions_to_willexecutor = staticmethod(fast_or_hang)
we_cls.push_transactions_to_willexecutor = staticmethod(fast_or_hang)
timed_out = []
@@ -154,7 +154,7 @@ def main():
timed_out.append(url)
start = time.time()
W.push_transactions_parallel(
we_cls.push_transactions_parallel(
wes, max_workers=2, deadline=1.0, on_timeout=on_timeout
)
elapsed = time.time() - start
@@ -163,7 +163,7 @@ def main():
print(f"[OK] global deadline enforced: returned in {elapsed:.1f}s, "
f"hung server reported via on_timeout")
finally:
W.push_transactions_to_willexecutor = orig_push2
we_cls.push_transactions_to_willexecutor = orig_push2
# ---- 2c) on_tick is fired periodically from the CALLING thread ----
# The elapsed-time counter is driven by an on_tick callback called from the
@@ -175,8 +175,8 @@ def main():
time.sleep(SLOW * 6) # ~3s, long enough for several ticks
return True
orig_push3 = W.push_transactions_to_willexecutor
W.push_transactions_to_willexecutor = staticmethod(slow_push2)
orig_push3 = we_cls.push_transactions_to_willexecutor
we_cls.push_transactions_to_willexecutor = staticmethod(slow_push2)
try:
wes = {
"https://tick.example": {
@@ -191,7 +191,7 @@ def main():
ticks.append(time.time())
tick_threads.add(threading.current_thread())
W.push_transactions_parallel(
we_cls.push_transactions_parallel(
wes, max_workers=1, on_tick=on_tick, tick_interval=0.5
)
# ~3s push with 0.5s ticks => at least a few ticks.
@@ -202,7 +202,7 @@ def main():
)
print(f"[OK] on_tick fired {len(ticks)} times from the calling thread")
finally:
W.push_transactions_to_willexecutor = orig_push3
we_cls.push_transactions_to_willexecutor = orig_push3
# ---- 2d) check_transactions_parallel: parallel + deadline + on_tick ----
# Pressing "Check" verifies each will-executor still holds its tx. This used
@@ -214,8 +214,8 @@ def main():
time.sleep(SLOW)
return {"tx": "ok"} if "good" in url else None
orig_check = W.check_transaction
W.check_transaction = staticmethod(slow_check)
orig_check = we_cls.check_transaction
we_cls.check_transaction = staticmethod(slow_check)
try:
targets = []
for i in range(N):
@@ -228,7 +228,7 @@ def main():
checked.append((wid, res))
start = time.time()
results = W.check_transactions_parallel(
results = we_cls.check_transactions_parallel(
targets, on_each=on_each_check, max_workers=N
)
elapsed = time.time() - start
@@ -239,7 +239,7 @@ def main():
print(f"[OK] check parallel: {elapsed:.2f}s for {N} servers "
f"(sequential would be ~{sequential:.2f}s)")
finally:
W.check_transaction = orig_check
we_cls.check_transaction = orig_check
# 2d-bis) global deadline + on_tick from the calling thread
def hanging_check(txid, url, **kwargs):
@@ -248,8 +248,8 @@ def main():
time.sleep(10)
return {"tx": "ok"}
orig_check2 = W.check_transaction
W.check_transaction = staticmethod(hanging_check)
orig_check2 = we_cls.check_transaction
we_cls.check_transaction = staticmethod(hanging_check)
try:
targets = [
("idf", "https://fast.example"),
@@ -268,7 +268,7 @@ def main():
tick_threads.add(threading.current_thread())
start = time.time()
W.check_transactions_parallel(
we_cls.check_transactions_parallel(
targets, max_workers=2, deadline=2.0,
on_timeout=on_timeout_check, on_tick=on_tick_check,
tick_interval=0.5,
@@ -282,7 +282,7 @@ def main():
print(f"[OK] check global deadline enforced ({elapsed:.1f}s), on_tick "
f"fired {len(ticks)}x from the calling thread")
finally:
W.check_transaction = orig_check2
we_cls.check_transaction = orig_check2
# ---- 3) the wizard's loop_push must use the parallel helper ----
# The "Building Will" wizard broadcasts via BalBuildWillDialog.loop_push.

View File

@@ -17,8 +17,8 @@ import sys
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
from PyQt6.QtWidgets import QApplication, QLabel, QWidget, QVBoxLayout
from PyQt6.QtCore import Qt
from PyQt6.QtWidgets import QApplication, QLabel, QVBoxLayout, QWidget
# Same colors as BalBuildWillDialog
COLOR_WARNING = "#cfa808"

View File

@@ -17,10 +17,15 @@ import os
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
from PyQt6.QtWidgets import ( # noqa: E402
QApplication, QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton,
)
from PyQt6.QtCore import Qt # noqa: E402
from PyQt6.QtWidgets import ( # noqa: E402
QApplication,
QHBoxLayout,
QLabel,
QPushButton,
QVBoxLayout,
QWidget,
)
COLOR_OK = "#05ad05"

View File

@@ -21,8 +21,8 @@ import sys
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
from PyQt6.QtWidgets import QApplication, QLabel, QWidget, QVBoxLayout
from PyQt6.QtCore import Qt
from PyQt6.QtWidgets import QApplication, QLabel, QVBoxLayout, QWidget
COLOR_ERROR = "#ff0000"
COLOR_OK = "#05ad05"

View File

@@ -20,12 +20,17 @@ import os
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
from PyQt6.QtWidgets import ( # noqa: E402
QApplication, QWidget, QHBoxLayout, QPushButton, QComboBox, QLineEdit,
QLabel,
)
from PyQt6.QtCore import QSize # noqa: E402
from PyQt6.QtGui import QIcon, QPixmap # noqa: E402
from PyQt6.QtCore import QSize, Qt # noqa: E402
from PyQt6.QtWidgets import ( # noqa: E402
QApplication,
QComboBox,
QHBoxLayout,
QLabel,
QLineEdit,
QPushButton,
QWidget,
)
ICON_PATH = os.path.join(os.path.dirname(__file__), "..", "bal", "icons",
"wizard.png")

View File

@@ -27,12 +27,19 @@ import os
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
from PyQt6.QtWidgets import ( # noqa: E402
QApplication, QWidget, QVBoxLayout, QHBoxLayout, QToolButton, QComboBox,
QLineEdit, QSpinBox, QLabel,
)
from PyQt6.QtGui import QFontMetrics # noqa: E402
from PyQt6.QtCore import Qt # noqa: E402
from PyQt6.QtGui import QFontMetrics # noqa: E402
from PyQt6.QtWidgets import ( # noqa: E402
QApplication,
QComboBox,
QHBoxLayout,
QLabel,
QLineEdit,
QSpinBox,
QToolButton,
QVBoxLayout,
QWidget,
)
def _char_w():

View File

@@ -21,18 +21,21 @@ Run:
QT_QPA_PLATFORM=offscreen PYTHONPATH=electrum-src python3 tests/sim_update_flows.py
"""
import sys
import os
import copy
import os
import sys
sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir))
from bal.core.will import (
WillItem, Will,
NotCompleteWillException, HeirNotFoundException, NoHeirsException,
TxFeesChangedException, WillExpiredException,
HeirNotFoundException,
NoHeirsException,
NotCompleteWillException,
TxFeesChangedException,
Will,
WillExpiredException,
WillItem,
)
from bal.core.util import Util
# A valid serialized tx (1 input + 1 output, version 2). Its nLockTime is 0.
_VALID_TX_HEX = (

View File

@@ -26,30 +26,28 @@ def main():
from PyQt6.QtWidgets import QApplication # noqa
_app = QApplication.instance() or QApplication([])
results = {}
# 1) Core modules import (these must be GUI-free).
bal = imp_core("bal", "core.plugin_base")
util = imp_core("util", "core.util")
heirs = imp_core("heirs", "core.heirs")
will = imp_core("will", "core.will")
we = imp_core("willexecutors", "core.willexecutors")
_we = imp_core("willexecutors", "core.willexecutors")
# 2) GUI module imports.
qt = imp_gui()
# 3) Behaviour checks (pure logic, must be identical across versions).
BalTimestamp = bal.BalTimestamp
assert BalTimestamp("30d").duration_to_days() == 30, "BalTimestamp 30d"
assert BalTimestamp("1y").duration_to_days() == 365, "BalTimestamp 1y"
assert str(BalTimestamp("7d")) == "7d", "BalTimestamp str"
bal_timestamp = bal.BalTimestamp
assert bal_timestamp("30d").duration_to_days() == 30, "BalTimestamp 30d"
assert bal_timestamp("1y").duration_to_days() == 365, "BalTimestamp 1y"
assert str(bal_timestamp("7d")) == "7d", "BalTimestamp str"
Util = util.Util
assert Util.is_perc("50%") is True
assert Util.is_perc("100") is False
assert Util.text_to_hex("BAL") == "42414c"
assert Util.hex_to_text("42414c") == "BAL"
assert Util.int_locktime(days=1) == 86400
util_cls = util.Util
assert util_cls.is_perc("50%") is True
assert util_cls.is_perc("100") is False
assert util_cls.text_to_hex("BAL") == "42414c"
assert util_cls.hex_to_text("42414c") == "BAL"
assert util_cls.int_locktime(days=1) == 86400
# heirs constants must keep the same column layout (very delicate!)
assert heirs.HEIR_ADDRESS == 0

View File

@@ -27,19 +27,19 @@ Run:
tests/test_anticipate_manual_locktime.py -q
"""
import sys
import os
import copy
import os
import sys
sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir))
import pytest # noqa: E402
from bal.core.will import ( # noqa: E402
WillItem,
Will,
NotCompleteWillException,
Will,
WillExpiredException,
WillItem,
)
# A valid serialized tx (1 input + 1 output, version 2).

View File

@@ -22,13 +22,13 @@ whether a fix is needed. Run:
python3 -m pytest tests/test_anticipate_past_locktime.py -q
"""
import sys
import os
import sys
import time
sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir))
from bal.core.util import Util, LOCKTIME_THRESHOLD
from bal.core.util import LOCKTIME_THRESHOLD, Util
# ---------------------------------------------------------------------------

View File

@@ -9,25 +9,34 @@ Run:
python3 tests/test_core_heirs.py
"""
import sys
import os
import sys
sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir))
from bal.core.heirs import (
HEIR_ADDRESS, HEIR_AMOUNT, HEIR_LOCKTIME, HEIR_REAL_AMOUNT,
HEIR_DUST_AMOUNT, TRANSACTION_LABEL,
HEIR_ADDRESS,
HEIR_AMOUNT,
HEIR_DUST_AMOUNT,
HEIR_LOCKTIME,
HEIR_REAL_AMOUNT,
OP_RETURN_PREFIX,
create_op_return_script,
is_op_return_address, get_op_return_hex, validate_op_return_hex,
TRANSACTION_LABEL,
AliasNotFoundException,
NotAnAddress, AmountNotValid, LocktimeNotValid,
HeirExpiredException, HeirAmountIsDustException,
NoHeirsException, WillExecutorFeeException,
AmountNotValid,
BalanceTooLowException,
HeirAmountIsDustException,
Heirs,
LocktimeNotValid,
NoHeirsException,
NotAnAddress,
WillExecutorFeeException,
create_op_return_script,
get_op_return_hex,
is_op_return_address,
validate_op_return_hex,
)
# ------------------------------------------------------------------ #
# Constants
# ------------------------------------------------------------------ #
@@ -70,7 +79,7 @@ def test_op_return_empty():
def test_op_return_too_big():
try:
create_op_return_script("ab" * 81) # 81 bytes > max 80
assert False, "expected ValueError"
raise AssertionError("expected ValueError")
except ValueError:
pass
@@ -179,13 +188,13 @@ def test_validate_amount():
# Invalid
try:
Heirs.validate_amount("0.000000001")
assert False, "expected AmountNotValid"
raise AssertionError("expected AmountNotValid")
except AmountNotValid:
pass
try:
Heirs.validate_amount("-1")
assert False, "expected AmountNotValid"
raise AssertionError("expected AmountNotValid")
except AmountNotValid:
pass
@@ -209,7 +218,7 @@ def test_validate_locktime_expired():
past = int(time.time()) - 86400 # yesterday
try:
Heirs.validate_locktime(str(past), timestamp_to_check=past + 1)
assert False, "expected LocktimeNotValid"
raise AssertionError("expected LocktimeNotValid")
except LocktimeNotValid:
pass
@@ -289,7 +298,7 @@ def test_validate_op_return_hex_valid():
def test_validate_op_return_hex_invalid():
try:
validate_op_return_hex("nothex!!")
assert False, "expected NotAnAddress"
raise AssertionError("expected NotAnAddress")
except NotAnAddress:
pass
@@ -297,7 +306,7 @@ def test_validate_op_return_hex_invalid():
def test_validate_op_return_hex_too_long():
try:
validate_op_return_hex("ab" * 81)
assert False, "expected NotAnAddress"
raise AssertionError("expected NotAnAddress")
except NotAnAddress:
pass
@@ -355,4 +364,4 @@ if __name__ == "__main__":
if name.startswith("test_"):
globals()[name]()
print(f" [OK] {name}")
print(f"[OK] All heirs tests passed")
print("[OK] All heirs tests passed")

View File

@@ -8,19 +8,20 @@ Run:
QT_QPA_PLATFORM=offscreen python3 tests/test_core_heirs_extra.py
"""
import sys
import os
import sys
from unittest.mock import MagicMock, patch
sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir))
from bal.core.heirs import (
Heirs, create_op_return_script, reduce_outputs,
HEIR_ADDRESS, HEIR_AMOUNT, HEIR_LOCKTIME, HEIR_REAL_AMOUNT,
HEIR_AMOUNT,
Heirs,
create_op_return_script,
reduce_outputs,
)
from bal.core.willexecutors import Willexecutors
# ------------------------------------------------------------------ #
# Heirs db-dependent methods
# ------------------------------------------------------------------ #
@@ -188,7 +189,7 @@ def test_validate_address_invalid():
from bal.core.heirs import NotAnAddress
try:
Heirs.validate_address("bad")
assert False, "should have raised"
raise AssertionError("should have raised")
except NotAnAddress:
pass

View File

@@ -8,14 +8,15 @@ Run:
python3 tests/test_core_plugin_base.py
"""
import sys
import os
import sys
import time
sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir))
from datetime import datetime, date, timedelta
from bal.core.plugin_base import BalTimestamp, BalPlugin, BalConfig
from datetime import date, datetime, timedelta
from bal.core.plugin_base import BalConfig, BalPlugin, BalTimestamp
# ------------------------------------------------------------------ #
# BalTimestamp

View File

@@ -9,13 +9,14 @@ Run:
python3 tests/test_core_util.py
"""
import sys
import os
import sys
sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir))
import pytest
from bal.core.util import Util, LOCKTIME_THRESHOLD
from bal.core.util import Util
def test_locktime_to_str():
@@ -40,7 +41,7 @@ def test_str_to_locktime():
# the block-height suffix "b" was removed (A1): "144b" is no longer a valid
# relative locktime, so it is NOT passed through unchanged.
with pytest.raises(Exception):
with pytest.raises(ValueError):
Util.str_to_locktime("144b")
# integer string -> int
@@ -347,44 +348,44 @@ def test_in_utxo():
def test_cmp_output():
class O:
class Obj:
def __init__(self, addr, val):
self.address = addr
self.value = val
assert Util.cmp_output(O("a", 100), O("a", 100)) is True
assert Util.cmp_output(O("a", 100), O("b", 100)) is False
assert Util.cmp_output(O("a", 100), O("a", 200)) is False
assert Util.cmp_output(Obj("a", 100), Obj("a", 100)) is True
assert Util.cmp_output(Obj("a", 100), Obj("b", 100)) is False
assert Util.cmp_output(Obj("a", 100), Obj("a", 200)) is False
def test_in_output():
class O:
class Obj:
def __init__(self, addr, val):
self.address = addr
self.value = val
outputs = [O("a", 100), O("b", 200)]
assert Util.in_output(O("a", 100), outputs) is True
assert Util.in_output(O("z", 999), outputs) is False
assert Util.in_output(O("a", 100), []) is False
outputs = [Obj("a", 100), Obj("b", 200)]
assert Util.in_output(Obj("a", 100), outputs) is True
assert Util.in_output(Obj("z", 999), outputs) is False
assert Util.in_output(Obj("a", 100), []) is False
def test_din_output():
class O:
class Obj:
def __init__(self, addr, val):
self.address = addr
self.value = val
outputs = [O("a", 100), O("b", 200)]
outputs = [Obj("a", 100), Obj("b", 200)]
# same amount AND same address
same_amt, same_addr = Util.din_output(O("a", 100), outputs)
same_amt, same_addr = Util.din_output(Obj("a", 100), outputs)
assert same_amt is True and same_addr is True
# same amount but different address
same_amt, same_addr = Util.din_output(O("c", 100), outputs)
same_amt, same_addr = Util.din_output(Obj("c", 100), outputs)
assert same_amt is True and same_addr is False
# different amount
same_amt, same_addr = Util.din_output(O("z", 999), outputs)
same_amt, same_addr = Util.din_output(Obj("z", 999), outputs)
assert same_amt is False and same_addr is False

View File

@@ -8,13 +8,13 @@ Run:
python3 tests/test_core_will.py
"""
import sys
import os
import copy
import os
import sys
sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir))
from bal.core.will import WillItem, Will
from bal.core.willexecutors import Willexecutors
from bal.core.will import Will, WillItem
# A valid serialized Bitcoin transaction hex (1 input + 1 P2PKH output, version 2)
_VALID_TX_HEX = (
@@ -332,11 +332,19 @@ def test_will_check_tx_height():
def test_exceptions():
from bal.core.will import (
WillException, WillExpiredException, NotCompleteWillException,
HeirChangeException, TxFeesChangedException, HeirNotFoundException,
WillexecutorChangeException, NoWillExecutorNotPresent,
WillExecutorNotPresent, NoHeirsException,
AmountException, PercAmountException, FixedAmountException,
AmountException,
FixedAmountException,
HeirChangeException,
HeirNotFoundException,
NoHeirsException,
NotCompleteWillException,
NoWillExecutorNotPresent,
PercAmountException,
TxFeesChangedException,
WillException,
WillexecutorChangeException,
WillExecutorNotPresent,
WillExpiredException,
WillPostponedException,
)
@@ -375,4 +383,4 @@ if __name__ == "__main__":
if name.startswith("test_"):
globals()[name]()
print(f" [OK] {name}")
print(f"[OK] All Will tests passed")
print("[OK] All Will tests passed")

View File

@@ -8,15 +8,16 @@ Run:
QT_QPA_PLATFORM=offscreen python3 tests/test_core_will_extra.py
"""
import sys
import os
from unittest.mock import MagicMock, patch, PropertyMock, call
import sys
from unittest.mock import MagicMock, patch
sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir))
from bal.core.will import Will, WillItem
from electrum.transaction import Transaction
from bal.core.will import Will, WillItem
_VALID_TX_HEX = (
"01000000012a5c9a94fcde98f5581cd00162c60a13936ceb75389ea65b"
"f38633b424eb4031000000006c493046022100a82bbc57a0136751e543"

View File

@@ -22,15 +22,14 @@ import os
import sys
from unittest.mock import MagicMock, patch
import pytest
sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir))
from bal.core.will import Will, WillItem
# Patch Transaction.add_info_from_wallet so WillItem can parse the tx hex
# without a live Electrum wallet connection.
from electrum.transaction import Transaction
from bal.core.will import Will, WillItem
_patcher = patch.object(Transaction, "add_info_from_wallet")
_patcher.start()

View File

@@ -28,7 +28,6 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir))
from bal.core.plugin_base import BalConfig
from bal.core.willexecutors import Willexecutors
# ------------------------------------------------------------------ #
# Mocks
# ------------------------------------------------------------------ #

View File

@@ -27,7 +27,6 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir))
from bal.core.plugin_base import BalConfig
# ------------------------------------------------------------------ #
# Mocks
# ------------------------------------------------------------------ #

View File

@@ -27,7 +27,6 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir))
from bal.core.plugin_base import BalConfig
from bal.gui.qt.widgets import compute_reminder_offsets
# ------------------------------------------------------------------ #
# Mocks
# ------------------------------------------------------------------ #

View File

@@ -25,7 +25,6 @@ import copy
import json
import os
import sys
import warnings
import pytest
@@ -42,15 +41,12 @@ from electrum import bitcoin
from electrum.transaction import (
PartialTransaction,
PartialTxInput,
PartialTxOutput,
TxOutpoint,
)
from electrum.util import bfh
from bal.core.heirs import Heirs
from bal.core.will import Will, WillItem
from bal.core.willexecutors import Willexecutors
# ------------------------------------------------------------------ #
# Load karen7 wallet data
@@ -166,7 +162,7 @@ def _build_utxo_value_map(data):
for txid, outputs in txo.items():
if not isinstance(outputs, dict):
continue
for addr, out_map in outputs.items():
for _, out_map in outputs.items():
if not isinstance(out_map, dict):
continue
for idx, info in out_map.items():
@@ -188,7 +184,7 @@ def _populate_input_values(will, utxo_value_map):
This is the equivalent of what ``add_info_from_wallet`` does in the
real flow: looking up the UTXO value and attaching it to the input.
"""
for wid, wi in will.items():
for _, wi in will.items():
for txin in wi.tx.inputs():
prevout_str = txin.prevout.to_str()
if txin._trusted_value_sats is None and prevout_str in utxo_value_map:

View File

@@ -36,13 +36,12 @@ import pytest
# below the repo root that contains the ``bal`` package).
sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir))
from bal.core.will import WillItem, Will, HeirNotFoundException
from bal.core.heirs import Heirs
from bal.core.will import HeirNotFoundException, Will, WillItem
from bal.core.willexecutors import Willexecutors
from bal.gui.qt.calendar import BalCalendar
from bal.gui.qt.widgets import compute_reminder_offsets
# A valid serialized Bitcoin transaction hex (1 input + 1 P2PKH output,
# version 2). Reused from test_core_will.py so WillItem can parse a real tx.
_VALID_TX_HEX = (
@@ -550,7 +549,7 @@ def test_e5_build_with_real_wallet_heirs_and_utxos():
for txid, outputs in txo.items():
if not isinstance(outputs, dict):
continue
for addr, out_map in outputs.items():
for _, out_map in outputs.items():
if not isinstance(out_map, dict):
continue
for idx, info in out_map.items():

View File

@@ -16,8 +16,9 @@ Run:
python3 tests/test_group_f_heir_change_rebuild.py
"""
import sys
import os
import sys
sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir))
from bal.core.will import Will

View File

@@ -24,8 +24,7 @@ import sys
sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir))
from bal.gui.qt.widgets import (BASIC_REMINDER_OFFSETS,
basic_reminder_offsets)
from bal.gui.qt.widgets import BASIC_REMINDER_OFFSETS, basic_reminder_offsets
def test_basic_offsets_all_future():

View File

@@ -32,7 +32,6 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir))
from bal.gui.qt.window import BalWindow # noqa: E402 (path insert above)
OFFSET = BalWindow.BASIC_MODE_CHECK_ALIVE_OFFSET_SECONDS

View File

@@ -10,14 +10,12 @@ Run:
import os
import sys
import tempfile
from datetime import datetime, timezone
sys.path.insert(0, __file__.rsplit("/", 2)[0])
from bal.gui.qt.calendar import BalCalendar
# ------------------------------------------------------------------ #
# format_time
# ------------------------------------------------------------------ #

View File

@@ -8,12 +8,13 @@ Run:
"""
import sys
sys.path.insert(0, __file__.rsplit("/", 2)[0])
from PyQt6.QtWidgets import QApplication, QGridLayout, QLabel, QWidget
from PyQt6.QtWidgets import QApplication, QGridLayout, QLabel
# Import the module itself, not via "from .common import *"
import bal.gui.qt.common as C
import bal.gui.qt.common as common
_app = QApplication.instance() or QApplication(sys.argv)
@@ -23,18 +24,18 @@ _app = QApplication.instance() or QApplication(sys.argv)
# ------------------------------------------------------------------ #
def test_shown_cv_default():
cv = C.shown_cv(True)
cv = common.shown_cv(True)
assert cv.get() is True
def test_shown_cv_set():
cv = C.shown_cv(True)
cv = common.shown_cv(True)
cv.set(False)
assert cv.get() is False
def test_shown_cv_roundtrip():
cv = C.shown_cv(False)
cv = common.shown_cv(False)
assert cv.get() is False
cv.set(True)
assert cv.get() is True
@@ -47,19 +48,19 @@ def test_shown_cv_roundtrip():
# ------------------------------------------------------------------ #
def test_check_alive_error_default():
err = C.CheckAliveError(1000000)
err = common.CheckAliveError(1000000)
assert err.timestamp_to_check == 1000000
def test_check_alive_error_str():
err = C.CheckAliveError(1000000)
err = common.CheckAliveError(1000000)
s = str(err)
assert "Check alive expired" in s
assert "1970" in s
def test_check_alive_error_subclass():
assert issubclass(C.CheckAliveError, Exception)
assert issubclass(common.CheckAliveError, Exception)
# ------------------------------------------------------------------ #
@@ -68,17 +69,15 @@ def test_check_alive_error_subclass():
def test_add_widget():
grid = QGridLayout()
parent = QWidget()
label = QLabel("test")
C.add_widget(grid, "Label", label, 0, "Help text")
common.add_widget(grid, "Label", label, 0, "Help text")
assert grid.count() == 3 # label + widget + help button
def test_add_widget_multiple_rows():
grid = QGridLayout()
parent = QWidget()
C.add_widget(grid, "A", QLabel("a"), 0, "help_a")
C.add_widget(grid, "B", QLabel("b"), 1, "help_b")
common.add_widget(grid, "A", QLabel("a"), 0, "help_a")
common.add_widget(grid, "B", QLabel("b"), 1, "help_b")
assert grid.count() == 6
@@ -87,7 +86,7 @@ def test_add_widget_multiple_rows():
# ------------------------------------------------------------------ #
def test_log_error_no_window():
C.log_error((Exception, Exception("test"), None))
common.log_error((Exception, Exception("test"), None))
# ------------------------------------------------------------------ #

View File

@@ -8,11 +8,11 @@ Run:
"""
import sys
sys.path.insert(0, __file__.rsplit("/", 2)[0])
from bal.gui.qt.theme import status_color
# ------------------------------------------------------------------ #
# Helpers
# ------------------------------------------------------------------ #

View File

@@ -13,13 +13,11 @@ Run:
"""
import sys
sys.path.insert(0, __file__.rsplit("/", 2)[0])
from PyQt6.QtCore import QTimer
from PyQt6.QtWidgets import QApplication, QWidget
from electrum.util import DECIMAL_POINT, decimal_point_to_base_unit_name
_app = QApplication.instance() or QApplication(sys.argv)

View File

@@ -8,13 +8,18 @@ Run:
"""
import sys
sys.path.insert(0, __file__.rsplit("/", 2)[0])
from PyQt6.QtCore import QTimer
from PyQt6.QtWidgets import QApplication, QDialog, QWidget
from bal.gui.qt.window_utils import (
bring_to_front, show_modal, show_on_top, stop_thread, top_level_of,
bring_to_front,
show_modal,
show_on_top,
stop_thread,
top_level_of,
)
_app = QApplication.instance() or QApplication(sys.argv)

View File

@@ -33,10 +33,9 @@ import logging
import os
import sys
import time
from unittest.mock import MagicMock, patch
from unittest.mock import patch
import pytest
from electrum import constants
constants.net = constants.BitcoinRegtest
@@ -48,10 +47,14 @@ from electrum.transaction import PartialTxInput, TxOutpoint
from electrum.util import bfh
from bal.core.heirs import Heirs
from bal.core.will import Will, WillItem, NoWillExecutorNotPresent, NotCompleteWillException
from bal.core.will import (
NotCompleteWillException,
NoWillExecutorNotPresent,
Will,
WillItem,
)
from bal.core.willexecutors import Willexecutors
# ------------------------------------------------------------------ #
# Load karen7 wallet data
# ------------------------------------------------------------------ #
@@ -479,11 +482,11 @@ class TestNoWillexecutorKaren7:
dialog = FakeBuildWillDialog(self.bal_window)
dialog.task_phase1()
assert any(
"Not present - select one or enable backup mode" in l
for l in dialog.labels
"Not present - select one or enable backup mode" in label
for label in dialog.labels
), "dialog labels must contain the 'not present' message"
assert any(
"#ff0000" in l for l in dialog.labels
"#ff0000" in label for label in dialog.labels
), "dialog labels must use red (COLOR_ERROR)"
def test_task_phase1_adds_action_buttons(self):

View File

@@ -23,9 +23,15 @@ if os.path.isdir(ELECTRUM_DIR):
sys.path.insert(0, ELECTRUM_DIR)
from bal.core.heirs import Heirs
from bal.core.willexecutors import Willexecutors
from bal.core.will import Will, WillItem, NotCompleteWillException, NoHeirsException, NoWillExecutorNotPresent
from bal.core.plugin_base import BalPlugin, BalTimestamp
from bal.core.will import (
NoHeirsException,
NotCompleteWillException,
NoWillExecutorNotPresent,
Will,
WillItem,
)
from bal.core.willexecutors import Willexecutors
# ------------------------------------------------------------------ #
# Load karen7 wallet data
@@ -53,7 +59,7 @@ def build_utxos(data):
for txid, outputs in txo.items():
if not isinstance(outputs, dict):
continue
for addr, out_map in outputs.items():
for _, out_map in outputs.items():
if not isinstance(out_map, dict):
continue
for idx, info in out_map.items():
@@ -85,7 +91,6 @@ class FakeBalWindow:
def init_class_variables(self):
if not self.heirs:
raise NoHeirsException("Heirs are not defined")
from bal.core.plugin_base import BalTimestamp
from datetime import datetime
self.date_to_check = BalTimestamp(self.will_settings['threshold']).to_timestamp()
self.no_willexecutor = self.bal_plugin.NO_WILLEXECUTOR.get()

View File

@@ -45,10 +45,10 @@ class _WindowsLikeDatetime(_real_datetime):
def main():
plugin_base = importlib.import_module(f"{PKG}.core.plugin_base")
BalTimestamp = plugin_base.BalTimestamp
bt_class = plugin_base.BalTimestamp
# 1) Sanity: with the real (Linux 64-bit) datetime, large ts works already.
bt = BalTimestamp(NLOCKTIME_MAX)
bt = bt_class(NLOCKTIME_MAX)
d = bt.to_date()
assert isinstance(d, _real_datetime), d
print("[OK] BalTimestamp(NLOCKTIME_MAX).to_date() works on this platform")
@@ -59,7 +59,7 @@ def main():
plugin_base.datetime = _WindowsLikeDatetime
try:
# 2a) Absolute sentinel timestamp (the exact crash path from the log).
bt = BalTimestamp(NLOCKTIME_MAX)
bt = bt_class(NLOCKTIME_MAX)
d = bt.to_date() # must NOT raise OverflowError anymore
assert d.year <= 2038, f"expected clamp to <=2038, got {d!r}"
print("[OK] to_date(NLOCKTIME_MAX) no longer raises (clamped to INT32_MAX)")
@@ -75,13 +75,13 @@ def main():
print("[OK] str()/repr() on out-of-range timestamp are safe")
# 2d) Relative durations that overflow when added (e.g. huge 'd').
bt_rel = BalTimestamp(f"{10 ** 9}d") # ~2.7M years -> overflow
bt_rel = bt_class(f"{10 ** 9}d") # ~2.7M years -> overflow
d2 = bt_rel.to_date()
assert d2 is not None
print("[OK] huge relative duration no longer raises")
# 2e) Normal values are unchanged (behaviour-preserving check).
bt_norm = BalTimestamp("90d")
bt_norm = bt_class("90d")
d3 = bt_norm.to_date()
# 90 days from now, normalised to midnight
assert d3.hour == 0 and d3.minute == 0 and d3.second == 0