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

76
AGENTS.md Normal file
View File

@@ -0,0 +1,76 @@
# AGENTS.md
BAL — Bitcoin After Life, an Electrum plugin (inheritance / dead-man's-switch).
Source-of-truth docs: `README.md`, `HANDOFF.md`, `COMPATIBILITY.md`.
## Environments (critical)
Two separate venvs; using the wrong one is the #1 mistake.
- **Runtime env** (Electrum + PyQt6, has `electrum` importable):
`source /home/steal/devel/bal/electrum/env/bin/activate`
This is an editable install of the Electrum 4.8.0 checkout at
`/home/steal/devel/bal/electrum`. Use it for anything that imports
`electrum`, runs GUI code, or runs tests.
- **Lint venv** (repo-local `venv/`): ruff, black, flake8 only. It cannot
import `electrum` or `PyQt6`. Do NOT use it to run tests.
The plugin's `bal/` directory is symlinked into
`electrum/electrum/plugins/bal` (internal-plugin install used during dev).
## Test & verify
Tests are **standalone scripts**, not pytest. Each `tests/test_*.py` file runs
its `test_*` functions from `if __name__ == "__main__"`. Run a file directly:
```bash
source /home/steal/devel/bal/electrum/env/bin/activate
python3 tests/test_core_heirs.py # core, no Qt needed
QT_QPA_PLATFORM=offscreen python3 tests/test_gui_common.py # GUI tests need offscreen
```
- Most core tests run offline (no wallet/network). Some files
(`test_group_*.py`, `test_no_willexecutor_karen7.py`, `parallel_ping_test.py`)
exercise will-executor/network flows and need the live servers — don't rely on
them for quick verification.
- `tests/smoke_test.py` proves clean import under real Electrum:
`QT_QPA_PLATFORM=offscreen python3 tests/smoke_test.py electrum.plugins.bal`
- `tests/external_zip_test.py` loads the built zip the way Electrum's plugin
dialog does (`electrum_external_plugins.bal`); run it after `build_zip.py`.
## Lint / typecheck
- **Ruff is NOT clean** (hundreds of pre-existing errors in `bal/` and
`tests/`). Do not run `--fix` wholesale and do not try to silence everything;
just avoid adding new violations. Config: `pyproject.toml` (line-length 88,
E501 ignored).
- Lint via the repo venv: `/home/steal/devel/bal/bal-electrum-plugin/venv/bin/ruff`
- Typecheck: `pyright` (npm, `node_modules/`), config `pyrightconfig.json`
(`extraPaths: ["../electrum"]`). Pyright reports many false positives on
dynamically-attached attrs (e.g. `self.window`, `BalPlugin.*`); don't chase
them.
## Architecture
- `bal/core/` = GUI-free logic (`heirs.py`, `will.py`, `willexecutors.py`,
`plugin_base.py`, `util.py`). Must never import Qt.
- `bal/gui/qt/` = PyQt6 layer. `window.py` is the per-wallet controller,
`plugin.py` is the Electrum `@hooks` entry, `qt.py` is a zipimport shim.
- `bal/manifest.json` = version source of truth (Electrum reads it; also read by
`make-release.sh`).
- Compatibility constraint: must support Electrum **4.7.2 and 4.8.0**; the DB
registration API differs between them (`json_db.register_dict` vs
`stored_dict.register_name`).
## Build / release
```bash
python3 build_zip.py # -> bal-electrum-plugin.zip (deterministic, prints sha256)
./make-release.sh [v0.x.y] # bump manifest version, tag, sign, push Gitea release
```
- `make-release.sh` requires gpg and Gitea credentials (`~/.git-credentials`
or `GITEA_USER`/`GITEA_TOKEN`). It bumps `bal/manifest.json` — bump the
version there, never invent a new source of truth.
- Remote is Gitea (`origin` = bitcoin-after.life). `.env` holds a Gitea token
(gitignored, never commit it).

View File

@@ -102,7 +102,7 @@ def validate_op_return_hex(data_hex: str) -> None:
try: try:
data = bytes.fromhex(data_hex) data = bytes.fromhex(data_hex)
except ValueError: except ValueError:
raise NotAnAddress(f"OP_RETURN data is not valid hex: {data_hex}") raise NotAnAddress(f"OP_RETURN data is not valid hex: {data_hex}") from None
if len(data) > 80: if len(data) > 80:
raise NotAnAddress( raise NotAnAddress(
f"OP_RETURN data too long ({len(data)} bytes, max 80)" f"OP_RETURN data too long ({len(data)} bytes, max 80)"
@@ -205,7 +205,7 @@ def prepare_transactions(locktimes, available_utxos, fees, wallet):
change = get_change_output(wallet, in_amount, out_amount, fee) change = get_change_output(wallet, in_amount, out_amount, fee)
if change: if change:
outputs.append(change) outputs.append(change)
for i in range(0, 100): for _ in range(0, 100):
random.shuffle(outputs) random.shuffle(outputs)
#op_return_text = "Hello Bal!" #op_return_text = "Hello Bal!"
@@ -281,7 +281,7 @@ def invalidate_inheritance_transactions(wallet):
del dtxs[txid] del dtxs[txid]
utxos = {} utxos = {}
for txid, tx in dtxs.items(): for _, tx in dtxs.items():
get_utxos_from_inputs(tx.inputs(), tx, utxos) get_utxos_from_inputs(tx.inputs(), tx, utxos)
utxos = sorted(utxos.items(), key=lambda item: len(item[1])) utxos = sorted(utxos.items(), key=lambda item: len(item[1]))
@@ -727,7 +727,7 @@ class Heirs(dict, Logger):
) )
break break
except Exception: except Exception:
raise e raise
total_fees = 0 total_fees = 0
total_fees_real = 0 total_fees_real = 0
total_in = 0 total_in = 0
@@ -853,6 +853,7 @@ class Heirs(dict, Logger):
except AttributeError: except AttributeError:
return None return None
@staticmethod
def validate_address(address): def validate_address(address):
if is_op_return_address(address): if is_op_return_address(address):
data_hex = address[len(OP_RETURN_PREFIX):] data_hex = address[len(OP_RETURN_PREFIX):]
@@ -862,24 +863,27 @@ class Heirs(dict, Logger):
raise NotAnAddress(f"not an address,{address}") raise NotAnAddress(f"not an address,{address}")
return address return address
@staticmethod
def validate_amount(amount): def validate_amount(amount):
try: try:
famount = float(amount[:-1]) if Util.is_perc(amount) else float(amount) famount = float(amount[:-1]) if Util.is_perc(amount) else float(amount)
if famount <= 0.00000001: if famount <= 0.00000001:
raise AmountNotValid(f"amount have to be positive {famount} < 0") raise AmountNotValid(f"amount have to be positive {famount} < 0")
except Exception as e: except Exception as e:
raise AmountNotValid(f"amount not properly formatted, {e}") raise AmountNotValid(f"amount not properly formatted, {e}") from e
return amount return amount
@staticmethod
def validate_locktime(locktime, timestamp_to_check=False): def validate_locktime(locktime, timestamp_to_check=False):
try: try:
if timestamp_to_check: if timestamp_to_check:
if Util.parse_locktime_string(locktime, None) < timestamp_to_check: if Util.parse_locktime_string(locktime, None) < timestamp_to_check:
raise HeirExpiredException() raise HeirExpiredException()
except Exception as e: except Exception as e:
raise LocktimeNotValid(f"locktime string not properly formatted, {e}") raise LocktimeNotValid(f"locktime string not properly formatted, {e}") from e
return locktime return locktime
@staticmethod
def validate_heir(k, v, timestamp_to_check=False): def validate_heir(k, v, timestamp_to_check=False):
address = Heirs.validate_address(v[HEIR_ADDRESS]) address = Heirs.validate_address(v[HEIR_ADDRESS])
if is_op_return_address(v[HEIR_ADDRESS]): if is_op_return_address(v[HEIR_ADDRESS]):
@@ -889,6 +893,7 @@ class Heirs(dict, Logger):
locktime = Heirs.validate_locktime(v[HEIR_LOCKTIME], timestamp_to_check) locktime = Heirs.validate_locktime(v[HEIR_LOCKTIME], timestamp_to_check)
return (address, amount, locktime) return (address, amount, locktime)
@staticmethod
def _validate(data, timestamp_to_check=False): def _validate(data, timestamp_to_check=False):
for k, v in list(data.items()): for k, v in list(data.items()):

View File

@@ -477,14 +477,14 @@ class BalTimestamp:
We clamp out-of-range timestamps to INT32_MAX, mirroring Electrum's own We clamp out-of-range timestamps to INT32_MAX, mirroring Electrum's own
``get_max_allowed_timestamp`` workaround (see Electrum issue #6170). ``get_max_allowed_timestamp`` workaround (see Electrum issue #6170).
""" """
INT32_MAX = 2 ** 31 - 1 int32_max = 2 ** 31 - 1
try: try:
return datetime.fromtimestamp(ts) return datetime.fromtimestamp(ts)
except (OSError, OverflowError, ValueError): except (OSError, OverflowError, ValueError):
try: try:
return datetime.fromtimestamp(min(int(ts), INT32_MAX)) return datetime.fromtimestamp(min(int(ts), int32_max))
except (OSError, OverflowError, ValueError): except (OSError, OverflowError, ValueError):
return datetime.fromtimestamp(INT32_MAX) return datetime.fromtimestamp(int32_max)
def to_date(self, from_date=None, reverse=False): def to_date(self, from_date=None, reverse=False):
"""Resolve to a ``datetime``. """Resolve to a ``datetime``.

View File

@@ -400,14 +400,14 @@ class Util:
def get_lowest_valid_tx(available_utxos, will): def get_lowest_valid_tx(available_utxos, will):
"""Placeholder kept from the original code (sorts the will by locktime).""" """Placeholder kept from the original code (sorts the will by locktime)."""
will = sorted(will.items(), key=lambda x: x[1]["tx"].locktime) will = sorted(will.items(), key=lambda x: x[1]["tx"].locktime)
for txid, willitem in will.items(): for _txid, _willitem in will.items():
pass pass
@staticmethod @staticmethod
def get_locktimes(will): def get_locktimes(will):
"""Return the distinct locktimes used by the transactions in ``will``.""" """Return the distinct locktimes used by the transactions in ``will``."""
locktimes = {} locktimes = {}
for txid, willitem in will.items(): for _, willitem in will.items():
locktimes[willitem["tx"].locktime] = True locktimes[willitem["tx"].locktime] = True
return locktimes.keys() return locktimes.keys()
@@ -446,7 +446,7 @@ class Util:
def get_will_spent_utxos(will): def get_will_spent_utxos(will):
"""Collect every input spent by any transaction in ``will``.""" """Collect every input spent by any transaction in ``will``."""
utxos = [] utxos = []
for txid, willitem in will.items(): for _, willitem in will.items():
utxos += willitem["tx"].inputs() utxos += willitem["tx"].inputs()
return utxos return utxos

View File

@@ -43,9 +43,9 @@ from electrum.util import (
bfh, bfh,
) )
from .heirs import WillExecutorFeeTooHighException
from .util import Util from .util import Util
from .willexecutors import Willexecutors from .willexecutors import Willexecutors
from .heirs import WillExecutorFeeTooHighException
MIN_LOCKTIME = 1 MIN_LOCKTIME = 1
MIN_BLOCK = 1 MIN_BLOCK = 1
@@ -220,13 +220,8 @@ class Will:
if ow.we["url"] == nw.we["url"]: if ow.we["url"] == nw.we["url"]:
if int(ow.we["base_fee"]) > int(nw.we["base_fee"]): if int(ow.we["base_fee"]) > int(nw.we["base_fee"]):
return anticipate return anticipate
else: elif int(ow.tx_fees) != int(nw.tx_fees):
if int(ow.tx_fees) != int(nw.tx_fees): return anticipate
return anticipate
else:
ow.tx.locktime
else:
ow.tx.locktime
else: else:
if nw.we == ow.we: if nw.we == ow.we:
if not Util.cmp_heirs_by_values(ow.heirs, nw.heirs, [0, 3]): if not Util.cmp_heirs_by_values(ow.heirs, nw.heirs, [0, 3]):
@@ -512,7 +507,7 @@ class Will:
@staticmethod @staticmethod
def is_new(will): def is_new(will):
for wid, w in will.items(): for _wid, w in will.items():
if w.get_status("VALID") and not w.get_status("COMPLETE"): if w.get_status("VALID") and not w.get_status("COMPLETE"):
return True return True
@@ -784,7 +779,7 @@ class Will:
timestamp_to_check: Reference UNIX timestamp (usually "now"). timestamp_to_check: Reference UNIX timestamp (usually "now").
""" """
_logger.info("check if some transaction is expired") _logger.info("check if some transaction is expired")
for prevout_str, wid in all_inputs_min_locktime.items(): for _inputs, wid in all_inputs_min_locktime.items():
for w in wid: for w in wid:
if w[1].get_status("VALID"): if w[1].get_status("VALID"):
locktime = int(wid[0][1].tx.locktime) locktime = int(wid[0][1].tx.locktime)

View File

@@ -389,7 +389,7 @@ class Willexecutors:
except Exception as e: except Exception as e:
_logger.debug(f"error:{e}") _logger.debug(f"error:{e}")
if str(e) == "already present": if str(e) == "already present":
raise Willexecutors.AlreadyPresentException() raise Willexecutors.AlreadyPresentException() from None
out = False out = False
willexecutor["broadcast_status"] = _("Failed") willexecutor["broadcast_status"] = _("Failed")
@@ -485,8 +485,7 @@ class Willexecutors:
Returns: Returns:
The same ``willexecutors`` mapping, updated in place. The same ``willexecutors`` mapping, updated in place.
""" """
from concurrent.futures import ThreadPoolExecutor, wait from concurrent.futures import FIRST_COMPLETED, ThreadPoolExecutor, wait
from concurrent.futures import FIRST_COMPLETED
items = list(willexecutors.items()) items = list(willexecutors.items())
if not items: if not items:
@@ -566,8 +565,7 @@ class Willexecutors:
Returns ``{url: (ok, exception_or_None)}`` for the servers that Returns ``{url: (ok, exception_or_None)}`` for the servers that
answered in time (timed-out servers are reported via ``on_timeout``). answered in time (timed-out servers are reported via ``on_timeout``).
""" """
from concurrent.futures import ThreadPoolExecutor, wait from concurrent.futures import FIRST_COMPLETED, ThreadPoolExecutor, wait
from concurrent.futures import FIRST_COMPLETED
targets = [(url, we) for url, we in willexecutors.items() if "txs" in we] targets = [(url, we) for url, we in willexecutors.items() if "txs" in we]
results = {} results = {}
@@ -684,8 +682,7 @@ class Willexecutors:
Returns ``{wid: (result_or_None, exception_or_None)}`` for the servers Returns ``{wid: (result_or_None, exception_or_None)}`` for the servers
that answered in time. that answered in time.
""" """
from concurrent.futures import ThreadPoolExecutor, wait from concurrent.futures import FIRST_COMPLETED, ThreadPoolExecutor, wait
from concurrent.futures import FIRST_COMPLETED
targets = [(wid, url) for wid, url in items if url] targets = [(wid, url) for wid, url in items if url]
results = {} results = {}

View File

@@ -9,11 +9,12 @@ to "check in" before the locktime expires. This module turns the event data
into an RFC-5545 .ics file and opens it with the OS default application. into an RFC-5545 .ics file and opens it with the OS default application.
""" """
from .common import *
from .common import _, _logger # underscore names are not re-exported by "import *"
from PyQt6.QtGui import QAction from PyQt6.QtGui import QAction
from PyQt6.QtWidgets import QToolButton from PyQt6.QtWidgets import QToolButton
from .common import *
from .common import _, _logger # underscore names are not re-exported by "import *"
class BalCalendarButton(QToolButton): class BalCalendarButton(QToolButton):
"""A QToolButton with a dropdown menu for .ics calendar file actions. """A QToolButton with a dropdown menu for .ics calendar file actions.
@@ -79,7 +80,8 @@ class BalCalendarButton(QToolButton):
path = self._ensure_ics() path = self._ensure_ics()
if not path: if not path:
return return
import shlex, subprocess import shlex
import subprocess
if self._bal_window.bal_plugin.is_basic_mode(): if self._bal_window.bal_plugin.is_basic_mode():
app = self._bal_window.bal_plugin.CALENDAR_APP.default app = self._bal_window.bal_plugin.CALENDAR_APP.default
else: else:

View File

@@ -27,65 +27,129 @@ from decimal import Decimal
from functools import partial from functools import partial
from typing import Any, Callable, Mapping, Optional, Union from typing import Any, Callable, Mapping, Optional, Union
from electrum.bitcoin import (NLOCKTIME_BLOCKHEIGHT_MAX, NLOCKTIME_MAX, from electrum.bitcoin import NLOCKTIME_BLOCKHEIGHT_MAX, NLOCKTIME_MAX, NLOCKTIME_MIN
NLOCKTIME_MIN)
from electrum.gui.qt.amountedit import BTCAmountEdit from electrum.gui.qt.amountedit import BTCAmountEdit
from electrum.gui.qt.main_window import ElectrumWindow, StatusBarButton from electrum.gui.qt.main_window import ElectrumWindow, StatusBarButton
from electrum.gui.qt.my_treeview import MyTreeView from electrum.gui.qt.my_treeview import MyTreeView
from electrum.gui.qt.password_dialog import PasswordDialog from electrum.gui.qt.password_dialog import PasswordDialog
from electrum.gui.qt.transaction_dialog import TxDialog from electrum.gui.qt.transaction_dialog import TxDialog
from electrum.gui.qt.util import (Buttons, CancelButton, ColorScheme, from electrum.gui.qt.util import (
EnterButton, HelpButton, MessageBoxMixin, Buttons,
OkButton, TaskThread, WindowModalDialog, CancelButton,
char_width_in_lineedit, getSaveFileName, ColorScheme,
import_meta_gui, read_QIcon_from_bytes, EnterButton,
read_QPixmap_from_bytes, webopen) HelpButton,
MessageBoxMixin,
OkButton,
TaskThread,
WindowModalDialog,
char_width_in_lineedit,
getSaveFileName,
import_meta_gui,
read_QIcon_from_bytes,
read_QPixmap_from_bytes,
webopen,
)
from electrum.i18n import _ from electrum.i18n import _
from electrum.logging import get_logger from electrum.logging import get_logger
from electrum.network import BestEffortRequestFailed, Network, TxBroadcastError from electrum.network import BestEffortRequestFailed, Network, TxBroadcastError
from electrum.payment_identifier import PaymentIdentifier from electrum.payment_identifier import PaymentIdentifier
from electrum.plugin import hook from electrum.plugin import hook
from electrum.transaction import SerializationError, Transaction, tx_from_any from electrum.transaction import SerializationError, Transaction, tx_from_any
from electrum.util import (DECIMAL_POINT, FileExportFailed, UserCancelled, from electrum.util import (
decimal_point_to_base_unit_name, read_json_file, DECIMAL_POINT,
write_json_file) FileExportFailed,
from PyQt6.QtCore import (QDateTime, QModelIndex, QPersistentModelIndex, QSize, UserCancelled,
Qt, QTimer, pyqtSignal) decimal_point_to_base_unit_name,
from PyQt6.QtGui import (QColor, QPainter, QPalette, QStandardItem, read_json_file,
QStandardItemModel) write_json_file,
from PyQt6.QtWidgets import (QAbstractItemView, QAbstractSpinBox, QCheckBox, )
QComboBox, QDateTimeEdit, QGridLayout, QHBoxLayout, from PyQt6.QtCore import (
QInputDialog, QLabel, QLineEdit, QTextEdit, QMenu, QDateTime,
QMenuBar, QPushButton, QScrollArea, QSizePolicy, QModelIndex,
QSpinBox, QStackedWidget, QStyle, QStyleOptionFrame, QPersistentModelIndex,
QVBoxLayout, QWidget, QDialog) QSize,
Qt,
QTimer,
pyqtSignal,
)
from PyQt6.QtGui import QColor, QPainter, QPalette, QStandardItem, QStandardItemModel
from PyQt6.QtWidgets import (
QAbstractItemView,
QAbstractSpinBox,
QCheckBox,
QComboBox,
QDateTimeEdit,
QDialog,
QGridLayout,
QHBoxLayout,
QInputDialog,
QLabel,
QLineEdit,
QMenu,
QMenuBar,
QPushButton,
QScrollArea,
QSizePolicy,
QSpinBox,
QStackedWidget,
QStyle,
QStyleOptionFrame,
QTextEdit,
QVBoxLayout,
QWidget,
)
from ...core.heirs import (
HEIR_DUST_AMOUNT,
HEIR_REAL_AMOUNT,
OP_RETURN_PREFIX,
HeirAmountIsDustException,
Heirs,
WillExecutorFeeTooHighException,
get_op_return_hex,
is_op_return_address,
validate_op_return_hex,
)
# --- Core (GUI-free) logic layer --- # --- Core (GUI-free) logic layer ---
from ...core.plugin_base import BalPlugin, BalTimestamp from ...core.plugin_base import BalPlugin, BalTimestamp
from ...core.heirs import (HEIR_DUST_AMOUNT, HEIR_REAL_AMOUNT,
HeirAmountIsDustException, Heirs,
OP_RETURN_PREFIX, is_op_return_address,
get_op_return_hex, validate_op_return_hex,
WillExecutorFeeTooHighException)
from ...core.util import Util from ...core.util import Util
from ...core.will import (AmountException, HeirChangeException, from ...core.will import (
HeirNotFoundException, NoHeirsException, AmountException,
NotCompleteWillException, NoWillExecutorNotPresent, HeirChangeException,
TxFeesChangedException, Will, HeirNotFoundException,
WillexecutorChangeException, WillExecutorNotPresent, NoHeirsException,
WillExpiredException, WillItem, WillPostponedException) NotCompleteWillException,
from ...core.willexecutors import Willexecutors NoWillExecutorNotPresent,
from ...core.willexecutors import is_onion_url, is_tor_active # noqa: F401 TxFeesChangedException,
Will,
WillexecutorChangeException,
WillExecutorNotPresent,
WillExpiredException,
WillItem,
WillPostponedException,
)
from ...core.willexecutors import ( # noqa: F401
Willexecutors,
is_onion_url,
is_tor_active,
)
# --- Presentation helpers --- # --- Presentation helpers ---
from .theme import server_status_text, server_status_tooltip, status_color from .theme import server_status_text, server_status_tooltip, status_color
from .window_utils import (bring_to_front, show_modal, show_on_top, from .window_utils import (
stop_thread, top_level_of) bring_to_front,
show_modal,
show_on_top,
stop_thread,
top_level_of,
)
_logger = get_logger(__name__) _logger = get_logger(__name__)
class shown_cv: class shown_cv: # noqa: N801 (intentionally lowercase: mirrors Qt signal naming)
_type = bool _type = bool
def __init__(self, value): def __init__(self, value):

View File

@@ -17,13 +17,16 @@ the few list classes they reference are imported lazily inside the methods that
use them (see ``lists`` imports below). use them (see ``lists`` imports below).
""" """
from .calendar import BalCalendar, BalCalendarButton
from .common import * from .common import *
from .common import _, _logger # underscore names are not re-exported by "import *" from .common import _, _logger # underscore names are not re-exported by "import *"
from .widgets import (BalCheckBox, BalLineEdit, BalTextEdit, BalTxFeesWidget, from .widgets import (
LockTimeWidget, PercAmountEdit, ThresholdTimeWidget, WillSettingsWidget,
WillSettingsWidget, WillWidget, basic_reminder_offsets, WillWidget,
compute_reminder_offsets) basic_reminder_offsets,
from .calendar import BalCalendar, BalCalendarButton compute_reminder_offsets,
)
# NOTE: list views (HeirListWidget, PreviewList, WillExecutorWidget) are # NOTE: list views (HeirListWidget, PreviewList, WillExecutorWidget) are
# imported lazily where needed to avoid a dialogs<->lists import cycle. # imported lazily where needed to avoid a dialogs<->lists import cycle.
@@ -31,9 +34,7 @@ from .calendar import BalCalendar, BalCalendarButton
class BalDialog(QDialog,MessageBoxMixin): class BalDialog(QDialog,MessageBoxMixin):
_stopping = False _stopping = False
def __init__(self, parent, bal_plugin, title=None, icon="icons/bal16x16.png"): def __init__(self, parent, bal_plugin, title=None, icon="icons/bal16x16.png"):
import signal
from PyQt6.QtCore import QMetaObject, Qt from PyQt6.QtCore import QMetaObject, Qt
from PyQt6.QtWidgets import QApplication
def handler(signum, frame): def handler(signum, frame):
QMetaObject.invokeMethod(self, "close", Qt.ConnectionType.QueuedConnection) QMetaObject.invokeMethod(self, "close", Qt.ConnectionType.QueuedConnection)
@@ -49,7 +50,7 @@ class BalDialog(QDialog,MessageBoxMixin):
self.setWindowTitle(title) self.setWindowTitle(title)
# WindowModalDialog.__init__(self,parent) # WindowModalDialog.__init__(self,parent)
self.setWindowIcon(read_QIcon_from_bytes(bal_plugin.read_file(icon))) self.setWindowIcon(read_QIcon_from_bytes(bal_plugin.read_file(icon)))
def closeEvent(self, event): def closeEvent(self, event):
self._stopping = True self._stopping = True
# NOTE: we deliberately do NOT stop ``self.thread`` here. # NOTE: we deliberately do NOT stop ``self.thread`` here.
@@ -701,7 +702,7 @@ class BalBuildWillDialog(BalDialog):
return None, Will.invalidate_will( return None, Will.invalidate_will(
self.bal_window.willitems, self.bal_window.wallet, fee_per_byte self.bal_window.willitems, self.bal_window.wallet, fee_per_byte
) )
except NoHeirsException as e: except NoHeirsException:
_logger.debug("no heirs") _logger.debug("no heirs")
self.msg_set_checking("No Heirs") self.msg_set_checking("No Heirs")
except NotCompleteWillException as e: except NotCompleteWillException as e:

View File

@@ -14,12 +14,13 @@ construction) for all business actions, so the heavy logic stays in ``window``
and ``dialogs``. and ``dialogs``.
""" """
from PyQt6.QtWidgets import QLineEdit as _QLineEdit
from PyQt6.QtWidgets import QMessageBox, QStyledItemDelegate
from .common import * from .common import *
from .common import _, _logger # underscore names are not re-exported by "import *" from .common import _, _logger # underscore names are not re-exported by "import *"
from .widgets import BalCheckBox, PercAmountEdit, WillSettingsWidget
from PyQt6.QtWidgets import QMessageBox
from PyQt6.QtWidgets import QStyledItemDelegate, QLineEdit as _QLineEdit
from .dialogs import BalBuildWillDialog, BalDialog from .dialogs import BalBuildWillDialog, BalDialog
from .widgets import BalCheckBox, WillSettingsWidget
class HeirListWidget(MyTreeView, MessageBoxMixin): class HeirListWidget(MyTreeView, MessageBoxMixin):
@@ -194,7 +195,7 @@ class HeirListWidget(MyTreeView, MessageBoxMixin):
set_current = QPersistentModelIndex(idx) set_current = QPersistentModelIndex(idx)
try: try:
self.will_settings_widget.on_locktime_change() self.will_settings_widget.on_locktime_change()
except Exception as e: except Exception:
pass pass
self.set_current_idx(set_current) self.set_current_idx(set_current)
# FIXME refresh loses sort order; so set "default" here: # FIXME refresh loses sort order; so set "default" here:
@@ -214,15 +215,15 @@ class HeirListWidget(MyTreeView, MessageBoxMixin):
menu.addAction(_("Import"), self.bal_window.import_heirs) menu.addAction(_("Import"), self.bal_window.import_heirs)
menu.addAction(_("Export"), lambda: self.bal_window.export_heirs()) menu.addAction(_("Export"), lambda: self.bal_window.export_heirs())
newHeirButton = QPushButton(_("New Heir")) new_heir_button = QPushButton(_("New Heir"))
newHeirButton.clicked.connect(self.bal_window.new_heir_dialog) new_heir_button.clicked.connect(self.bal_window.new_heir_dialog)
widget = QWidget(self) widget = QWidget(self)
layout = QHBoxLayout(widget) layout = QHBoxLayout(widget)
self.will_settings_widget = WillSettingsWidget(self.bal_window, self) self.will_settings_widget = WillSettingsWidget(self.bal_window, self)
layout.addWidget(self.will_settings_widget) layout.addWidget(self.will_settings_widget)
layout.addWidget(newHeirButton) layout.addWidget(new_heir_button)
toolbar.insertWidget(2, widget) toolbar.insertWidget(2, widget)
@@ -281,7 +282,7 @@ class PreviewList(MyTreeView, MessageBoxMixin):
self.setModel(QStandardItemModel(self)) self.setModel(QStandardItemModel(self))
self.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection) self.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection)
self.sortByColumn(self.Columns.NAME, Qt.SortOrder.AscendingOrder) self.sortByColumn(self.Columns.NAME, Qt.SortOrder.AscendingOrder)
except Exception as e: except Exception:
pass pass
self.setSortingEnabled(True) self.setSortingEnabled(True)

