tMerge pull request #3951 from SomberNight/file_import_export_unification - electrum - Electrum Bitcoin wallet HTML git clone https://git.parazyd.org/electrum DIR Log DIR Files DIR Refs DIR Submodules --- DIR commit 18ba4319dad3fdcc14dbf4456bc29069439d256e DIR parent aaf89d2325ce114fab152b17d6cacbe150938e89 HTML Author: ThomasV <thomasv@electrum.org> Date: Fri, 23 Feb 2018 11:46:04 +0100 Merge pull request #3951 from SomberNight/file_import_export_unification File import-export unification Diffstat: M gui/qt/contact_list.py | 18 +++++++----------- M gui/qt/invoice_list.py | 18 +++++++----------- M gui/qt/main_window.py | 45 +++++++++++++------------------ M gui/qt/util.py | 37 ++++++++++++++++++++++++++++--- M lib/contacts.py | 23 ++++++++++------------- M lib/paymentrequest.py | 32 ++++++++++++++++--------------- M lib/util.py | 37 ++++++++++++++++++++++++++----- 7 files changed, 126 insertions(+), 84 deletions(-) --- DIR diff --git a/gui/qt/contact_list.py b/gui/qt/contact_list.py t@@ -26,13 +26,13 @@ import webbrowser from electrum.i18n import _ from electrum.bitcoin import is_address -from electrum.util import block_explorer_URL, FileImportFailed +from electrum.util import block_explorer_URL from electrum.plugins import run_hook from PyQt5.QtGui import * from PyQt5.QtCore import * from PyQt5.QtWidgets import ( QAbstractItemView, QFileDialog, QMenu, QTreeWidgetItem) -from .util import MyTreeWidget +from .util import MyTreeWidget, import_meta_gui, export_meta_gui class ContactList(MyTreeWidget): t@@ -53,15 +53,10 @@ class ContactList(MyTreeWidget): self.parent.set_contact(item.text(0), item.text(1)) def import_contacts(self): - wallet_folder = self.parent.get_wallet_folder() - filename, __ = QFileDialog.getOpenFileName(self.parent, "Select your wallet file", wallet_folder) - if not filename: - return - try: - self.parent.contacts.import_file(filename) - except FileImportFailed as e: - self.parent.show_message(str(e)) - self.on_update() + import_meta_gui(self.parent, _('contacts'), self.parent.contacts.import_file, self.on_update) + + def export_contacts(self): + export_meta_gui(self.parent, _('contacts'), self.parent.contacts.export_file) def create_menu(self, position): menu = QMenu() t@@ -69,6 +64,7 @@ class ContactList(MyTreeWidget): if not selected: menu.addAction(_("New contact"), lambda: self.parent.new_contact_dialog()) menu.addAction(_("Import file"), lambda: self.import_contacts()) + menu.addAction(_("Export file"), lambda: self.export_contacts()) else: names = [item.text(0) for item in selected] keys = [item.text(1) for item in selected] DIR diff --git a/gui/qt/invoice_list.py b/gui/qt/invoice_list.py t@@ -23,9 +23,10 @@ # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -from .util import * from electrum.i18n import _ -from electrum.util import format_time, FileImportFailed +from electrum.util import format_time + +from .util import * class InvoiceList(MyTreeWidget): t@@ -57,15 +58,10 @@ class InvoiceList(MyTreeWidget): self.parent.invoices_label.setVisible(len(inv_list)) def import_invoices(self): - wallet_folder = self.parent.get_wallet_folder() - filename, __ = QFileDialog.getOpenFileName(self.parent, "Select your wallet file", wallet_folder) - if not filename: - return - try: - self.parent.invoices.import_file(filename) - except FileImportFailed as e: - self.parent.show_message(str(e)) - self.on_update() + import_meta_gui(self.parent, _('invoices'), self.parent.invoices.import_file, self.on_update) + + def export_invoices(self): + export_meta_gui(self.parent, _('invoices'), self.parent.invoices.export_file) def create_menu(self, position): menu = QMenu() DIR diff --git a/gui/qt/main_window.py b/gui/qt/main_window.py t@@ -39,15 +39,14 @@ import PyQt5.QtCore as QtCore from .exception_window import Exception_Hook from PyQt5.QtWidgets import * -from electrum.util import bh2u, bfh - from electrum import keystore, simple_config from electrum.bitcoin import COIN, is_address, TYPE_ADDRESS, NetworkConstants from electrum.plugins import run_hook from electrum.i18n import _ from electrum.util import (format_time, format_satoshis, PrintError, format_satoshis_plain, NotEnoughFunds, - UserCancelled, NoDynamicFeeEstimates) + UserCancelled, NoDynamicFeeEstimates, profiler, + export_meta, import_meta, bh2u, bfh) from electrum import Transaction from electrum import util, bitcoin, commands, coinchooser from electrum import paymentrequest t@@ -58,10 +57,8 @@ from .qrcodewidget import QRCodeWidget, QRDialog from .qrtextedit import ShowQRTextEdit, ScanQRTextEdit from .transaction_dialog import show_transaction from .fee_slider import FeeSlider - from .util import * -from electrum.util import profiler class StatusBarButton(QPushButton): def __init__(self, icon, tooltip, func): t@@ -484,8 +481,10 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): contacts_menu = wallet_menu.addMenu(_("Contacts")) contacts_menu.addAction(_("&New"), self.new_contact_dialog) contacts_menu.addAction(_("Import"), lambda: self.contact_list.import_contacts()) + contacts_menu.addAction(_("Export"), lambda: self.contact_list.export_contacts()) invoices_menu = wallet_menu.addMenu(_("Invoices")) invoices_menu.addAction(_("Import"), lambda: self.invoice_list.import_invoices()) + invoices_menu.addAction(_("Export"), lambda: self.invoice_list.export_invoices()) wallet_menu.addSeparator() wallet_menu.addAction(_("Find"), self.toggle_search).setShortcut(QKeySequence("Ctrl+F")) t@@ -2417,29 +2416,23 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): f.write(json.dumps(pklist, indent = 4)) def do_import_labels(self): - labelsFile = self.getOpenFileName(_("Open labels file"), "*.json") - if not labelsFile: return - try: - with open(labelsFile, 'r') as f: - data = f.read() - for key, value in json.loads(data).items(): - self.wallet.set_label(key, value) - self.show_message(_("Your labels were imported from") + " '%s'" % str(labelsFile)) - except (IOError, os.error) as reason: - self.show_critical(_("Electrum was unable to import your labels.") + "\n" + str(reason)) - self.address_list.update() - self.history_list.update() + def import_labels(path): + def _validate(data): + return data # TODO + + def import_labels_assign(data): + for key, value in data.items(): + self.wallet.set_label(key, value) + import_meta(path, _validate, import_labels_assign) + + def on_import(): + self.need_update.set() + import_meta_gui(self, _('labels'), import_labels, on_import) def do_export_labels(self): - labels = self.wallet.labels - try: - fileName = self.getSaveFileName(_("Select file to save your labels"), 'electrum_labels.json', "*.json") - if fileName: - with open(fileName, 'w+') as f: - json.dump(labels, f, indent=4, sort_keys=True) - self.show_message(_("Your labels were exported to") + " '%s'" % str(fileName)) - except (IOError, os.error) as reason: - self.show_critical(_("Electrum was unable to export your labels.") + "\n" + str(reason)) + def export_labels(filename): + export_meta(self.wallet.labels, filename) + export_meta_gui(self, _('labels'), export_labels) def sweep_key_dialog(self): d = WindowModalDialog(self, title=_('Sweep private keys')) DIR diff --git a/gui/qt/util.py b/gui/qt/util.py t@@ -6,11 +6,15 @@ import queue from collections import namedtuple from functools import partial -from electrum.i18n import _ from PyQt5.QtGui import * from PyQt5.QtCore import * from PyQt5.QtWidgets import * +from electrum.i18n import _ +from electrum.util import FileImportFailed, FileExportFailed +from electrum.paymentrequest import PR_UNPAID, PR_PAID, PR_EXPIRED + + if platform.system() == 'Windows': MONOSPACE_FONT = 'Lucida Console' elif platform.system() == 'Darwin': t@@ -21,8 +25,6 @@ else: dialogs = [] -from electrum.paymentrequest import PR_UNPAID, PR_PAID, PR_EXPIRED - pr_icons = { PR_UNPAID:":icons/unpaid.png", PR_PAID:":icons/confirmed.png", t@@ -675,6 +677,35 @@ class AcceptFileDragDrop: raise NotImplementedError() +def import_meta_gui(electrum_window, title, importer, on_success): + filter_ = "JSON (*.json);;All files (*)" + filename = electrum_window.getOpenFileName(_("Open {} file").format(title), filter_) + if not filename: + return + try: + importer(filename) + except FileImportFailed as e: + electrum_window.show_critical(str(e)) + else: + electrum_window.show_message(_("Your {} were successfully imported").format(title)) + on_success() + + +def export_meta_gui(electrum_window, title, exporter): + filter_ = "JSON (*.json);;All files (*)" + filename = electrum_window.getSaveFileName(_("Select file to save your {}").format(title), + 'electrum_{}.json'.format(title), filter_) + if not filename: + return + try: + exporter(filename) + except FileExportFailed as e: + electrum_window.show_critical(str(e)) + else: + electrum_window.show_message(_("Your {0} were exported to '{1}'") + .format(title, str(filename))) + + if __name__ == "__main__": app = QApplication([]) t = WaitingDialog(None, 'testing ...', lambda: [time.sleep(1)], lambda x: QMessageBox.information(None, 'done', "done")) DIR diff --git a/lib/contacts.py b/lib/contacts.py t@@ -28,7 +28,7 @@ import sys from . import bitcoin from . import dnssec -from .util import FileImportFailed, FileImportFailedEncrypted +from .util import export_meta, import_meta class Contacts(dict): t@@ -51,18 +51,15 @@ class Contacts(dict): self.storage.put('contacts', dict(self)) def import_file(self, path): - try: - with open(path, 'r') as f: - d = self._validate(json.loads(f.read())) - except json.decoder.JSONDecodeError: - traceback.print_exc(file=sys.stderr) - raise FileImportFailedEncrypted() - except BaseException: - traceback.print_exc(file=sys.stdout) - raise FileImportFailed() - self.update(d) + import_meta(path, self._validate, self.load_meta) + + def load_meta(self, data): + self.update(data) self.save() + def export_file(self, filename): + export_meta(self, filename) + def __setitem__(self, key, value): dict.__setitem__(self, key, value) self.save() t@@ -120,13 +117,13 @@ class Contacts(dict): return None def _validate(self, data): - for k,v in list(data.items()): + for k, v in list(data.items()): if k == 'contacts': return self._validate(v) if not bitcoin.is_address(k): data.pop(k) else: - _type,_ = v + _type, _ = v if _type != 'address': data.pop(k) return data DIR diff --git a/lib/paymentrequest.py b/lib/paymentrequest.py t@@ -40,7 +40,7 @@ except ImportError: from . import bitcoin from . import util from .util import print_error, bh2u, bfh -from .util import FileImportFailed, FileImportFailedEncrypted +from .util import export_meta, import_meta from . import transaction from . import x509 from . import rsakey t@@ -468,27 +468,29 @@ class InvoiceStore(object): continue def import_file(self, path): - try: - with open(path, 'r') as f: - d = json.loads(f.read()) - self.load(d) - except json.decoder.JSONDecodeError: - traceback.print_exc(file=sys.stderr) - raise FileImportFailedEncrypted() - except BaseException: - traceback.print_exc(file=sys.stdout) - raise FileImportFailed() + def validate(data): + return data # TODO + import_meta(path, validate, self.on_import) + + def on_import(self, data): + self.load(data) self.save() - def save(self): - l = {} + def export_file(self, filename): + export_meta(self.dump(), filename) + + def dump(self): + d = {} for k, pr in self.invoices.items(): - l[k] = { + d[k] = { 'hex': bh2u(pr.raw), 'requestor': pr.requestor, 'txid': pr.tx } - self.storage.put('invoices', l) + return d + + def save(self): + self.storage.put('invoices', self.dump()) def get_status(self, key): pr = self.get(key) DIR diff --git a/lib/util.py b/lib/util.py t@@ -59,15 +59,19 @@ class InvalidPassword(Exception): class FileImportFailed(Exception): + def __init__(self, message=''): + self.message = str(message) + def __str__(self): - return _("Failed to import file.") + return _("Failed to import from file.") + "\n" + self.message + +class FileExportFailed(Exception): + def __init__(self, message=''): + self.message = str(message) -class FileImportFailedEncrypted(FileImportFailed): def __str__(self): - return (_('Failed to import file.') + ' ' + - _('Perhaps it is encrypted...') + '\n' + - _('Importing encrypted files is not supported.')) + return _("Failed to export to file.") + "\n" + self.message # Throw this exception to unwind the stack like when an error occurs. t@@ -784,3 +788,26 @@ def setup_thread_excepthook(): def versiontuple(v): return tuple(map(int, (v.split(".")))) + + +def import_meta(path, validater, load_meta): + try: + with open(path, 'r') as f: + d = validater(json.loads(f.read())) + load_meta(d) + #backwards compatibility for JSONDecodeError + except ValueError: + traceback.print_exc(file=sys.stderr) + raise FileImportFailed(_("Invalid JSON code.")) + except BaseException as e: + traceback.print_exc(file=sys.stdout) + raise FileImportFailed(e) + + +def export_meta(meta, fileName): + try: + with open(fileName, 'w+') as f: + json.dump(meta, f, indent=4, sort_keys=True) + except (IOError, os.error) as e: + traceback.print_exc(file=sys.stderr) + raise FileExportFailed(e)