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:
76
AGENTS.md
Normal file
76
AGENTS.md
Normal 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).
|
||||
@@ -102,7 +102,7 @@ def validate_op_return_hex(data_hex: str) -> None:
|
||||
try:
|
||||
data = bytes.fromhex(data_hex)
|
||||
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:
|
||||
raise NotAnAddress(
|
||||
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)
|
||||
if change:
|
||||
outputs.append(change)
|
||||
for i in range(0, 100):
|
||||
for _ in range(0, 100):
|
||||
random.shuffle(outputs)
|
||||
|
||||
#op_return_text = "Hello Bal!"
|
||||
@@ -281,7 +281,7 @@ def invalidate_inheritance_transactions(wallet):
|
||||
del dtxs[txid]
|
||||
|
||||
utxos = {}
|
||||
for txid, tx in dtxs.items():
|
||||
for _, tx in dtxs.items():
|
||||
get_utxos_from_inputs(tx.inputs(), tx, utxos)
|
||||
|
||||
utxos = sorted(utxos.items(), key=lambda item: len(item[1]))
|
||||
@@ -727,7 +727,7 @@ class Heirs(dict, Logger):
|
||||
)
|
||||
break
|
||||
except Exception:
|
||||
raise e
|
||||
raise
|
||||
total_fees = 0
|
||||
total_fees_real = 0
|
||||
total_in = 0
|
||||
@@ -853,6 +853,7 @@ class Heirs(dict, Logger):
|
||||
except AttributeError:
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def validate_address(address):
|
||||
if is_op_return_address(address):
|
||||
data_hex = address[len(OP_RETURN_PREFIX):]
|
||||
@@ -862,24 +863,27 @@ class Heirs(dict, Logger):
|
||||
raise NotAnAddress(f"not an address,{address}")
|
||||
return address
|
||||
|
||||
@staticmethod
|
||||
def validate_amount(amount):
|
||||
try:
|
||||
famount = float(amount[:-1]) if Util.is_perc(amount) else float(amount)
|
||||
if famount <= 0.00000001:
|
||||
raise AmountNotValid(f"amount have to be positive {famount} < 0")
|
||||
except Exception as e:
|
||||
raise AmountNotValid(f"amount not properly formatted, {e}")
|
||||
raise AmountNotValid(f"amount not properly formatted, {e}") from e
|
||||
return amount
|
||||
|
||||
@staticmethod
|
||||
def validate_locktime(locktime, timestamp_to_check=False):
|
||||
try:
|
||||
if timestamp_to_check:
|
||||
if Util.parse_locktime_string(locktime, None) < timestamp_to_check:
|
||||
raise HeirExpiredException()
|
||||
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
|
||||
|
||||
@staticmethod
|
||||
def validate_heir(k, v, timestamp_to_check=False):
|
||||
address = Heirs.validate_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)
|
||||
return (address, amount, locktime)
|
||||
|
||||
@staticmethod
|
||||
def _validate(data, timestamp_to_check=False):
|
||||
|
||||
for k, v in list(data.items()):
|
||||
|
||||
@@ -477,14 +477,14 @@ class BalTimestamp:
|
||||
We clamp out-of-range timestamps to INT32_MAX, mirroring Electrum's own
|
||||
``get_max_allowed_timestamp`` workaround (see Electrum issue #6170).
|
||||
"""
|
||||
INT32_MAX = 2 ** 31 - 1
|
||||
int32_max = 2 ** 31 - 1
|
||||
try:
|
||||
return datetime.fromtimestamp(ts)
|
||||
except (OSError, OverflowError, ValueError):
|
||||
try:
|
||||
return datetime.fromtimestamp(min(int(ts), INT32_MAX))
|
||||
return datetime.fromtimestamp(min(int(ts), int32_max))
|
||||
except (OSError, OverflowError, ValueError):
|
||||
return datetime.fromtimestamp(INT32_MAX)
|
||||
return datetime.fromtimestamp(int32_max)
|
||||
|
||||
def to_date(self, from_date=None, reverse=False):
|
||||
"""Resolve to a ``datetime``.
|
||||
|
||||
@@ -400,14 +400,14 @@ class Util:
|
||||
def get_lowest_valid_tx(available_utxos, will):
|
||||
"""Placeholder kept from the original code (sorts the will by 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
|
||||
|
||||
@staticmethod
|
||||
def get_locktimes(will):
|
||||
"""Return the distinct locktimes used by the transactions in ``will``."""
|
||||
locktimes = {}
|
||||
for txid, willitem in will.items():
|
||||
for _, willitem in will.items():
|
||||
locktimes[willitem["tx"].locktime] = True
|
||||
return locktimes.keys()
|
||||
|
||||
@@ -446,7 +446,7 @@ class Util:
|
||||
def get_will_spent_utxos(will):
|
||||
"""Collect every input spent by any transaction in ``will``."""
|
||||
utxos = []
|
||||
for txid, willitem in will.items():
|
||||
for _, willitem in will.items():
|
||||
utxos += willitem["tx"].inputs()
|
||||
|
||||
return utxos
|
||||
|
||||
@@ -43,9 +43,9 @@ from electrum.util import (
|
||||
bfh,
|
||||
)
|
||||
|
||||
from .heirs import WillExecutorFeeTooHighException
|
||||
from .util import Util
|
||||
from .willexecutors import Willexecutors
|
||||
from .heirs import WillExecutorFeeTooHighException
|
||||
|
||||
MIN_LOCKTIME = 1
|
||||
MIN_BLOCK = 1
|
||||
@@ -220,13 +220,8 @@ class Will:
|
||||
if ow.we["url"] == nw.we["url"]:
|
||||
if int(ow.we["base_fee"]) > int(nw.we["base_fee"]):
|
||||
return anticipate
|
||||
else:
|
||||
if int(ow.tx_fees) != int(nw.tx_fees):
|
||||
return anticipate
|
||||
else:
|
||||
ow.tx.locktime
|
||||
else:
|
||||
ow.tx.locktime
|
||||
elif int(ow.tx_fees) != int(nw.tx_fees):
|
||||
return anticipate
|
||||
else:
|
||||
if nw.we == ow.we:
|
||||
if not Util.cmp_heirs_by_values(ow.heirs, nw.heirs, [0, 3]):
|
||||
@@ -512,7 +507,7 @@ class Will:
|
||||
|
||||
@staticmethod
|
||||
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"):
|
||||
return True
|
||||
|
||||
@@ -784,7 +779,7 @@ class Will:
|
||||
timestamp_to_check: Reference UNIX timestamp (usually "now").
|
||||
"""
|
||||
_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:
|
||||
if w[1].get_status("VALID"):
|
||||
locktime = int(wid[0][1].tx.locktime)
|
||||
|
||||
@@ -389,7 +389,7 @@ class Willexecutors:
|
||||
except Exception as e:
|
||||
_logger.debug(f"error:{e}")
|
||||
if str(e) == "already present":
|
||||
raise Willexecutors.AlreadyPresentException()
|
||||
raise Willexecutors.AlreadyPresentException() from None
|
||||
out = False
|
||||
willexecutor["broadcast_status"] = _("Failed")
|
||||
|
||||
@@ -485,8 +485,7 @@ class Willexecutors:
|
||||
Returns:
|
||||
The same ``willexecutors`` mapping, updated in place.
|
||||
"""
|
||||
from concurrent.futures import ThreadPoolExecutor, wait
|
||||
from concurrent.futures import FIRST_COMPLETED
|
||||
from concurrent.futures import FIRST_COMPLETED, ThreadPoolExecutor, wait
|
||||
|
||||
items = list(willexecutors.items())
|
||||
if not items:
|
||||
@@ -566,8 +565,7 @@ class Willexecutors:
|
||||
Returns ``{url: (ok, exception_or_None)}`` for the servers that
|
||||
answered in time (timed-out servers are reported via ``on_timeout``).
|
||||
"""
|
||||
from concurrent.futures import ThreadPoolExecutor, wait
|
||||
from concurrent.futures import FIRST_COMPLETED
|
||||
from concurrent.futures import FIRST_COMPLETED, ThreadPoolExecutor, wait
|
||||
|
||||
targets = [(url, we) for url, we in willexecutors.items() if "txs" in we]
|
||||
results = {}
|
||||
@@ -684,8 +682,7 @@ class Willexecutors:
|
||||
Returns ``{wid: (result_or_None, exception_or_None)}`` for the servers
|
||||
that answered in time.
|
||||
"""
|
||||
from concurrent.futures import ThreadPoolExecutor, wait
|
||||
from concurrent.futures import FIRST_COMPLETED
|
||||
from concurrent.futures import FIRST_COMPLETED, ThreadPoolExecutor, wait
|
||||
|
||||
targets = [(wid, url) for wid, url in items if url]
|
||||
results = {}
|
||||
|
||||
@@ -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.
|
||||
"""
|
||||
|
||||
from .common import *
|
||||
from .common import _, _logger # underscore names are not re-exported by "import *"
|
||||
from PyQt6.QtGui import QAction
|
||||
from PyQt6.QtWidgets import QToolButton
|
||||
|
||||
from .common import *
|
||||
from .common import _, _logger # underscore names are not re-exported by "import *"
|
||||
|
||||
|
||||
class BalCalendarButton(QToolButton):
|
||||
"""A QToolButton with a dropdown menu for .ics calendar file actions.
|
||||
@@ -79,7 +80,8 @@ class BalCalendarButton(QToolButton):
|
||||
path = self._ensure_ics()
|
||||
if not path:
|
||||
return
|
||||
import shlex, subprocess
|
||||
import shlex
|
||||
import subprocess
|
||||
if self._bal_window.bal_plugin.is_basic_mode():
|
||||
app = self._bal_window.bal_plugin.CALENDAR_APP.default
|
||||
else:
|
||||
|
||||
@@ -27,65 +27,129 @@ from decimal import Decimal
|
||||
from functools import partial
|
||||
from typing import Any, Callable, Mapping, Optional, Union
|
||||
|
||||
from electrum.bitcoin import (NLOCKTIME_BLOCKHEIGHT_MAX, NLOCKTIME_MAX,
|
||||
NLOCKTIME_MIN)
|
||||
from electrum.bitcoin import NLOCKTIME_BLOCKHEIGHT_MAX, NLOCKTIME_MAX, NLOCKTIME_MIN
|
||||
from electrum.gui.qt.amountedit import BTCAmountEdit
|
||||
from electrum.gui.qt.main_window import ElectrumWindow, StatusBarButton
|
||||
from electrum.gui.qt.my_treeview import MyTreeView
|
||||
from electrum.gui.qt.password_dialog import PasswordDialog
|
||||
from electrum.gui.qt.transaction_dialog import TxDialog
|
||||
from electrum.gui.qt.util import (Buttons, CancelButton, ColorScheme,
|
||||
EnterButton, HelpButton, MessageBoxMixin,
|
||||
OkButton, TaskThread, WindowModalDialog,
|
||||
char_width_in_lineedit, getSaveFileName,
|
||||
import_meta_gui, read_QIcon_from_bytes,
|
||||
read_QPixmap_from_bytes, webopen)
|
||||
from electrum.gui.qt.util import (
|
||||
Buttons,
|
||||
CancelButton,
|
||||
ColorScheme,
|
||||
EnterButton,
|
||||
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.logging import get_logger
|
||||
from electrum.network import BestEffortRequestFailed, Network, TxBroadcastError
|
||||
from electrum.payment_identifier import PaymentIdentifier
|
||||
from electrum.plugin import hook
|
||||
from electrum.transaction import SerializationError, Transaction, tx_from_any
|
||||
from electrum.util import (DECIMAL_POINT, FileExportFailed, UserCancelled,
|
||||
decimal_point_to_base_unit_name, read_json_file,
|
||||
write_json_file)
|
||||
from PyQt6.QtCore import (QDateTime, QModelIndex, QPersistentModelIndex, QSize,
|
||||
Qt, QTimer, pyqtSignal)
|
||||
from PyQt6.QtGui import (QColor, QPainter, QPalette, QStandardItem,
|
||||
QStandardItemModel)
|
||||
from PyQt6.QtWidgets import (QAbstractItemView, QAbstractSpinBox, QCheckBox,
|
||||
QComboBox, QDateTimeEdit, QGridLayout, QHBoxLayout,
|
||||
QInputDialog, QLabel, QLineEdit, QTextEdit, QMenu,
|
||||
QMenuBar, QPushButton, QScrollArea, QSizePolicy,
|
||||
QSpinBox, QStackedWidget, QStyle, QStyleOptionFrame,
|
||||
QVBoxLayout, QWidget, QDialog)
|
||||
from electrum.util import (
|
||||
DECIMAL_POINT,
|
||||
FileExportFailed,
|
||||
UserCancelled,
|
||||
decimal_point_to_base_unit_name,
|
||||
read_json_file,
|
||||
write_json_file,
|
||||
)
|
||||
from PyQt6.QtCore import (
|
||||
QDateTime,
|
||||
QModelIndex,
|
||||
QPersistentModelIndex,
|
||||
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 ---
|
||||
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.will import (AmountException, HeirChangeException,
|
||||
HeirNotFoundException, NoHeirsException,
|
||||
NotCompleteWillException, NoWillExecutorNotPresent,
|
||||
TxFeesChangedException, Will,
|
||||
WillexecutorChangeException, WillExecutorNotPresent,
|
||||
WillExpiredException, WillItem, WillPostponedException)
|
||||
from ...core.willexecutors import Willexecutors
|
||||
from ...core.willexecutors import is_onion_url, is_tor_active # noqa: F401
|
||||
from ...core.will import (
|
||||
AmountException,
|
||||
HeirChangeException,
|
||||
HeirNotFoundException,
|
||||
NoHeirsException,
|
||||
NotCompleteWillException,
|
||||
NoWillExecutorNotPresent,
|
||||
TxFeesChangedException,
|
||||
Will,
|
||||
WillexecutorChangeException,
|
||||
WillExecutorNotPresent,
|
||||
WillExpiredException,
|
||||
WillItem,
|
||||
WillPostponedException,
|
||||
)
|
||||
from ...core.willexecutors import ( # noqa: F401
|
||||
Willexecutors,
|
||||
is_onion_url,
|
||||
is_tor_active,
|
||||
)
|
||||
|
||||
# --- Presentation helpers ---
|
||||
from .theme import server_status_text, server_status_tooltip, status_color
|
||||
from .window_utils import (bring_to_front, show_modal, show_on_top,
|
||||
stop_thread, top_level_of)
|
||||
from .window_utils import (
|
||||
bring_to_front,
|
||||
show_modal,
|
||||
show_on_top,
|
||||
stop_thread,
|
||||
top_level_of,
|
||||
)
|
||||
|
||||
_logger = get_logger(__name__)
|
||||
|
||||
|
||||
class shown_cv:
|
||||
class shown_cv: # noqa: N801 (intentionally lowercase: mirrors Qt signal naming)
|
||||
_type = bool
|
||||
|
||||
def __init__(self, value):
|
||||
|
||||
@@ -17,13 +17,16 @@ the few list classes they reference are imported lazily inside the methods that
|
||||
use them (see ``lists`` imports below).
|
||||
"""
|
||||
|
||||
from .calendar import BalCalendar, BalCalendarButton
|
||||
from .common import *
|
||||
from .common import _, _logger # underscore names are not re-exported by "import *"
|
||||
from .widgets import (BalCheckBox, BalLineEdit, BalTextEdit, BalTxFeesWidget,
|
||||
LockTimeWidget, PercAmountEdit, ThresholdTimeWidget,
|
||||
WillSettingsWidget, WillWidget, basic_reminder_offsets,
|
||||
compute_reminder_offsets)
|
||||
from .calendar import BalCalendar, BalCalendarButton
|
||||
from .widgets import (
|
||||
WillSettingsWidget,
|
||||
WillWidget,
|
||||
basic_reminder_offsets,
|
||||
compute_reminder_offsets,
|
||||
)
|
||||
|
||||
# NOTE: list views (HeirListWidget, PreviewList, WillExecutorWidget) are
|
||||
# imported lazily where needed to avoid a dialogs<->lists import cycle.
|
||||
|
||||
@@ -31,9 +34,7 @@ from .calendar import BalCalendar, BalCalendarButton
|
||||
class BalDialog(QDialog,MessageBoxMixin):
|
||||
_stopping = False
|
||||
def __init__(self, parent, bal_plugin, title=None, icon="icons/bal16x16.png"):
|
||||
import signal
|
||||
from PyQt6.QtCore import QMetaObject, Qt
|
||||
from PyQt6.QtWidgets import QApplication
|
||||
def handler(signum, frame):
|
||||
QMetaObject.invokeMethod(self, "close", Qt.ConnectionType.QueuedConnection)
|
||||
|
||||
@@ -49,7 +50,7 @@ class BalDialog(QDialog,MessageBoxMixin):
|
||||
self.setWindowTitle(title)
|
||||
# WindowModalDialog.__init__(self,parent)
|
||||
self.setWindowIcon(read_QIcon_from_bytes(bal_plugin.read_file(icon)))
|
||||
|
||||
|
||||
def closeEvent(self, event):
|
||||
self._stopping = True
|
||||
# NOTE: we deliberately do NOT stop ``self.thread`` here.
|
||||
@@ -701,7 +702,7 @@ class BalBuildWillDialog(BalDialog):
|
||||
return None, Will.invalidate_will(
|
||||
self.bal_window.willitems, self.bal_window.wallet, fee_per_byte
|
||||
)
|
||||
except NoHeirsException as e:
|
||||
except NoHeirsException:
|
||||
_logger.debug("no heirs")
|
||||
self.msg_set_checking("No Heirs")
|
||||
except NotCompleteWillException as e:
|
||||
|
||||
@@ -14,12 +14,13 @@ construction) for all business actions, so the heavy logic stays in ``window``
|
||||
and ``dialogs``.
|
||||
"""
|
||||
|
||||
from PyQt6.QtWidgets import QLineEdit as _QLineEdit
|
||||
from PyQt6.QtWidgets import QMessageBox, QStyledItemDelegate
|
||||
|
||||
from .common 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 .widgets import BalCheckBox, WillSettingsWidget
|
||||
|
||||
|
||||
class HeirListWidget(MyTreeView, MessageBoxMixin):
|
||||
@@ -194,7 +195,7 @@ class HeirListWidget(MyTreeView, MessageBoxMixin):
|
||||
set_current = QPersistentModelIndex(idx)
|
||||
try:
|
||||
self.will_settings_widget.on_locktime_change()
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
pass
|
||||
self.set_current_idx(set_current)
|
||||
# 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(_("Export"), lambda: self.bal_window.export_heirs())
|
||||
|
||||
newHeirButton = QPushButton(_("New Heir"))
|
||||
newHeirButton.clicked.connect(self.bal_window.new_heir_dialog)
|
||||
new_heir_button = QPushButton(_("New Heir"))
|
||||
new_heir_button.clicked.connect(self.bal_window.new_heir_dialog)
|
||||
|
||||
widget = QWidget(self)
|
||||
layout = QHBoxLayout(widget)
|
||||
self.will_settings_widget = WillSettingsWidget(self.bal_window, self)
|
||||
|
||||
layout.addWidget(self.will_settings_widget)
|
||||
layout.addWidget(newHeirButton)
|
||||
layout.addWidget(new_heir_button)
|
||||
|
||||
toolbar.insertWidget(2, widget)
|
||||
|
||||
@@ -281,7 +282,7 @@ class PreviewList(MyTreeView, MessageBoxMixin):
|
||||
self.setModel(QStandardItemModel(self))
|
||||
self.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection)
|
||||
self.sortByColumn(self.Columns.NAME, Qt.SortOrder.AscendingOrder)
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
self.setSortingEnabled(True)
|
||||
|
||||
@@ -15,14 +15,17 @@ and cached in ``self.bal_windows``.
|
||||
"""
|
||||
|
||||
from electrum.gui.qt.main_window import StatusBarButton
|
||||
from PyQt6.QtWidgets import QLayout
|
||||
|
||||
from .common import *
|
||||
from .common import _, _logger # underscore names are not re-exported by "import *"
|
||||
from .common import read_QIcon_from_bytes
|
||||
from .common import ( # underscore names are not re-exported by "import *"
|
||||
_,
|
||||
_logger,
|
||||
read_QIcon_from_bytes,
|
||||
)
|
||||
from .dialogs import BalDialog
|
||||
from .widgets import BalCheckBox, BalLineEdit, BalSpinBox, BalTextEdit
|
||||
from .window import BalWindow
|
||||
from .dialogs import BalDialog
|
||||
from PyQt6.QtWidgets import QLayout
|
||||
|
||||
|
||||
def _window_key(window):
|
||||
@@ -88,9 +91,11 @@ class Plugin(BalPlugin):
|
||||
except Exception:
|
||||
return []
|
||||
try:
|
||||
from electrum.gui.qt.plugins_dialog import PluginsDialog
|
||||
from electrum.gui.qt.plugins_dialog import (
|
||||
PluginsDialog as plugins_dialog, # noqa: N813
|
||||
)
|
||||
except Exception:
|
||||
PluginsDialog = None
|
||||
plugins_dialog = None
|
||||
app = QApplication.instance()
|
||||
if app is None:
|
||||
return []
|
||||
@@ -106,7 +111,7 @@ class Plugin(BalPlugin):
|
||||
for w in app.topLevelWidgets():
|
||||
try:
|
||||
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
|
||||
elif type(w).__name__ == "PluginsDialog":
|
||||
is_match = True
|
||||
@@ -142,17 +147,17 @@ class Plugin(BalPlugin):
|
||||
each is guarded independently.
|
||||
"""
|
||||
try:
|
||||
from PyQt6.QtWidgets import QDialog
|
||||
from PyQt6.QtWidgets import QDialog as qdialog # noqa: N813
|
||||
except Exception:
|
||||
QDialog = None
|
||||
qdialog = None
|
||||
# 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:
|
||||
d.reject()
|
||||
except Exception as e:
|
||||
_logger.debug("reject() failed: {}".format(e))
|
||||
try:
|
||||
d.done(QDialog.DialogCode.Rejected)
|
||||
d.done(qdialog.DialogCode.Rejected)
|
||||
except Exception as e:
|
||||
_logger.debug("done() failed: {}".format(e))
|
||||
# 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).
|
||||
"""
|
||||
try:
|
||||
from PyQt6.QtCore import QTimer
|
||||
from PyQt6.QtCore import QTimer as qtimer # noqa: N813
|
||||
except Exception:
|
||||
QTimer = None
|
||||
qtimer = None
|
||||
# Schedule of retry delays (ms) measured from each call.
|
||||
retry_delays = [400, 800, 1500]
|
||||
dialogs = Plugin._find_plugins_manager_dialogs()
|
||||
@@ -190,8 +195,8 @@ class Plugin(BalPlugin):
|
||||
if not still_open:
|
||||
_logger.info("plugins dialog closed successfully")
|
||||
return
|
||||
if attempt < len(retry_delays) and QTimer is not None:
|
||||
QTimer.singleShot(
|
||||
if attempt < len(retry_delays) and qtimer is not None:
|
||||
qtimer.singleShot(
|
||||
retry_delays[attempt],
|
||||
lambda: Plugin._handle_plugins_manager_dialog(attempt + 1),
|
||||
)
|
||||
|
||||
@@ -18,9 +18,9 @@ Contents:
|
||||
* WillWidget - single will-tx box
|
||||
"""
|
||||
|
||||
from .calendar import BalCalendar, BalCalendarButton
|
||||
from .common import *
|
||||
from .common import _, _logger # underscore names are not re-exported by "import *"
|
||||
from .calendar import BalCalendar, BalCalendarButton
|
||||
|
||||
|
||||
def compute_reminder_offsets(days, count):
|
||||
@@ -690,7 +690,7 @@ class LockTimeDateEdit(QDateTimeEdit, _LockTimeEditor):
|
||||
return
|
||||
try:
|
||||
x = int(x)
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
x = QDateTime.currentDateTime().timestamp()
|
||||
finally:
|
||||
# Use the overflow-safe converter: on Windows datetime.fromtimestamp
|
||||
@@ -1395,15 +1395,15 @@ class PercAmountEdit(BTCAmountEdit):
|
||||
if self.base_unit:
|
||||
panel = QStyleOptionFrame()
|
||||
self.initStyleOption(panel)
|
||||
textRect = self.style().subElementRect(
|
||||
text_rect = self.style().subElementRect(
|
||||
QStyle.SubElement.SE_LineEditContents, panel, self
|
||||
)
|
||||
textRect.adjust(2, 0, -10, 0)
|
||||
text_rect.adjust(2, 0, -10, 0)
|
||||
painter = QPainter(self)
|
||||
painter.setPen(ColorScheme.GRAY.as_color())
|
||||
if len(self.text()) == 0:
|
||||
painter.drawText(
|
||||
textRect,
|
||||
text_rect,
|
||||
int(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter),
|
||||
self.base_unit() + " or perc value",
|
||||
)
|
||||
|
||||
@@ -18,11 +18,16 @@ import threading
|
||||
|
||||
from .common import *
|
||||
from .common import _, _logger # underscore names are not re-exported by "import *"
|
||||
from .widgets import LockTimeWidget, PercAmountEdit, WillSettingsWidget
|
||||
from .dialogs import (BalBlockingWaitingDialog, BalBuildWillDialog, BalDialog,
|
||||
BalWaitingDialog, BalWizardDialog, WillDetailDialog,
|
||||
WillExecutorDialog)
|
||||
from .lists import HeirListWidget, PreviewList, WillExecutorWidget
|
||||
from .dialogs import (
|
||||
BalBuildWillDialog,
|
||||
BalDialog,
|
||||
BalWaitingDialog,
|
||||
BalWizardDialog,
|
||||
WillDetailDialog,
|
||||
WillExecutorDialog,
|
||||
)
|
||||
from .lists import HeirListWidget, PreviewList
|
||||
from .widgets import LockTimeWidget, PercAmountEdit
|
||||
|
||||
|
||||
class BalWindow:
|
||||
|
||||
@@ -5,3 +5,15 @@ target-version = "py312"
|
||||
[tool.ruff.lint]
|
||||
select =["E", "W", "F", "I", "N", "B"]
|
||||
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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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 = (
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
@@ -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
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
@@ -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
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
@@ -8,11 +8,11 @@ Run:
|
||||
"""
|
||||
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, __file__.rsplit("/", 2)[0])
|
||||
|
||||
from bal.gui.qt.theme import status_color
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# Helpers
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user