View File

@@ -15,14 +15,17 @@ and cached in ``self.bal_windows``.
""" """
from electrum.gui.qt.main_window import StatusBarButton from electrum.gui.qt.main_window import StatusBarButton
from PyQt6.QtWidgets import QLayout
from .common import * from .common import *
from .common import _, _logger # underscore names are not re-exported by "import *" from .common import ( # underscore names are not re-exported by "import *"
from .common import read_QIcon_from_bytes _,
_logger,
read_QIcon_from_bytes,
)
from .dialogs import BalDialog
from .widgets import BalCheckBox, BalLineEdit, BalSpinBox, BalTextEdit from .widgets import BalCheckBox, BalLineEdit, BalSpinBox, BalTextEdit
from .window import BalWindow from .window import BalWindow
from .dialogs import BalDialog
from PyQt6.QtWidgets import QLayout
def _window_key(window): def _window_key(window):
@@ -88,9 +91,11 @@ class Plugin(BalPlugin):
except Exception: except Exception:
return [] return []
try: try:
from electrum.gui.qt.plugins_dialog import PluginsDialog from electrum.gui.qt.plugins_dialog import (
PluginsDialog as plugins_dialog, # noqa: N813
)
except Exception: except Exception:
PluginsDialog = None plugins_dialog = None
app = QApplication.instance() app = QApplication.instance()
if app is None: if app is None:
return [] return []
@@ -106,7 +111,7 @@ class Plugin(BalPlugin):
for w in app.topLevelWidgets(): for w in app.topLevelWidgets():
try: try:
is_match = False is_match = False
if PluginsDialog is not None and isinstance(w, PluginsDialog): if plugins_dialog is not None and isinstance(w, plugins_dialog):
is_match = True is_match = True
elif type(w).__name__ == "PluginsDialog": elif type(w).__name__ == "PluginsDialog":
is_match = True is_match = True
@@ -142,17 +147,17 @@ class Plugin(BalPlugin):
each is guarded independently. each is guarded independently.
""" """
try: try:
from PyQt6.QtWidgets import QDialog from PyQt6.QtWidgets import QDialog as qdialog # noqa: N813
except Exception: except Exception:
QDialog = None qdialog = None
# 1) reject() / done(): the reliable way to end an exec() modal loop. # 1) reject() / done(): the reliable way to end an exec() modal loop.
if QDialog is not None and isinstance(d, QDialog): if qdialog is not None and isinstance(d, qdialog):
try: try:
d.reject() d.reject()
except Exception as e: except Exception as e:
_logger.debug("reject() failed: {}".format(e)) _logger.debug("reject() failed: {}".format(e))
try: try:
d.done(QDialog.DialogCode.Rejected) d.done(qdialog.DialogCode.Rejected)
except Exception as e: except Exception as e:
_logger.debug("done() failed: {}".format(e)) _logger.debug("done() failed: {}".format(e))
# 2) close(): covers non-QDialog top-levels and is a harmless extra. # 2) close(): covers non-QDialog top-levels and is a harmless extra.
@@ -172,9 +177,9 @@ class Plugin(BalPlugin):
it and closes it themselves (it must not linger in the background). it and closes it themselves (it must not linger in the background).
""" """
try: try:
from PyQt6.QtCore import QTimer from PyQt6.QtCore import QTimer as qtimer # noqa: N813
except Exception: except Exception:
QTimer = None qtimer = None
# Schedule of retry delays (ms) measured from each call. # Schedule of retry delays (ms) measured from each call.
retry_delays = [400, 800, 1500] retry_delays = [400, 800, 1500]
dialogs = Plugin._find_plugins_manager_dialogs() dialogs = Plugin._find_plugins_manager_dialogs()
@@ -190,8 +195,8 @@ class Plugin(BalPlugin):
if not still_open: if not still_open:
_logger.info("plugins dialog closed successfully") _logger.info("plugins dialog closed successfully")
return return
if attempt < len(retry_delays) and QTimer is not None: if attempt < len(retry_delays) and qtimer is not None:
QTimer.singleShot( qtimer.singleShot(
retry_delays[attempt], retry_delays[attempt],
lambda: Plugin._handle_plugins_manager_dialog(attempt + 1), lambda: Plugin._handle_plugins_manager_dialog(attempt + 1),
) )

