From 5997c18aef7200d8367014707f9212b440372010 Mon Sep 17 00:00:00 2001 From: Abdussamad Date: Thu, 15 Feb 2018 18:07:00 +0100 Subject: [PATCH 1/2] better code organization function parameters should be lowercase Fix crash on invalid labels import Added invoice exporting and reduced duplicate code Better exception handling removed json module import some more cleanup Cleaned up some stuff Added exporting contacts --- gui/qt/contact_list.py | 19 ++++++++----------- gui/qt/invoice_list.py | 15 +++++---------- gui/qt/main_window.py | 40 ++++++++++++++++++---------------------- gui/qt/util.py | 23 +++++++++++++++++++++++ lib/contacts.py | 22 ++++++++++------------ lib/paymentrequest.py | 32 ++++++++++++++++++-------------- lib/util.py | 35 +++++++++++++++++++++++++++++------ 7 files changed, 111 insertions(+), 75 deletions(-) diff --git a/gui/qt/contact_list.py b/gui/qt/contact_list.py index a17944593..81b6ca869 100644 --- a/gui/qt/contact_list.py +++ b/gui/qt/contact_list.py @@ -23,16 +23,17 @@ # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import webbrowser +import os 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): @@ -53,15 +54,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() @@ -69,6 +65,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] diff --git a/gui/qt/invoice_list.py b/gui/qt/invoice_list.py index a4a8374f7..d36c4866b 100644 --- a/gui/qt/invoice_list.py +++ b/gui/qt/invoice_list.py @@ -25,7 +25,7 @@ from .util import * from electrum.i18n import _ -from electrum.util import format_time, FileImportFailed +from electrum.util import format_time class InvoiceList(MyTreeWidget): @@ -57,15 +57,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() diff --git a/gui/qt/main_window.py b/gui/qt/main_window.py index 6b4a1f9bc..89c0ce1d7 100644 --- a/gui/qt/main_window.py +++ b/gui/qt/main_window.py @@ -61,7 +61,7 @@ from .fee_slider import FeeSlider from .util import * -from electrum.util import profiler +from electrum.util import profiler, export_meta, import_meta class StatusBarButton(QPushButton): def __init__(self, icon, tooltip, func): @@ -484,8 +484,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")) @@ -2417,29 +2419,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): + #TODO: Import labels validation + def import_labels_validate(data): + return data + def import_labels_assign(data): + for key, value in data.items(): + self.wallet.set_label(key, value) + import_meta(path, import_labels_validate, import_labels_assign) + def on_import(): + self.address_list.update() + self.history_list.update() + 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')) diff --git a/gui/qt/util.py b/gui/qt/util.py index 7f0cb238f..60a594bb0 100644 --- a/gui/qt/util.py +++ b/gui/qt/util.py @@ -11,6 +11,7 @@ from PyQt5.QtGui import * from PyQt5.QtCore import * from PyQt5.QtWidgets import * +from electrum.util import FileImportFailed, FileExportFailed if platform.system() == 'Windows': MONOSPACE_FONT = 'Lucida Console' elif platform.system() == 'Darwin': @@ -674,6 +675,28 @@ class AcceptFileDragDrop: def onFileAdded(self, fn): raise NotImplementedError() +def import_meta_gui(electrum_window, title, importer, on_success): + filename = electrum_window.getOpenFileName(_("Open {} file").format(title) , "*.json") + 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): + filename = electrum_window.getSaveFileName(_("Select file to save your {}").format(title), 'electrum_{}.json'.format(title), "*.json") + 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([]) diff --git a/lib/contacts.py b/lib/contacts.py index 5157adc41..df10e0863 100644 --- a/lib/contacts.py +++ b/lib/contacts.py @@ -25,10 +25,11 @@ import dns import json import traceback import sys +import os from . import bitcoin from . import dnssec -from .util import FileImportFailed, FileImportFailedEncrypted +from .util import export_meta, import_meta class Contacts(dict): @@ -51,18 +52,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() @@ -119,7 +117,7 @@ class Contacts(dict): except AttributeError: return None - def _validate(self, data): + def validate(self, data): for k,v in list(data.items()): if k == 'contacts': return self._validate(v) diff --git a/lib/paymentrequest.py b/lib/paymentrequest.py index c1e25441a..878c541e7 100644 --- a/lib/paymentrequest.py +++ b/lib/paymentrequest.py @@ -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 @@ -468,27 +468,31 @@ 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() + import_meta(path, self.validate, self.on_import) + + #TODO: Invoice import validation + def validate(self, data): + return data + + def on_import(self, data): + self.load(data) self.save() - def save(self): - l = {} + def export_file(self, fileName): + export_meta(self.before_save(), fileName) + + def before_save(self): + l= {} for k, pr in self.invoices.items(): l[k] = { 'hex': bh2u(pr.raw), 'requestor': pr.requestor, 'txid': pr.tx } - self.storage.put('invoices', l) + return l + + def save(self): + self.storage.put('invoices', self.before_save()) def get_status(self, key): pr = self.get(key) diff --git a/lib/util.py b/lib/util.py index 9dc95b912..ee66ff3f3 100644 --- a/lib/util.py +++ b/lib/util.py @@ -60,16 +60,18 @@ 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, reason=''): + self.message = str(reason) -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. # However unlike other exceptions the user won't be informed. @@ -785,3 +787,24 @@ 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 reason: + traceback.print_exc(file=sys.stderr) + raise FileExportFailed(str(reason)) From 500c0493d062f5fb239601587f9f466212590fc1 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 21 Feb 2018 18:55:37 +0100 Subject: [PATCH 2/2] clean up prev commit --- gui/qt/contact_list.py | 1 - gui/qt/invoice_list.py | 3 ++- gui/qt/main_window.py | 19 ++++++++----------- gui/qt/util.py | 32 ++++++++++++++++++++------------ lib/contacts.py | 13 ++++++------- lib/paymentrequest.py | 22 ++++++++++------------ lib/util.py | 24 ++++++++++++++---------- 7 files changed, 60 insertions(+), 54 deletions(-) diff --git a/gui/qt/contact_list.py b/gui/qt/contact_list.py index 81b6ca869..27c9efb59 100644 --- a/gui/qt/contact_list.py +++ b/gui/qt/contact_list.py @@ -23,7 +23,6 @@ # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import webbrowser -import os from electrum.i18n import _ from electrum.bitcoin import is_address diff --git a/gui/qt/invoice_list.py b/gui/qt/invoice_list.py index d36c4866b..586dd71c5 100644 --- a/gui/qt/invoice_list.py +++ b/gui/qt/invoice_list.py @@ -23,10 +23,11 @@ # 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 +from .util import * + class InvoiceList(MyTreeWidget): filter_columns = [0, 1, 2, 3] # Date, Requestor, Description, Amount diff --git a/gui/qt/main_window.py b/gui/qt/main_window.py index 89c0ce1d7..8ab63b871 100644 --- a/gui/qt/main_window.py +++ b/gui/qt/main_window.py @@ -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 @@ -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, export_meta, import_meta class StatusBarButton(QPushButton): def __init__(self, icon, tooltip, func): @@ -2420,16 +2417,16 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): def do_import_labels(self): def import_labels(path): - #TODO: Import labels validation - def import_labels_validate(data): - return data + 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, import_labels_validate, import_labels_assign) + import_meta(path, _validate, import_labels_assign) + def on_import(): - self.address_list.update() - self.history_list.update() + self.need_update.set() import_meta_gui(self, _('labels'), import_labels, on_import) def do_export_labels(self): diff --git a/gui/qt/util.py b/gui/qt/util.py index 60a594bb0..916762162 100644 --- a/gui/qt/util.py +++ b/gui/qt/util.py @@ -6,12 +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': @@ -22,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", @@ -675,28 +676,35 @@ class AcceptFileDragDrop: def onFileAdded(self, fn): raise NotImplementedError() + def import_meta_gui(electrum_window, title, importer, on_success): - filename = electrum_window.getOpenFileName(_("Open {} file").format(title) , "*.json") + filter_ = "JSON (*.json);;All files (*)" + filename = electrum_window.getOpenFileName(_("Open {} file").format(title), filter_) if not filename: - return + return try: - importer(filename) + importer(filename) except FileImportFailed as e: - electrum_window.show_critical(str(e)) + electrum_window.show_critical(str(e)) else: - electrum_window.show_message(_("Your {} were successfully imported" ).format(title)) - on_success() + electrum_window.show_message(_("Your {} were successfully imported").format(title)) + on_success() + def export_meta_gui(electrum_window, title, exporter): - filename = electrum_window.getSaveFileName(_("Select file to save your {}").format(title), 'electrum_{}.json'.format(title), "*.json") - if not filename: + 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))) + electrum_window.show_message(_("Your {0} were exported to '{1}'") + .format(title, str(filename))) + if __name__ == "__main__": app = QApplication([]) diff --git a/lib/contacts.py b/lib/contacts.py index df10e0863..0015a8610 100644 --- a/lib/contacts.py +++ b/lib/contacts.py @@ -25,7 +25,6 @@ import dns import json import traceback import sys -import os from . import bitcoin from . import dnssec @@ -52,14 +51,14 @@ class Contacts(dict): self.storage.put('contacts', dict(self)) def import_file(self, path): - import_meta(path, self.validate, self.load_meta) + 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 export_file(self, filename): + export_meta(self, filename) def __setitem__(self, key, value): dict.__setitem__(self, key, value) @@ -117,14 +116,14 @@ class Contacts(dict): except AttributeError: return None - def validate(self, data): - for k,v in list(data.items()): + def _validate(self, data): + 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 diff --git a/lib/paymentrequest.py b/lib/paymentrequest.py index 878c541e7..471186705 100644 --- a/lib/paymentrequest.py +++ b/lib/paymentrequest.py @@ -468,31 +468,29 @@ class InvoiceStore(object): continue def import_file(self, path): - import_meta(path, self.validate, self.on_import) - - #TODO: Invoice import validation - def validate(self, data): - return data + def validate(data): + return data # TODO + import_meta(path, validate, self.on_import) def on_import(self, data): self.load(data) self.save() - def export_file(self, fileName): - export_meta(self.before_save(), fileName) + def export_file(self, filename): + export_meta(self.dump(), filename) - def before_save(self): - l= {} + 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 } - return l + return d def save(self): - self.storage.put('invoices', self.before_save()) + self.storage.put('invoices', self.dump()) def get_status(self, key): pr = self.get(key) diff --git a/lib/util.py b/lib/util.py index ee66ff3f3..7099141b3 100644 --- a/lib/util.py +++ b/lib/util.py @@ -66,12 +66,14 @@ class FileImportFailed(Exception): def __str__(self): return _("Failed to import from file.") + "\n" + self.message + class FileExportFailed(Exception): - def __init__(self, reason=''): - self.message = str(reason) + def __init__(self, message=''): + self.message = str(message) def __str__(self): - return( _("Failed to export to file.") + "\n" + self.message ) + return _("Failed to export to file.") + "\n" + self.message + # Throw this exception to unwind the stack like when an error occurs. # However unlike other exceptions the user won't be informed. @@ -788,6 +790,7 @@ 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: @@ -798,13 +801,14 @@ def import_meta(path, validater, load_meta): traceback.print_exc(file=sys.stderr) raise FileImportFailed(_("Invalid JSON code.")) except BaseException as e: - traceback.print_exc(file=sys.stdout) - raise FileImportFailed(e) + traceback.print_exc(file=sys.stdout) + raise FileImportFailed(e) + def export_meta(meta, fileName): - try: - with open(fileName, 'w+') as f: + try: + with open(fileName, 'w+') as f: json.dump(meta, f, indent=4, sort_keys=True) - except (IOError, os.error) as reason: - traceback.print_exc(file=sys.stderr) - raise FileExportFailed(str(reason)) + except (IOError, os.error) as e: + traceback.print_exc(file=sys.stderr) + raise FileExportFailed(e)