In [180]:
from __future__ import print_function # py 2.7 compat.

import base64
import urllib

# Widget definitions.
from IPython.html import widgets

# Traitlet needed to add synced attributes to the widget.
from IPython.utils.traitlets import Unicode

This is a custom widget that allows the user to upload file data to the notebook server. The file data is sent via a statefull value attribute of the widget. The widget has an upload failed event that fires in the front-end and is echoed to the back-end using a custom msg.

In [181]:
class FileWidget(widgets.DOMWidget):
    _view_name = Unicode('FilePickerView', sync=True)
    value = Unicode(sync=True)
    filename = Unicode(sync=True)
    
    def __init__(self, **kwargs):
        """Constructor"""
        widgets.DOMWidget.__init__(self, **kwargs) # Call the base.
        
        # Allow the user to register error callbacks with the following signatures:
        #    callback()
        #    callback(sender)
        self.errors = widgets.CallbackDispatcher(accepted_nargs=[0, 1])
        
        # Listen for custom msgs
        self.on_msg(self._handle_custom_msg)

    def _handle_custom_msg(self, content):
        """Handle a msg from the front-end.

        Parameters
        ----------
        content: dict
            Content of the msg."""
        if 'event' in content and content['event'] == 'error':
            self.errors()
            self.errors(self)
    
In [182]:
%%javascript
require(

["jquery", "underscore", "widgets/js/manager"],

function($, _, wm, Dropzone){    
    var FilePickerView = IPython.DOMWidgetView.extend({
        render: function(){
            // Render the view.
            this.$el.append($("<input/>", {type: "file"}));
        },
       
        events: {
            change: "handle_file_change"
        },
        
        handle_file_change: function(evt) { 
            // Handle when the user has changed the file.
            
            // Retrieve the first (and only!) File from the FileList object
            var file = evt.target.files[0];
            if (file) {
                // Read the file's textual content and set value to those contents.
                var that = this;
                var file_reader = new FileReader();
                file_reader.onload = function(e) {
                    that.model.set('value', e.target.result);
                    that.touch();
                }
                // by using a `data:` url, we get a nice portable representation which can be
                // easily rendered or embedded.
                file_reader.readAsDataURL(file);
            } else {
                // The file couldn't be opened.  Send an error msg to the
                // back-end.
                this.send({event: 'error'});
            }

            // Set the filename of the file.
            this.model.set('filename', file.name);
            this.touch();
        },
    });
        
    // Register the DatePickerView with the widget manager.
    wm.WidgetManager.register_widget_view('FilePickerView', FilePickerView);
});

Once the data url gets back to the backend, we'll want to do some work against it: this gist provides this nice API.

In [183]:
import mimetypes
import re
import urllib
 
 
MIMETYPE_REGEX = r'[\w]+\/[\w\-\+\.]+'
_MIMETYPE_RE = re.compile('^{}$'.format(MIMETYPE_REGEX))
 
CHARSET_REGEX = r'[\w\-\+\.]+'
_CHARSET_RE = re.compile('^{}$'.format(CHARSET_REGEX))
 
DATA_URI_REGEX = (
    r'data:' +
    r'(?P<mimetype>{})?'.format(MIMETYPE_REGEX) +
    r'(?:\;charset\=(?P<charset>{}))?'.format(CHARSET_REGEX) +
    r'(?P<base64>\;base64)?' +
    r',(?P<data>.*)')
_DATA_URI_RE = re.compile(r'^{}$'.format(DATA_URI_REGEX), re.DOTALL)
 
 
class DataURI(str):
 
    @classmethod
    def make(cls, mimetype, charset, base64, data):
        parts = ['data:']
        if mimetype is not None:
            if not _MIMETYPE_RE.match(mimetype):
                raise ValueError("Invalid mimetype: %r" % mimetype)
            parts.append(mimetype)
        if charset is not None:
            if not _CHARSET_RE.match(charset):
                raise ValueError("Invalid charset: %r" % charset)
            parts.extend([';charset=', charset])
        if base64:
            parts.append(';base64')
            encoded_data = data.encode('base64').replace('\n', '')
        else:
            encoded_data = urllib.quote(data)
        parts.extend([',', encoded_data])
        return cls(''.join(parts))
 
    @classmethod
    def from_file(cls, filename, charset=None, base64=True):
        mimetype, _ = mimetypes.guess_type(filename, strict=False)
        with open(filename) as fp:
            data = fp.read()
        return cls.make(mimetype, charset, base64, data)
 
    def __new__(cls, *args, **kwargs):
        uri = super(DataURI, cls).__new__(cls, *args, **kwargs)
        uri._parse  # Trigger any ValueErrors on instantiation.
        return uri
 
    def __repr__(self):
        return 'DataURI(%s)' % (super(DataURI, self).__repr__(),)
 
    def wrap(self, width=76):
        return type(self)('\n'.join(textwrap.wrap(self, width)))
 
    @property
    def mimetype(self):
        return self._parse[0]
 
    @property
    def charset(self):
        return self._parse[1]
 
    @property
    def is_base64(self):
        return self._parse[2]
 
    @property
    def data(self):
        return self._parse[3]
 
    @property
    def _parse(self):
        match = _DATA_URI_RE.match(self)
        if not match:
            raise ValueError("Not a valid data URI: %r" % self)
        mimetype = match.group('mimetype') or None
        charset = match.group('charset') or None
        if match.group('base64'):
            data = match.group('data').decode('base64')
        else:
            data = urllib.unquote(match.group('data'))
        return mimetype, charset, bool(match.group('base64')), data

The following shows how the file widget can be used.

In [184]:
import struct
file_widget = FileWidget()

# Register an event to echo the filename when it has been changed.
def file_loading():
    print("Loading %s" % file_widget.filename)
file_widget.on_trait_change(file_loading, 'filename')

# Register an event to echo the filename and contents when a file
# has been uploaded.
def file_loaded():
    uri = DataURI(file_widget.value)
    with open(file_widget.filename, "w+") as f:
        f.write(uri.data)
    print("Loaded, file contents saved to: %s" % file_widget.filename)
file_widget.on_trait_change(file_loaded, 'value')

# Register an event to print an error message when a file could not
# be opened.  Since the error messages are not handled through
# traitlets but instead handled through custom msgs, the registration
# of the handler is different than the two examples above.  Instead
# the API provided by the CallbackDispatcher must be used.
def file_failed():
    print("Could not load file contents of %s" % file_widget.filename)
file_widget.errors.register_callback(file_failed)

file_widget
Loading thumbnail.png
Loaded, file contents saved to: thumbnail.png