View File

@@ -18,9 +18,9 @@ Contents:
* WillWidget - single will-tx box * WillWidget - single will-tx box
""" """
from .calendar import BalCalendar, BalCalendarButton
from .common import * from .common import *
from .common import _, _logger # underscore names are not re-exported by "import *" from .common import _, _logger # underscore names are not re-exported by "import *"
from .calendar import BalCalendar, BalCalendarButton
def compute_reminder_offsets(days, count): def compute_reminder_offsets(days, count):
@@ -690,7 +690,7 @@ class LockTimeDateEdit(QDateTimeEdit, _LockTimeEditor):
return return
try: try:
x = int(x) x = int(x)
except Exception as e: except Exception:
x = QDateTime.currentDateTime().timestamp() x = QDateTime.currentDateTime().timestamp()
finally: finally:
# Use the overflow-safe converter: on Windows datetime.fromtimestamp # Use the overflow-safe converter: on Windows datetime.fromtimestamp
@@ -1395,15 +1395,15 @@ class PercAmountEdit(BTCAmountEdit):
if self.base_unit: if self.base_unit:
panel = QStyleOptionFrame() panel = QStyleOptionFrame()
self.initStyleOption(panel) self.initStyleOption(panel)
textRect = self.style().subElementRect( text_rect = self.style().subElementRect(
QStyle.SubElement.SE_LineEditContents, panel, self QStyle.SubElement.SE_LineEditContents, panel, self
) )
textRect.adjust(2, 0, -10, 0) text_rect.adjust(2, 0, -10, 0)
painter = QPainter(self) painter = QPainter(self)
painter.setPen(ColorScheme.GRAY.as_color()) painter.setPen(ColorScheme.GRAY.as_color())
if len(self.text()) == 0: if len(self.text()) == 0:
painter.drawText( painter.drawText(
textRect, text_rect,
int(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter), int(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter),
self.base_unit() + " or perc value", self.base_unit() + " or perc value",
) )

