URI: 
       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)