traitlets offer an observable implementation that makes the state of a single value observable, to which your code can react... in Python.
Widgets, or bundles of traitlets, make the state of the those traitlets to be updated, and reacted to, simultaneously on the kernel (backend) and in the notebook (front-end) over a high-performance WebSocket connection.
Building our playgrounds out of widgets lets us use both backend modules and front-end modules to achieve transformations, without writing too much code per playground, and very possibly only Python!
link, this doesn't let us supply arbitrary transformationsWe'll implement both of these as widgets... and then the playgrounds themselves. Let's get going!
In [ ]:
import traitlets
import ipywidgets as widgets
import types
Writing and distributing widget frontend code is a bit of a pain right now: however, we can (ab)use the live Jupyter Notebook to just ram some named modules right into the runtime environment. If these ever become a real library, they'll move to a static folder, and be installed with something like jupyter nbextension install.
In [ ]:
%%javascript
requirejs.undef("widget_codemirror");
define("widget_codemirror",
[
"underscore",
"jquery",
"components/codemirror/lib/codemirror",
"nbextensions/widgets/widgets/js/widget",
"base/js/namespace",
// silent upgrade
"components/codemirror/mode/meta"
],
function(_, $, CodeMirror, widget, Jupyter){
var CodeMirrorView = widget.DOMWidgetView.extend({
render: function(){
_.bindAll(this, "_init", "_cm_changed", "_mode_loaded");
this.displayed.then(this._init);
},
_init: function(){
this.$description = $("<div/>").appendTo(this.$el);
this._cm = new CodeMirror($("<div/>").appendTo(this.$el)[0], {
value: this.m("value")
});
this._cm.on("change", this._cm_changed);
this._theme_changed();
this._mode_changed();
this._description_changed();
// wire up magic functions
_.map(
["value", "theme", "mode", "description"],
function(name){
this.listenTo(
this.model,
"change:"+name,
this["_" + name + "_changed"]
);
}, this);
},
_cm_changed: function(){
this.m("value", this.cmv());
this.touch();
},
// model listeners
_value_changed: function(){
var value = this.m("value");
this._cm.hasFocus() || value === this.cmv() || this.cmv(value);
},
_theme_changed: function(){
var theme = this.m("theme"),
href = Jupyter.notebook.base_url + "static/components/codemirror/theme/" + theme + ".css",
style = $('link[href$="' + theme +'"]');
if(theme && !style.length){
$("<link/>", {rel: "stylesheet", href: href})
.appendTo($("head"));
}
this._cm.setOption("theme", theme);
},
_description_changed: function(){
this.$description.text(this.m("description"));
},
_mode_changed: function(){
var that = this,
mode = this.m("mode"),
spec = _.reduce(["Name", "MIME", "FileName"],
function(spec, approach){
return spec || CodeMirror["findModeBy" + approach](mode);
}, null);
if(!spec){ return; }
require(["components/codemirror/mode/" + spec.mode + "/" + spec.mode], function(){
that._cm.setOption("mode", spec.mime);
})
},
_mode_loaded: function(){
this._cm.setOption("mode", this.m("mode"));
},
cmv: function(_new){
var old = this._cm.getValue();
if(arguments.length){
old === _new || this._cm.setValue(_new);
return this;
}
return old;
},
// d3-style (g|s)etter
m: function(name, val){
if(arguments.length == 2){
this.model.set(name, val);
return this;
}
return this.model.get(name);
}
});
var CodeMirrorModel = widget.WidgetModel.extend({});
return {
CodeMirrorView: CodeMirrorView,
CodeMirrorModel: CodeMirrorModel
};
});
In [ ]:
class CodeMirror(widgets.DOMWidget):
_view_module = traitlets.Unicode("widget_codemirror").tag(sync=True)
_view_name = traitlets.Unicode("CodeMirrorView").tag(sync=True)
value = traitlets.Unicode().tag(sync=True)
description = traitlets.Unicode().tag(sync=True)
mode = traitlets.Unicode().tag(sync=True)
theme = traitlets.Unicode("monokai").tag(sync=True)
In [ ]:
cm = CodeMirror(value="print('hello world')",
description="Yay, code!",
mode="python",
theme="material")
cm
In [ ]:
class PipeBase(widgets.Widget):
# a tuple of (widget, "trait")s... or, if setting "value", just (widget)
sources = traitlets.Tuple([]).tag(sync=True, **widgets.widget_serialization)
# a single (widget, "trait")
dest = traitlets.Tuple().tag(sync=True, **widgets.widget_serialization)
# a place to put the last error
error = traitlets.CUnicode().tag(sync=True)
In [ ]:
class Pipe(PipeBase):
fn = traitlets.Union([
traitlets.Instance(klass=_type)
for _type in [types.FunctionType, types.BuiltinFunctionType]
])
def _update(self):
[src[0].observe(self._src_changed) for src in self.sources]
def _sources_changed(self, old, new):
self._update()
def _dest_changed(self, old, new):
self._update()
def _src_changed(self, change):
args = [
getattr(src[0], src[1] if len(src) == 2 else "value")
for src in self.sources
]
try:
setattr(self.dest[0],
self.dest[1] if len(self.dest) == 2 else "value",
self.fn(*args))
except Exception as err:
self.error = err
In [ ]:
import operator
x, y, z = [widgets.FloatSlider(value=1, description=it)
for it in "xyz"]
p = Pipe(fn=operator.mul,
sources=[[x], [y]],
dest=[z])
widgets.VBox(children=[x,y,z])
Sometimes the transformation you want is available only from a JavaScript module. We'll need a different approach to providing that capability. As with the CodeMirror widget, we'll write the JavaScript first. Note that since the user in the browser doesn't interact with the widget, instead of a WidgetView, we're overloading the WidgetModel.
In [ ]:
%%javascript
requirejs.undef("widget_pipe");
define("widget_pipe",
[
"underscore",
"jquery",
"nbextensions/widgets/widgets/js/widget",
'base/js/namespace',
],
function(_, $, widget, Jupyter){
var JSPipeModel = widget.WidgetModel.extend({
initialize: function(){
this.on("change:sources", this._update);
this.on("change:dest", this._update);
this.on("change:fn_module", this._module_changed);
},
_update: function(){
_.map(this.m("sources"), function(src){
this.listenTo(src[0], "change", this._evaluate)
}, this);
},
_module_changed: function(){
var that = this;
require([this.m("fn_module")], function(_module){
that._module = _module;
that._evaluate();
}, function(error){
console.error(error);
that.m("error", error);
})
},
_evaluate: function(){
var args = _.map(this.m("sources"), function(src){
return src[0].get(src.length === 1 ? "value": src[1])
}),
dest = this.m("dest"),
dest_attr = dest.length === 1 ? "value" : dest[1];
console.log()
try{
dest[0].set(
dest_attr,
this._module[this.m("fn")].apply(null, args)
);
}catch(error){
console.error(error)
this.m("error", error);
}
},
// d3-style (g|s)etter
m: function(name, val){
if(arguments.length == 2){
this.set(name, val);
return this;
}
return this.get(name);
}
}, {
serializers: _.extend({
sources: {deserialize: widget.unpack_models},
dest: {deserialize: widget.unpack_models},
}, widget.WidgetModel.prototype.serializers)
});
return {
JSPipeModel: JSPipeModel
};
});
In [ ]:
class JSPipe(PipeBase):
_model_module = traitlets.Unicode("widget_pipe").tag(sync=True)
_model_name = traitlets.Unicode("JSPipeModel").tag(sync=True)
fn = traitlets.Unicode().tag(sync=True)
fn_module = traitlets.Unicode().tag(sync=True)
CDNJS = "https://cdnjs.cloudflare.com/ajax/libs/"
JSDELIVR = "https://cdn.jsdelivr.net/g/"
This is the same thing as above, but handled by the frontend. We'll use math.js to get a nice, callable multiply function, but you can use any AMD-compatible module. If you need complex behavior, you'll have to make your own module!
In [ ]:
mathjs = JSPipe.CDNJS + "mathjs/2.6.0/math.js"
jx, jy, jz = [widgets.FloatSlider(value=1, description=it) for it in "xyz"]
p = JSPipe(fn="multiply", fn_module=mathjs, sources=[[jx], [jy]], dest=[jz])
widgets.VBox(children=[jx, jy, jz])
In [88]:
import yaml
import json
class YamlPlayground(widgets.FlexBox):
json = traitlets.Any({})
def __init__(self, *args, **kwargs):
self._yaml = CodeMirror(
value="x: 1",
description="YAML",
mode="yaml",
width="50%")
self._json = CodeMirror(
description="JSON",
mode="javascript",
width="50%")
# transform to YAML
Pipe(fn=lambda x: json.dumps(
yaml.safe_load(x),
indent=2),
sources=[[self._yaml]],
dest=[self._json])
kwargs.update(
children=[self._yaml, self._json],
orientation="horizontal"
)
super(YamlPlayground, self).__init__(
*args, **kwargs)
YamlPlayground()
In [ ]:
doc = """{
"@context": {
"rdfs": "http://www.w3.org/2000/01/rdf-schema#",
"name" : "rdfs:label"
},
"name": "Jane Doe"
}"""
context = """{
"@context": {
"rdfs": "http://www.w3.org/2000/01/rdf-schema#",
"name" : "rdfs:label"
}
}"""
In [ ]:
from pyld import jsonld
class JSONLDPlayground(widgets.FlexBox):
doc = traitlets.Any({})
context = traitlets.Any({})
compacted = traitlets.Any({})
def __init__(self, *args, **kwargs):
self._doc = CodeMirror(
value=doc,
description="Document",
mode="jsonld",
width="33%")
self._context = CodeMirror(
value=context,
description="Context",
mode="json",
width="33%")
self._compacted = CodeMirror(
value="{}",
description="Compacted",
mode="json",
width="33%")
kwargs.update(
children=[self._doc, self._context, self._compacted],
orientation="horizontal")
super(JSONLDPlayground, self).__init__(*args, **kwargs)
# transform to JSON
Pipe(fn=json.loads,
sources=[[self._doc]], dest=[self, "doc"])
Pipe(fn=json.loads,
sources=[[self._context]], dest=[self, "context"])
Pipe(fn=lambda x: json.dumps(x, indent=2),
sources=[[self, "compacted"]], dest=[self._compacted])
# finally, we can compact the JSON-LD
Pipe(fn=jsonld.compact,
sources=[[self, "doc"], [self, "context"]],
dest=[self, "compacted"])
pg = JSONLDPlayground()
pg
In [ ]:
%%javascript
requirejs.undef("simple_babel");
define("simple_babel", [
"https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/6.4.4/babel.min.js"
], function(Babel){
return {
transform: function(input){
return Babel.transform(
input, {
presets: ['es2015']
}).code;
}
}
})
In [ ]:
class BabelPlayground(widgets.FlexBox):
def __init__(self, *args, **kwargs):
self._es6 = CodeMirror(
value="()=> console.log('hello world')",
description="ES2015",
mode="javascript",
width="50%")
self._es5 = CodeMirror(
description="ES5",
mode="javascript",
width="50%")
kwargs.update(
children=[self._es6, self._es5],
orientation="horizontal"
)
super(BabelPlayground, self).__init__(*args, **kwargs)
# transform to YAML
JSPipe(
fn="transform",
fn_module="simple_babel",
sources=[[self._es6]],
dest=[self._es5])
BabelPlayground()