View File

@@ -18,11 +18,16 @@ import threading
from .common import * from .common import *
from .common import _, _logger # underscore names are not re-exported by "import *" from .common import _, _logger # underscore names are not re-exported by "import *"
from .widgets import LockTimeWidget, PercAmountEdit, WillSettingsWidget from .dialogs import (
from .dialogs import (BalBlockingWaitingDialog, BalBuildWillDialog, BalDialog, BalBuildWillDialog,
BalWaitingDialog, BalWizardDialog, WillDetailDialog, BalDialog,
WillExecutorDialog) BalWaitingDialog,
from .lists import HeirListWidget, PreviewList, WillExecutorWidget BalWizardDialog,
WillDetailDialog,
WillExecutorDialog,
)
from .lists import HeirListWidget, PreviewList
from .widgets import LockTimeWidget, PercAmountEdit
class BalWindow: class BalWindow:

View File

@@ -5,3 +5,15 @@ target-version = "py312"
[tool.ruff.lint] [tool.ruff.lint]
select =["E", "W", "F", "I", "N", "B"] select =["E", "W", "F", "I", "N", "B"]
ignore = ["E501"] ignore = ["E501"]
[tool.ruff.lint.per-file-ignores]
"bal/gui/qt/*.py" = ["F403", "F405"] # intentional `from .common import *` hub
"bal/gui/qt/common.py" = ["F401"] # re-exports consumed via `import *`
"bal/gui/qt/dialogs.py" = ["N802"] # Qt overrides: closeEvent/hideEvent/getText
"bal/gui/qt/lists.py" = ["N802"] # Qt overrides: createEditor/setEditorData/setModelData
"bal/gui/qt/widgets.py" = ["N802", "N815"] # Qt overrides + Qt signal attrs (valueChanged, ...)
"bal/gui/qt/window.py" = ["N802"] # getMsg
"bal/core/heirs.py" = ["N818", "N802"] # public exception names + buildTransactions API
"bal/core/will.py" = ["N818"] # public exception names
"bal/core/willexecutors.py" = ["N818"] # public exception names
"tests/*.py" = ["N802", "E402"] # deliberate UPPER_CASE helpers + sys.path-before-import

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -27,12 +27,19 @@ import os
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen") 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.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(): def _char_w():

View File

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

View File

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

View File

@@ -27,19 +27,19 @@ Run:
tests/test_anticipate_manual_locktime.py -q tests/test_anticipate_manual_locktime.py -q
""" """
import sys
import os
import copy import copy
import os
import sys
sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir)) sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir))
import pytest # noqa: E402 import pytest # noqa: E402
from bal.core.will import ( # noqa: E402 from bal.core.will import ( # noqa: E402
WillItem,
Will,
NotCompleteWillException, NotCompleteWillException,
Will,
WillExpiredException, WillExpiredException,
WillItem,
) )
# A valid serialized tx (1 input + 1 output, version 2). # 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 python3 -m pytest tests/test_anticipate_past_locktime.py -q
""" """
import sys
import os import os
import sys
import time import time
sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir)) 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 python3 tests/test_core_heirs.py
""" """
import sys
import os import os
import sys
sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir)) sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir))
from bal.core.heirs import ( from bal.core.heirs import (
HEIR_ADDRESS, HEIR_AMOUNT, HEIR_LOCKTIME, HEIR_REAL_AMOUNT, HEIR_ADDRESS,
HEIR_DUST_AMOUNT, TRANSACTION_LABEL, HEIR_AMOUNT,
HEIR_DUST_AMOUNT,
HEIR_LOCKTIME,
HEIR_REAL_AMOUNT,
OP_RETURN_PREFIX, OP_RETURN_PREFIX,
create_op_return_script, TRANSACTION_LABEL,
is_op_return_address, get_op_return_hex, validate_op_return_hex,
AliasNotFoundException, AliasNotFoundException,
NotAnAddress, AmountNotValid, LocktimeNotValid, AmountNotValid,
HeirExpiredException, HeirAmountIsDustException,
NoHeirsException, WillExecutorFeeException,
BalanceTooLowException, BalanceTooLowException,
HeirAmountIsDustException,
Heirs, Heirs,
LocktimeNotValid,
NoHeirsException,
NotAnAddress,
WillExecutorFeeException,
create_op_return_script,
get_op_return_hex,
is_op_return_address,
validate_op_return_hex,
) )
# ------------------------------------------------------------------ # # ------------------------------------------------------------------ #
# Constants # Constants
# ------------------------------------------------------------------ # # ------------------------------------------------------------------ #
@@ -70,7 +79,7 @@ def test_op_return_empty():
def test_op_return_too_big(): def test_op_return_too_big():
try: try:
create_op_return_script("ab" * 81) # 81 bytes > max 80 create_op_return_script("ab" * 81) # 81 bytes > max 80
assert False, "expected ValueError" raise AssertionError("expected ValueError")
except ValueError: except ValueError:
pass pass
@@ -179,13 +188,13 @@ def test_validate_amount():
# Invalid # Invalid
try: try:
Heirs.validate_amount("0.000000001") Heirs.validate_amount("0.000000001")
assert False, "expected AmountNotValid" raise AssertionError("expected AmountNotValid")
except AmountNotValid: except AmountNotValid:
pass pass
try: try:
Heirs.validate_amount("-1") Heirs.validate_amount("-1")
assert False, "expected AmountNotValid" raise AssertionError("expected AmountNotValid")
except AmountNotValid: except AmountNotValid:
pass pass
@@ -209,7 +218,7 @@ def test_validate_locktime_expired():
past = int(time.time()) - 86400 # yesterday past = int(time.time()) - 86400 # yesterday
try: try:
Heirs.validate_locktime(str(past), timestamp_to_check=past + 1) Heirs.validate_locktime(str(past), timestamp_to_check=past + 1)
assert False, "expected LocktimeNotValid" raise AssertionError("expected LocktimeNotValid")
except LocktimeNotValid: except LocktimeNotValid:
pass pass
@@ -289,7 +298,7 @@ def test_validate_op_return_hex_valid():
def test_validate_op_return_hex_invalid(): def test_validate_op_return_hex_invalid():
try: try:
validate_op_return_hex("nothex!!") validate_op_return_hex("nothex!!")
assert False, "expected NotAnAddress" raise AssertionError("expected NotAnAddress")
except NotAnAddress: except NotAnAddress:
pass pass
@@ -297,7 +306,7 @@ def test_validate_op_return_hex_invalid():
def test_validate_op_return_hex_too_long(): def test_validate_op_return_hex_too_long():
try: try:
validate_op_return_hex("ab" * 81) validate_op_return_hex("ab" * 81)
assert False, "expected NotAnAddress" raise AssertionError("expected NotAnAddress")
except NotAnAddress: except NotAnAddress:
pass pass
@@ -355,4 +364,4 @@ if __name__ == "__main__":
if name.startswith("test_"): if name.startswith("test_"):
globals()[name]() globals()[name]()
print(f" [OK] {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 QT_QPA_PLATFORM=offscreen python3 tests/test_core_heirs_extra.py
""" """
import sys
import os import os
import sys
from unittest.mock import MagicMock, patch from unittest.mock import MagicMock, patch
sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir)) sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir))
from bal.core.heirs import ( from bal.core.heirs import (
Heirs, create_op_return_script, reduce_outputs, HEIR_AMOUNT,
HEIR_ADDRESS, HEIR_AMOUNT, HEIR_LOCKTIME, HEIR_REAL_AMOUNT, Heirs,
create_op_return_script,
reduce_outputs,
) )
from bal.core.willexecutors import Willexecutors from bal.core.willexecutors import Willexecutors
# ------------------------------------------------------------------ # # ------------------------------------------------------------------ #
# Heirs db-dependent methods # Heirs db-dependent methods
# ------------------------------------------------------------------ # # ------------------------------------------------------------------ #
@@ -188,7 +189,7 @@ def test_validate_address_invalid():
from bal.core.heirs import NotAnAddress from bal.core.heirs import NotAnAddress
try: try:
Heirs.validate_address("bad") Heirs.validate_address("bad")
assert False, "should have raised" raise AssertionError("should have raised")
except NotAnAddress: except NotAnAddress:
pass pass

View File

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

View File

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

View File

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

View File

@@ -22,15 +22,14 @@ import os
import sys import sys
from unittest.mock import MagicMock, patch from unittest.mock import MagicMock, patch
import pytest
sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir)) 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 # Patch Transaction.add_info_from_wallet so WillItem can parse the tx hex
# without a live Electrum wallet connection. # without a live Electrum wallet connection.
from electrum.transaction import Transaction from electrum.transaction import Transaction
from bal.core.will import Will, WillItem
_patcher = patch.object(Transaction, "add_info_from_wallet") _patcher = patch.object(Transaction, "add_info_from_wallet")
_patcher.start() _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.plugin_base import BalConfig
from bal.core.willexecutors import Willexecutors from bal.core.willexecutors import Willexecutors
# ------------------------------------------------------------------ # # ------------------------------------------------------------------ #
# Mocks # 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.core.plugin_base import BalConfig
# ------------------------------------------------------------------ # # ------------------------------------------------------------------ #
# Mocks # 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.core.plugin_base import BalConfig
from bal.gui.qt.widgets import compute_reminder_offsets from bal.gui.qt.widgets import compute_reminder_offsets
# ------------------------------------------------------------------ # # ------------------------------------------------------------------ #
# Mocks # Mocks
# ------------------------------------------------------------------ # # ------------------------------------------------------------------ #

View File

@@ -25,7 +25,6 @@ import copy
import json import json
import os import os
import sys import sys
import warnings
import pytest import pytest
@@ -42,15 +41,12 @@ from electrum import bitcoin
from electrum.transaction import ( from electrum.transaction import (
PartialTransaction, PartialTransaction,
PartialTxInput, PartialTxInput,
PartialTxOutput,
TxOutpoint, TxOutpoint,
) )
from electrum.util import bfh from electrum.util import bfh
from bal.core.heirs import Heirs from bal.core.heirs import Heirs
from bal.core.will import Will, WillItem from bal.core.will import Will, WillItem
from bal.core.willexecutors import Willexecutors
# ------------------------------------------------------------------ # # ------------------------------------------------------------------ #
# Load karen7 wallet data # Load karen7 wallet data
@@ -166,7 +162,7 @@ def _build_utxo_value_map(data):
for txid, outputs in txo.items(): for txid, outputs in txo.items():
if not isinstance(outputs, dict): if not isinstance(outputs, dict):
continue continue
for addr, out_map in outputs.items(): for _, out_map in outputs.items():
if not isinstance(out_map, dict): if not isinstance(out_map, dict):
continue continue
for idx, info in out_map.items(): 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 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. 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(): for txin in wi.tx.inputs():
prevout_str = txin.prevout.to_str() prevout_str = txin.prevout.to_str()
if txin._trusted_value_sats is None and prevout_str in utxo_value_map: 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). # below the repo root that contains the ``bal`` package).
sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir)) 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.heirs import Heirs
from bal.core.will import HeirNotFoundException, Will, WillItem
from bal.core.willexecutors import Willexecutors from bal.core.willexecutors import Willexecutors
from bal.gui.qt.calendar import BalCalendar from bal.gui.qt.calendar import BalCalendar
from bal.gui.qt.widgets import compute_reminder_offsets from bal.gui.qt.widgets import compute_reminder_offsets
# A valid serialized Bitcoin transaction hex (1 input + 1 P2PKH output, # 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. # version 2). Reused from test_core_will.py so WillItem can parse a real tx.
_VALID_TX_HEX = ( _VALID_TX_HEX = (
@@ -550,7 +549,7 @@ def test_e5_build_with_real_wallet_heirs_and_utxos():
for txid, outputs in txo.items(): for txid, outputs in txo.items():
if not isinstance(outputs, dict): if not isinstance(outputs, dict):
continue continue
for addr, out_map in outputs.items(): for _, out_map in outputs.items():
if not isinstance(out_map, dict): if not isinstance(out_map, dict):
continue continue
for idx, info in out_map.items(): for idx, info in out_map.items():

View File

@@ -16,8 +16,9 @@ Run:
python3 tests/test_group_f_heir_change_rebuild.py python3 tests/test_group_f_heir_change_rebuild.py
""" """
import sys
import os import os
import sys
sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir)) sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir))
from bal.core.will import Will 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)) sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir))
from bal.gui.qt.widgets import (BASIC_REMINDER_OFFSETS, from bal.gui.qt.widgets import BASIC_REMINDER_OFFSETS, basic_reminder_offsets
basic_reminder_offsets)
def test_basic_offsets_all_future(): 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) from bal.gui.qt.window import BalWindow # noqa: E402 (path insert above)
OFFSET = BalWindow.BASIC_MODE_CHECK_ALIVE_OFFSET_SECONDS OFFSET = BalWindow.BASIC_MODE_CHECK_ALIVE_OFFSET_SECONDS

View File

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

View File

@@ -8,12 +8,13 @@ Run:
""" """
import sys import sys
sys.path.insert(0, __file__.rsplit("/", 2)[0]) 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 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) _app = QApplication.instance() or QApplication(sys.argv)
@@ -23,18 +24,18 @@ _app = QApplication.instance() or QApplication(sys.argv)
# ------------------------------------------------------------------ # # ------------------------------------------------------------------ #
def test_shown_cv_default(): def test_shown_cv_default():
cv = C.shown_cv(True) cv = common.shown_cv(True)
assert cv.get() is True assert cv.get() is True
def test_shown_cv_set(): def test_shown_cv_set():
cv = C.shown_cv(True) cv = common.shown_cv(True)
cv.set(False) cv.set(False)
assert cv.get() is False assert cv.get() is False
def test_shown_cv_roundtrip(): def test_shown_cv_roundtrip():
cv = C.shown_cv(False) cv = common.shown_cv(False)
assert cv.get() is False assert cv.get() is False
cv.set(True) cv.set(True)
assert cv.get() is True assert cv.get() is True
@@ -47,19 +48,19 @@ def test_shown_cv_roundtrip():
# ------------------------------------------------------------------ # # ------------------------------------------------------------------ #
def test_check_alive_error_default(): def test_check_alive_error_default():
err = C.CheckAliveError(1000000) err = common.CheckAliveError(1000000)
assert err.timestamp_to_check == 1000000 assert err.timestamp_to_check == 1000000
def test_check_alive_error_str(): def test_check_alive_error_str():
err = C.CheckAliveError(1000000) err = common.CheckAliveError(1000000)
s = str(err) s = str(err)
assert "Check alive expired" in s assert "Check alive expired" in s
assert "1970" in s assert "1970" in s
def test_check_alive_error_subclass(): 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(): def test_add_widget():
grid = QGridLayout() grid = QGridLayout()
parent = QWidget()
label = QLabel("test") 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 assert grid.count() == 3 # label + widget + help button
def test_add_widget_multiple_rows(): def test_add_widget_multiple_rows():
grid = QGridLayout() grid = QGridLayout()
parent = QWidget() common.add_widget(grid, "A", QLabel("a"), 0, "help_a")
C.add_widget(grid, "A", QLabel("a"), 0, "help_a") common.add_widget(grid, "B", QLabel("b"), 1, "help_b")
C.add_widget(grid, "B", QLabel("b"), 1, "help_b")
assert grid.count() == 6 assert grid.count() == 6
@@ -87,7 +86,7 @@ def test_add_widget_multiple_rows():
# ------------------------------------------------------------------ # # ------------------------------------------------------------------ #
def test_log_error_no_window(): 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 import sys
sys.path.insert(0, __file__.rsplit("/", 2)[0]) sys.path.insert(0, __file__.rsplit("/", 2)[0])
from bal.gui.qt.theme import status_color from bal.gui.qt.theme import status_color
# ------------------------------------------------------------------ # # ------------------------------------------------------------------ #
# Helpers # Helpers
# ------------------------------------------------------------------ # # ------------------------------------------------------------------ #

View File

@@ -13,13 +13,11 @@ Run:
""" """
import sys import sys
sys.path.insert(0, __file__.rsplit("/", 2)[0]) sys.path.insert(0, __file__.rsplit("/", 2)[0])
from PyQt6.QtCore import QTimer
from PyQt6.QtWidgets import QApplication, QWidget 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) _app = QApplication.instance() or QApplication(sys.argv)

View File

@@ -8,13 +8,18 @@ Run:
""" """
import sys import sys
sys.path.insert(0, __file__.rsplit("/", 2)[0]) sys.path.insert(0, __file__.rsplit("/", 2)[0])
from PyQt6.QtCore import QTimer from PyQt6.QtCore import QTimer
from PyQt6.QtWidgets import QApplication, QDialog, QWidget from PyQt6.QtWidgets import QApplication, QDialog, QWidget
from bal.gui.qt.window_utils import ( 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) _app = QApplication.instance() or QApplication(sys.argv)

View File

@@ -33,10 +33,9 @@ import logging
import os import os
import sys import sys
import time import time
from unittest.mock import MagicMock, patch from unittest.mock import patch
import pytest import pytest
from electrum import constants from electrum import constants
constants.net = constants.BitcoinRegtest constants.net = constants.BitcoinRegtest
@@ -48,10 +47,14 @@ from electrum.transaction import PartialTxInput, TxOutpoint
from electrum.util import bfh from electrum.util import bfh
from bal.core.heirs import Heirs 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 from bal.core.willexecutors import Willexecutors
# ------------------------------------------------------------------ # # ------------------------------------------------------------------ #
# Load karen7 wallet data # Load karen7 wallet data
# ------------------------------------------------------------------ # # ------------------------------------------------------------------ #
@@ -479,11 +482,11 @@ class TestNoWillexecutorKaren7:
dialog = FakeBuildWillDialog(self.bal_window) dialog = FakeBuildWillDialog(self.bal_window)
dialog.task_phase1() dialog.task_phase1()
assert any( assert any(
"Not present - select one or enable backup mode" in l "Not present - select one or enable backup mode" in label
for l in dialog.labels for label in dialog.labels
), "dialog labels must contain the 'not present' message" ), "dialog labels must contain the 'not present' message"
assert any( 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)" ), "dialog labels must use red (COLOR_ERROR)"
def test_task_phase1_adds_action_buttons(self): 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) sys.path.insert(0, ELECTRUM_DIR)
from bal.core.heirs import Heirs 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.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 # Load karen7 wallet data
@@ -53,7 +59,7 @@ def build_utxos(data):
for txid, outputs in txo.items(): for txid, outputs in txo.items():
if not isinstance(outputs, dict): if not isinstance(outputs, dict):
continue continue
for addr, out_map in outputs.items(): for _, out_map in outputs.items():
if not isinstance(out_map, dict): if not isinstance(out_map, dict):
continue continue
for idx, info in out_map.items(): for idx, info in out_map.items():
@@ -85,7 +91,6 @@ class FakeBalWindow:
def init_class_variables(self): def init_class_variables(self):
if not self.heirs: if not self.heirs:
raise NoHeirsException("Heirs are not defined") raise NoHeirsException("Heirs are not defined")
from bal.core.plugin_base import BalTimestamp
from datetime import datetime from datetime import datetime
self.date_to_check = BalTimestamp(self.will_settings['threshold']).to_timestamp() self.date_to_check = BalTimestamp(self.will_settings['threshold']).to_timestamp()
self.no_willexecutor = self.bal_plugin.NO_WILLEXECUTOR.get() self.no_willexecutor = self.bal_plugin.NO_WILLEXECUTOR.get()

View File

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