├── .gitignore ├── Apps in the notebook.ipynb ├── EuroScipy 2015 demo.ipynb ├── LICENSE ├── README.md ├── Scipy 2016 demo - Hijack notebook.ipynb ├── Scipy 2016 demo.ipynb ├── flexx_tutorial_app.ipynb ├── flexx_tutorial_event.ipynb ├── flexx_tutorial_pscript.ipynb ├── flexx_tutorial_ui.ipynb ├── flexx_tutorial_webruntime.ipynb └── remove_output.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | *.html 6 | .ipynb_checkpoints 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | env/ 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .coverage 43 | .coverage.* 44 | .cache 45 | nosetests.xml 46 | coverage.xml 47 | *,cover 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | 56 | # Sphinx documentation 57 | docs/_build/ 58 | 59 | # PyBuilder 60 | target/ 61 | -------------------------------------------------------------------------------- /EuroScipy 2015 demo.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "This is the demo that I used during the EuroScipy 2015 talk on Flexx." 8 | ] 9 | }, 10 | { 11 | "cell_type": "markdown", 12 | "metadata": {}, 13 | "source": [ 14 | "# flexx.webruntime" 15 | ] 16 | }, 17 | { 18 | "cell_type": "markdown", 19 | "metadata": {}, 20 | "source": [ 21 | "Launch a web runtime. Can be a browser or something that looks like a desktop app." 22 | ] 23 | }, 24 | { 25 | "cell_type": "code", 26 | "execution_count": 1, 27 | "metadata": { 28 | "collapsed": false 29 | }, 30 | "outputs": [], 31 | "source": [ 32 | "from flexx.webruntime import launch\n", 33 | "rt = launch('http://flexx.rtfd.org', 'xul', title='Test title')" 34 | ] 35 | }, 36 | { 37 | "cell_type": "markdown", 38 | "metadata": {}, 39 | "source": [ 40 | "# flexx.pyscript" 41 | ] 42 | }, 43 | { 44 | "cell_type": "code", 45 | "execution_count": 2, 46 | "metadata": { 47 | "collapsed": true 48 | }, 49 | "outputs": [], 50 | "source": [ 51 | "from flexx.pyscript import py2js" 52 | ] 53 | }, 54 | { 55 | "cell_type": "code", 56 | "execution_count": 3, 57 | "metadata": { 58 | "collapsed": false 59 | }, 60 | "outputs": [ 61 | { 62 | "name": "stdout", 63 | "output_type": "stream", 64 | "text": [ 65 | "var square;\n", 66 | "square = function (x) {return Math.pow(x, 2);};\n" 67 | ] 68 | } 69 | ], 70 | "source": [ 71 | "print(py2js('square = lambda x: x**2'))" 72 | ] 73 | }, 74 | { 75 | "cell_type": "code", 76 | "execution_count": 4, 77 | "metadata": { 78 | "collapsed": false 79 | }, 80 | "outputs": [ 81 | { 82 | "name": "stdout", 83 | "output_type": "stream", 84 | "text": [ 85 | "var foo;\n", 86 | "foo = function (n) {\n", 87 | " var i, res;\n", 88 | " res = [];\n", 89 | " for (i = 0; i < n; i += 1) {\n", 90 | " (res.append || res.push).apply(res, [Math.pow(i, 2)]);\n", 91 | " }\n", 92 | " return res;\n", 93 | "};\n", 94 | "\n" 95 | ] 96 | } 97 | ], 98 | "source": [ 99 | "def foo(n):\n", 100 | " res = []\n", 101 | " for i in range(n):\n", 102 | " res.append(i**2)\n", 103 | " return res\n", 104 | "print(py2js(foo))" 105 | ] 106 | }, 107 | { 108 | "cell_type": "code", 109 | "execution_count": 5, 110 | "metadata": { 111 | "collapsed": false 112 | }, 113 | "outputs": [ 114 | { 115 | "name": "stdout", 116 | "output_type": "stream", 117 | "text": [ 118 | "var foo;\n", 119 | "foo = function (n) {\n", 120 | " return (function list_comprehenson () {var res = [];var i, iter0, i0;iter0 = (function (start, end, step) {var i, res = []; for (i=start; i" 186 | ] 187 | }, 188 | "execution_count": 8, 189 | "metadata": {}, 190 | "output_type": "execute_result" 191 | } 192 | ], 193 | "source": [ 194 | "name" 195 | ] 196 | }, 197 | { 198 | "cell_type": "code", 199 | "execution_count": 9, 200 | "metadata": { 201 | "collapsed": false 202 | }, 203 | "outputs": [ 204 | { 205 | "name": "stdout", 206 | "output_type": "stream", 207 | "text": [ 208 | "hello John doe\n" 209 | ] 210 | } 211 | ], 212 | "source": [ 213 | "@react.connect('name')\n", 214 | "def greet(n):\n", 215 | " print('hello %s' % n)" 216 | ] 217 | }, 218 | { 219 | "cell_type": "code", 220 | "execution_count": 10, 221 | "metadata": { 222 | "collapsed": false 223 | }, 224 | "outputs": [ 225 | { 226 | "name": "stdout", 227 | "output_type": "stream", 228 | "text": [ 229 | "hello Almar klein\n" 230 | ] 231 | } 232 | ], 233 | "source": [ 234 | "name(\"almar klein\")" 235 | ] 236 | }, 237 | { 238 | "cell_type": "markdown", 239 | "metadata": {}, 240 | "source": [ 241 | "A signal can have multiple upstream signals." 242 | ] 243 | }, 244 | { 245 | "cell_type": "code", 246 | "execution_count": 11, 247 | "metadata": { 248 | "collapsed": false 249 | }, 250 | "outputs": [], 251 | "source": [ 252 | "@react.connect('first_name', 'last_name')\n", 253 | "def greet(first, last):\n", 254 | " print('hello %s %s!' % (first, last))" 255 | ] 256 | }, 257 | { 258 | "cell_type": "markdown", 259 | "metadata": {}, 260 | "source": [ 261 | "*Dynamism* provides great flexibility" 262 | ] 263 | }, 264 | { 265 | "cell_type": "code", 266 | "execution_count": 12, 267 | "metadata": { 268 | "collapsed": true 269 | }, 270 | "outputs": [], 271 | "source": [ 272 | "class Person(react.HasSignals):\n", 273 | " \n", 274 | " @react.input\n", 275 | " def father(f):\n", 276 | " assert isinstance(f, Person)\n", 277 | " return f\n", 278 | "\n", 279 | " @react.connect('father.last_name')\n", 280 | " def last_name(s):\n", 281 | " return s\n", 282 | " \n", 283 | " @react.connect('children.*.name')\n", 284 | " def child_names(*names):\n", 285 | " return ', '.join(name)" 286 | ] 287 | }, 288 | { 289 | "cell_type": "markdown", 290 | "metadata": {}, 291 | "source": [ 292 | "# flexx.app" 293 | ] 294 | }, 295 | { 296 | "cell_type": "code", 297 | "execution_count": 13, 298 | "metadata": { 299 | "collapsed": false 300 | }, 301 | "outputs": [ 302 | { 303 | "name": "stdout", 304 | "output_type": "stream", 305 | "text": [ 306 | "Serving apps at http://localhost:54307/\n" 307 | ] 308 | }, 309 | { 310 | "data": { 311 | "text/html": [ 312 | "Injecting Flexx JS and CSS\n", 317 | "" 1260 | ], 1261 | "text/plain": [ 1262 | "" 1263 | ] 1264 | }, 1265 | "metadata": {}, 1266 | "output_type": "display_data" 1267 | } 1268 | ], 1269 | "source": [ 1270 | "from flexx import app, react\n", 1271 | "app.init_notebook()" 1272 | ] 1273 | }, 1274 | { 1275 | "cell_type": "code", 1276 | "execution_count": 14, 1277 | "metadata": { 1278 | "collapsed": false 1279 | }, 1280 | "outputs": [ 1281 | { 1282 | "name": "stdout", 1283 | "output_type": "stream", 1284 | "text": [ 1285 | "new ws connection __default__\n" 1286 | ] 1287 | } 1288 | ], 1289 | "source": [ 1290 | "class Greeter(app.Model):\n", 1291 | " \n", 1292 | " @react.input\n", 1293 | " def name(s):\n", 1294 | " return str(s)\n", 1295 | " \n", 1296 | " class JS:\n", 1297 | " \n", 1298 | " @react.connect('name')\n", 1299 | " def _greet(name):\n", 1300 | " alert('Hello %s!' % name)" 1301 | ] 1302 | }, 1303 | { 1304 | "cell_type": "code", 1305 | "execution_count": 15, 1306 | "metadata": { 1307 | "collapsed": false 1308 | }, 1309 | "outputs": [ 1310 | { 1311 | "name": "stderr", 1312 | "output_type": "stream", 1313 | "text": [ 1314 | "WARNING:root:Dynamically defining class \n" 1315 | ] 1316 | }, 1317 | { 1318 | "data": { 1319 | "application/javascript": [ 1320 | "flexx.command('DEFINE-JS flexx.classes.Greeter = function () {\\n if (this.__init__) {\\n this.__init__.apply(this, arguments);\\n }\\n};\\nflexx.classes.Greeter.prototype = Object.create(flexx.classes.Model.prototype);\\nflexx.classes.Greeter.prototype._base_class = flexx.classes.Model.prototype;\\n\\n\\nflexx.classes.Greeter.prototype.__signals__ = flexx.classes.Model.prototype.__signals__.concat([\\'_greet\\', \\'name\\']).sort();\\nflexx.classes.Greeter.prototype.__greet_func = function (name) {\\n alert(\\'Hello \\' + name + \\'!\\');\\n return null;\\n};\\n\\nflexx.classes.Greeter.prototype.__greet_func._upstream = [\\'name\\'];\\n\\nflexx.classes.Greeter.prototype.__greet_func._signal_type = \\'Signal\\';\\n\\nflexx.classes.Greeter.prototype.__greet_func.flags = JSON.parse(\\'{}\\');\\n\\nflexx.classes.Greeter.prototype._name_func = function (v) {\\n return v;\\n};\\n\\nflexx.classes.Greeter.prototype._name_func._upstream = [];\\n\\nflexx.classes.Greeter.prototype._name_func._signal_type = \\'PyInputSignal\\';\\n\\nflexx.classes.Greeter.prototype._name_func.flags = JSON.parse(\\'{}\\');\\nflexx.classes.Greeter.prototype._class_name = \"Greeter\";\\n');" 1321 | ], 1322 | "text/plain": [ 1323 | "" 1324 | ] 1325 | }, 1326 | "metadata": {}, 1327 | "output_type": "display_data" 1328 | }, 1329 | { 1330 | "data": { 1331 | "application/javascript": [ 1332 | "flexx.command(\"EXEC flexx.instances.Greeter1 = new flexx.classes.Greeter('Greeter1');\");" 1333 | ], 1334 | "text/plain": [ 1335 | "" 1336 | ] 1337 | }, 1338 | "metadata": {}, 1339 | "output_type": "display_data" 1340 | } 1341 | ], 1342 | "source": [ 1343 | "greeter = Greeter()" 1344 | ] 1345 | }, 1346 | { 1347 | "cell_type": "code", 1348 | "execution_count": 16, 1349 | "metadata": { 1350 | "collapsed": false 1351 | }, 1352 | "outputs": [ 1353 | { 1354 | "data": { 1355 | "application/javascript": [ 1356 | "flexx.command('EXEC flexx.instances.Greeter1._set_signal_from_py(\\'name\\', \\'\"John\"\\', 0);');" 1357 | ], 1358 | "text/plain": [ 1359 | "" 1360 | ] 1361 | }, 1362 | "metadata": {}, 1363 | "output_type": "display_data" 1364 | } 1365 | ], 1366 | "source": [ 1367 | "greeter.name('John')" 1368 | ] 1369 | }, 1370 | { 1371 | "cell_type": "code", 1372 | "execution_count": null, 1373 | "metadata": { 1374 | "collapsed": true 1375 | }, 1376 | "outputs": [], 1377 | "source": [] 1378 | } 1379 | ], 1380 | "metadata": { 1381 | "kernelspec": { 1382 | "display_name": "Python 3", 1383 | "language": "python", 1384 | "name": "python3" 1385 | }, 1386 | "language_info": { 1387 | "codemirror_mode": { 1388 | "name": "ipython", 1389 | "version": 3 1390 | }, 1391 | "file_extension": ".py", 1392 | "mimetype": "text/x-python", 1393 | "name": "python", 1394 | "nbconvert_exporter": "python", 1395 | "pygments_lexer": "ipython3", 1396 | "version": "3.4.3" 1397 | } 1398 | }, 1399 | "nbformat": 4, 1400 | "nbformat_minor": 0 1401 | } 1402 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, Zoof org 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | * Neither the name of flexx-notebooks nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # flexx-notebooks 2 | Jupyter notebooks with Flexx examples. 3 | -------------------------------------------------------------------------------- /Scipy 2016 demo - Hijack notebook.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Flexx demo - Hijacking the notebook" 8 | ] 9 | }, 10 | { 11 | "cell_type": "code", 12 | "execution_count": null, 13 | "metadata": { 14 | "collapsed": false 15 | }, 16 | "outputs": [], 17 | "source": [ 18 | "from flexx import app, event, ui\n", 19 | "app.init_notebook()" 20 | ] 21 | }, 22 | { 23 | "cell_type": "markdown", 24 | "metadata": {}, 25 | "source": [ 26 | "### Create a dock panel and attach to ````" 27 | ] 28 | }, 29 | { 30 | "cell_type": "code", 31 | "execution_count": null, 32 | "metadata": { 33 | "collapsed": false 34 | }, 35 | "outputs": [], 36 | "source": [ 37 | "dock = ui.DockPanel(container='body')" 38 | ] 39 | }, 40 | { 41 | "cell_type": "code", 42 | "execution_count": null, 43 | "metadata": { 44 | "collapsed": false 45 | }, 46 | "outputs": [], 47 | "source": [ 48 | "from flexx.ui.examples.twente import Twente\n", 49 | "twente = Twente(parent=dock, title='Temperature')" 50 | ] 51 | }, 52 | { 53 | "cell_type": "code", 54 | "execution_count": null, 55 | "metadata": { 56 | "collapsed": false 57 | }, 58 | "outputs": [], 59 | "source": [ 60 | "from flexx.ui.examples.splines import Splines\n", 61 | "splines = Splines(parent=dock, title='Splines')" 62 | ] 63 | }, 64 | { 65 | "cell_type": "markdown", 66 | "metadata": {}, 67 | "source": [ 68 | "Now the hijack ...." 69 | ] 70 | }, 71 | { 72 | "cell_type": "code", 73 | "execution_count": null, 74 | "metadata": { 75 | "collapsed": false 76 | }, 77 | "outputs": [], 78 | "source": [ 79 | "class JupyterContainer(ui.Widget):\n", 80 | " \n", 81 | " class JS:\n", 82 | " \n", 83 | " def init(self):\n", 84 | " for name in ('header', 'site'):\n", 85 | " el = document.getElementById(name)\n", 86 | " self.node.appendChild(el)" 87 | ] 88 | }, 89 | { 90 | "cell_type": "code", 91 | "execution_count": null, 92 | "metadata": { 93 | "collapsed": false 94 | }, 95 | "outputs": [], 96 | "source": [ 97 | "notebook = JupyterContainer(parent=dock, title='Notebook')" 98 | ] 99 | }, 100 | { 101 | "cell_type": "code", 102 | "execution_count": null, 103 | "metadata": { 104 | "collapsed": false 105 | }, 106 | "outputs": [], 107 | "source": [ 108 | "splines.closed.checked = True" 109 | ] 110 | }, 111 | { 112 | "cell_type": "markdown", 113 | "metadata": {}, 114 | "source": [ 115 | "### Add Bokeh plot" 116 | ] 117 | }, 118 | { 119 | "cell_type": "code", 120 | "execution_count": null, 121 | "metadata": { 122 | "collapsed": false 123 | }, 124 | "outputs": [], 125 | "source": [ 126 | "from bokeh.models import Range1d, WMTSTileSource, ColumnDataSource, HoverTool\n", 127 | "from bokeh.plotting import figure\n", 128 | "from bokeh.sampledata.airports import data as airports\n", 129 | "\n", 130 | "title = \"US Airports: Field Elevation > 1500m\"\n", 131 | "\n", 132 | "points_source = ColumnDataSource(airports)\n", 133 | "tile_source = WMTSTileSource(url='http://otile2.mqcdn.com/tiles/1.0.0/sat/{Z}/{X}/{Y}.png')\n", 134 | "\n", 135 | "x_range = Range1d(start=airports['x'].min() - 10000, end=airports['x'].max() + 10000, bounds=None)\n", 136 | "y_range = Range1d(start=airports['y'].min() - 10000, end=airports['y'].max() + 10000, bounds=None)\n", 137 | "\n", 138 | "p = figure(tools='wheel_zoom,pan', x_range=x_range, y_range=y_range, title=title)\n", 139 | "p.axis.visible = False\n", 140 | "hover_tool = HoverTool(tooltips=[(\"Name\", \"@name\"), (\"Elevation\", \"@elevation (m)\")])\n", 141 | "p.add_tools(hover_tool)\n", 142 | "p.add_tile(tile_source)\n", 143 | "p.circle(x='x', y='y', size=9, fill_color=\"#60ACA1\", line_color=\"#D2C4C1\", line_width=1.5, source=points_source)" 144 | ] 145 | }, 146 | { 147 | "cell_type": "code", 148 | "execution_count": null, 149 | "metadata": { 150 | "collapsed": false 151 | }, 152 | "outputs": [], 153 | "source": [ 154 | "bw1 = ui.BokehWidget(plot=p, title='Airports', style='height:400px')\n", 155 | "bw1" 156 | ] 157 | }, 158 | { 159 | "cell_type": "code", 160 | "execution_count": null, 161 | "metadata": { 162 | "collapsed": false 163 | }, 164 | "outputs": [], 165 | "source": [ 166 | "bw1.parent = dock" 167 | ] 168 | }, 169 | { 170 | "cell_type": "code", 171 | "execution_count": null, 172 | "metadata": { 173 | "collapsed": false 174 | }, 175 | "outputs": [], 176 | "source": [ 177 | "import numpy as np\n", 178 | "from bokeh.plotting import figure \n", 179 | "\n", 180 | "N = 10000\n", 181 | "x = np.random.normal(0, np.pi, N)\n", 182 | "y = np.sin(x) + np.random.normal(0, 0.2, N)\n", 183 | "\n", 184 | "TOOLS = \"pan,wheel_zoom,box_zoom,reset,save,box_select\"\n", 185 | "p = figure(tools=TOOLS, webgl=True)\n", 186 | "\n", 187 | "p.circle(x, y, alpha=0.1, nonselection_alpha=0.01)" 188 | ] 189 | }, 190 | { 191 | "cell_type": "code", 192 | "execution_count": null, 193 | "metadata": { 194 | "collapsed": false 195 | }, 196 | "outputs": [], 197 | "source": [ 198 | "bw2 = ui.BokehWidget(plot=p, title='10K points', style='height:400px')\n", 199 | "bw2" 200 | ] 201 | }, 202 | { 203 | "cell_type": "code", 204 | "execution_count": null, 205 | "metadata": { 206 | "collapsed": false 207 | }, 208 | "outputs": [], 209 | "source": [ 210 | "bw2.parent = dock" 211 | ] 212 | }, 213 | { 214 | "cell_type": "code", 215 | "execution_count": null, 216 | "metadata": { 217 | "collapsed": true 218 | }, 219 | "outputs": [], 220 | "source": [] 221 | } 222 | ], 223 | "metadata": { 224 | "kernelspec": { 225 | "display_name": "Python 3", 226 | "language": "python", 227 | "name": "python3" 228 | }, 229 | "language_info": { 230 | "codemirror_mode": { 231 | "name": "ipython", 232 | "version": 3 233 | }, 234 | "file_extension": ".py", 235 | "mimetype": "text/x-python", 236 | "name": "python", 237 | "nbconvert_exporter": "python", 238 | "pygments_lexer": "ipython3", 239 | "version": "3.5.1" 240 | } 241 | }, 242 | "nbformat": 4, 243 | "nbformat_minor": 0 244 | } 245 | -------------------------------------------------------------------------------- /flexx_tutorial_event.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Tutorial for flexx.event - properties and events" 8 | ] 9 | }, 10 | { 11 | "cell_type": "code", 12 | "execution_count": 1, 13 | "metadata": {}, 14 | "outputs": [], 15 | "source": [ 16 | "%gui asyncio\n", 17 | "from flexx import event" 18 | ] 19 | }, 20 | { 21 | "cell_type": "markdown", 22 | "metadata": {}, 23 | "source": [ 24 | "### Events\n", 25 | "In `flexx.event`, events are represented with dictionary objects that\n", 26 | "provide information about the event (such as what button was pressed,\n", 27 | "or the new value of a property). A custom `Dict` class is used that inherits from ``dict`` and allows attribute access,\n", 28 | "e.g. ``ev.button`` as an alternative to ``ev['button']``." 29 | ] 30 | }, 31 | { 32 | "cell_type": "markdown", 33 | "metadata": {}, 34 | "source": [ 35 | "### Reactions\n", 36 | "\n", 37 | "Events originate from `Component` objects. When an event is emitted, it can be reacted upon." 38 | ] 39 | }, 40 | { 41 | "cell_type": "code", 42 | "execution_count": 2, 43 | "metadata": {}, 44 | "outputs": [], 45 | "source": [ 46 | "class MyObject(event.Component):\n", 47 | " \n", 48 | " @event.reaction('!foo')\n", 49 | " def on_foo(self, *events):\n", 50 | " print('received the foo event %i times' % len(events))\n", 51 | "\n", 52 | "ob = MyObject()" 53 | ] 54 | }, 55 | { 56 | "cell_type": "code", 57 | "execution_count": 3, 58 | "metadata": {}, 59 | "outputs": [ 60 | { 61 | "name": "stdout", 62 | "output_type": "stream", 63 | "text": [ 64 | "received the foo event 3 times\n" 65 | ] 66 | } 67 | ], 68 | "source": [ 69 | "for i in range(3):\n", 70 | " ob.emit('foo', {})" 71 | ] 72 | }, 73 | { 74 | "cell_type": "markdown", 75 | "metadata": {}, 76 | "source": [ 77 | "Note how the reaction is connected using a \"connection string\", which (in this case) indicates we connect to the type \"foo\" event of the object. The connection string allows some powerful mechanics, as we will see later in this tutorial. Here, we prefixed the connection string with \"!\", to supress a warning that Flexx would otherwise give, because it does not know about the \"foo\" event.\n", 78 | "\n", 79 | "Also note how the reaction accepts multiple events at once. This means that in situations where we only care about something being changed, we can skip \"duplicate\" events. In situation where each individual event needs processing, use `for ev in events: ...`." 80 | ] 81 | }, 82 | { 83 | "cell_type": "markdown", 84 | "metadata": {}, 85 | "source": [ 86 | "Reactions can also be used as normal methods:" 87 | ] 88 | }, 89 | { 90 | "cell_type": "code", 91 | "execution_count": 4, 92 | "metadata": {}, 93 | "outputs": [ 94 | { 95 | "name": "stdout", 96 | "output_type": "stream", 97 | "text": [ 98 | "received the foo event 0 times\n" 99 | ] 100 | } 101 | ], 102 | "source": [ 103 | "ob.on_foo()" 104 | ] 105 | }, 106 | { 107 | "cell_type": "markdown", 108 | "metadata": {}, 109 | "source": [ 110 | "A reaction can also connect to multiple events:" 111 | ] 112 | }, 113 | { 114 | "cell_type": "code", 115 | "execution_count": 5, 116 | "metadata": {}, 117 | "outputs": [ 118 | { 119 | "data": { 120 | "text/plain": [ 121 | "Dict([('type', 'bar'), ('source', )])" 122 | ] 123 | }, 124 | "execution_count": 5, 125 | "metadata": {}, 126 | "output_type": "execute_result" 127 | }, 128 | { 129 | "name": "stdout", 130 | "output_type": "stream", 131 | "text": [ 132 | "received the foo event\n", 133 | "received the foo event\n", 134 | "received the bar event\n" 135 | ] 136 | } 137 | ], 138 | "source": [ 139 | "class MyObject(event.Component):\n", 140 | " \n", 141 | " @event.reaction('!foo', '!bar')\n", 142 | " def on_foo_or_bar(self, *events):\n", 143 | " for ev in events:\n", 144 | " print('received the %s event' % ev.type)\n", 145 | "\n", 146 | "ob = MyObject()\n", 147 | "ob.emit('foo', {}); ob.emit('foo', {}); ob.emit('bar', {})" 148 | ] 149 | }, 150 | { 151 | "cell_type": "markdown", 152 | "metadata": {}, 153 | "source": [ 154 | "### Properties\n", 155 | "Properties represent the state of a component." 156 | ] 157 | }, 158 | { 159 | "cell_type": "code", 160 | "execution_count": 6, 161 | "metadata": {}, 162 | "outputs": [ 163 | { 164 | "name": "stdout", 165 | "output_type": "stream", 166 | "text": [ 167 | "foo changed from 2 to 2\n" 168 | ] 169 | } 170 | ], 171 | "source": [ 172 | "class MyObject(event.Component):\n", 173 | " \n", 174 | " foo = event.IntProp(2, settable=True)\n", 175 | " \n", 176 | " @event.reaction('foo')\n", 177 | " def on_foo(self, *events):\n", 178 | " print('foo changed from', events[0].old_value, 'to', events[-1].new_value)\n", 179 | "\n", 180 | "ob = MyObject()" 181 | ] 182 | }, 183 | { 184 | "cell_type": "code", 185 | "execution_count": 7, 186 | "metadata": {}, 187 | "outputs": [ 188 | { 189 | "data": { 190 | "text/plain": [ 191 | "" 192 | ] 193 | }, 194 | "execution_count": 7, 195 | "metadata": {}, 196 | "output_type": "execute_result" 197 | }, 198 | { 199 | "name": "stdout", 200 | "output_type": "stream", 201 | "text": [ 202 | "foo changed from 2 to 7\n" 203 | ] 204 | } 205 | ], 206 | "source": [ 207 | "ob.set_foo(7)" 208 | ] 209 | }, 210 | { 211 | "cell_type": "code", 212 | "execution_count": 8, 213 | "metadata": {}, 214 | "outputs": [ 215 | { 216 | "name": "stdout", 217 | "output_type": "stream", 218 | "text": [ 219 | "7\n" 220 | ] 221 | } 222 | ], 223 | "source": [ 224 | "print(ob.foo)" 225 | ] 226 | }, 227 | { 228 | "cell_type": "markdown", 229 | "metadata": {}, 230 | "source": [ 231 | "Properties can also be set during initialization." 232 | ] 233 | }, 234 | { 235 | "cell_type": "code", 236 | "execution_count": 9, 237 | "metadata": {}, 238 | "outputs": [ 239 | { 240 | "name": "stdout", 241 | "output_type": "stream", 242 | "text": [ 243 | "foo changed from 12 to 12\n" 244 | ] 245 | } 246 | ], 247 | "source": [ 248 | "ob = MyObject(foo=12)" 249 | ] 250 | }, 251 | { 252 | "cell_type": "markdown", 253 | "metadata": {}, 254 | "source": [ 255 | "Properties are readonly. This may seem like a limitation at first, but it helps make apps more predictable, especially as they become larger. Properties can be mutated using actions. In the above example, a setter action was created automatically because we specified `setter=True` in the definition of the property." 256 | ] 257 | }, 258 | { 259 | "cell_type": "markdown", 260 | "metadata": {}, 261 | "source": [ 262 | "### Actions\n", 263 | "Actions are special functions that are invoked asynchronously, i.e. when they are invoked (called) the action itself is applied in a futre iteration of the event loop. (The `%gui asyncio` at the top of the notebook makes sure that Flexx' event loop is running.)" 264 | ] 265 | }, 266 | { 267 | "cell_type": "code", 268 | "execution_count": 10, 269 | "metadata": {}, 270 | "outputs": [ 271 | { 272 | "name": "stdout", 273 | "output_type": "stream", 274 | "text": [ 275 | "foo changed from 2 to 2\n" 276 | ] 277 | } 278 | ], 279 | "source": [ 280 | "class MyObject(event.Component):\n", 281 | " \n", 282 | " foo = event.IntProp(2, settable=True)\n", 283 | " \n", 284 | " @event.action\n", 285 | " def increase_foo(self):\n", 286 | " self._mutate_foo(self.foo + 1)\n", 287 | " \n", 288 | " @event.reaction('foo')\n", 289 | " def on_foo(self, *events):\n", 290 | " print('foo changed from', events[0].old_value, 'to', events[-1].new_value)\n", 291 | "\n", 292 | "ob = MyObject()" 293 | ] 294 | }, 295 | { 296 | "cell_type": "code", 297 | "execution_count": 11, 298 | "metadata": {}, 299 | "outputs": [ 300 | { 301 | "data": { 302 | "text/plain": [ 303 | "" 304 | ] 305 | }, 306 | "execution_count": 11, 307 | "metadata": {}, 308 | "output_type": "execute_result" 309 | }, 310 | { 311 | "name": "stdout", 312 | "output_type": "stream", 313 | "text": [ 314 | "foo changed from 2 to 3\n" 315 | ] 316 | } 317 | ], 318 | "source": [ 319 | "ob.increase_foo()" 320 | ] 321 | }, 322 | { 323 | "cell_type": "markdown", 324 | "metadata": {}, 325 | "source": [ 326 | "The action above mutates the foo property. Properties can only be mutated by actions. This ensures that the state of a component (and of the whole app) is consistent during the handling of reactions." 327 | ] 328 | }, 329 | { 330 | "cell_type": "markdown", 331 | "metadata": {}, 332 | "source": [ 333 | "### Emitters\n", 334 | "Emitters make it easy to generate events from specific input (e.g. an event from another kind of event system) and act as a placeholder for the docs of public events." 335 | ] 336 | }, 337 | { 338 | "cell_type": "code", 339 | "execution_count": 12, 340 | "metadata": {}, 341 | "outputs": [], 342 | "source": [ 343 | "class MyObject(event.Component):\n", 344 | "\n", 345 | " @event.emitter\n", 346 | " def mouse_down(self, js_event):\n", 347 | " ''' Event emitted when the mouse is pressed down. '''\n", 348 | " return dict(button=js_event['button'])\n", 349 | " \n", 350 | " @event.reaction('mouse_down')\n", 351 | " def on_bar(self, *events):\n", 352 | " for ev in events:\n", 353 | " print('detected mouse_down, button', ev.button)\n", 354 | "\n", 355 | "ob = MyObject()" 356 | ] 357 | }, 358 | { 359 | "cell_type": "code", 360 | "execution_count": 13, 361 | "metadata": {}, 362 | "outputs": [ 363 | { 364 | "name": "stdout", 365 | "output_type": "stream", 366 | "text": [ 367 | "detected mouse_down, button 1\n", 368 | "detected mouse_down, button 2\n" 369 | ] 370 | } 371 | ], 372 | "source": [ 373 | "ob.mouse_down({'button': 1})\n", 374 | "ob.mouse_down({'button': 2})" 375 | ] 376 | }, 377 | { 378 | "cell_type": "markdown", 379 | "metadata": {}, 380 | "source": [ 381 | "### Implicit reactions\n", 382 | "Implicit reactions make it easy to write concise code that needs to keep track of state. To create an implicit reaction, simply provide no connection strings. The reaction will now automatically track all properties that the reaction is accessing. This even works dynamically, e.g. when accessing a property on each element in a list property, the reaction will automatically \"reconnect\" when the list changes.\n" 383 | ] 384 | }, 385 | { 386 | "cell_type": "code", 387 | "execution_count": 14, 388 | "metadata": {}, 389 | "outputs": [ 390 | { 391 | "name": "stdout", 392 | "output_type": "stream", 393 | "text": [ 394 | "foo changed is now 2\n" 395 | ] 396 | } 397 | ], 398 | "source": [ 399 | "class MyObject(event.Component):\n", 400 | " \n", 401 | " foo = event.IntProp(2, settable=True)\n", 402 | " \n", 403 | " @event.reaction\n", 404 | " def on_foo(self):\n", 405 | " print('foo changed is now', self.foo)\n", 406 | "\n", 407 | "ob = MyObject()" 408 | ] 409 | }, 410 | { 411 | "cell_type": "code", 412 | "execution_count": 15, 413 | "metadata": {}, 414 | "outputs": [ 415 | { 416 | "data": { 417 | "text/plain": [ 418 | "" 419 | ] 420 | }, 421 | "execution_count": 15, 422 | "metadata": {}, 423 | "output_type": "execute_result" 424 | }, 425 | { 426 | "name": "stdout", 427 | "output_type": "stream", 428 | "text": [ 429 | "foo changed is now 99\n" 430 | ] 431 | } 432 | ], 433 | "source": [ 434 | "ob.set_foo(99)" 435 | ] 436 | }, 437 | { 438 | "cell_type": "markdown", 439 | "metadata": {}, 440 | "source": [ 441 | "### Labels\n", 442 | "Labels are a feature that makes it possible to infuence the order by\n", 443 | "which event handlers are called, and provide a means to disconnect\n", 444 | "specific (groups of) handlers. The label is part of the connection\n", 445 | "string: 'foo.bar:label'." 446 | ] 447 | }, 448 | { 449 | "cell_type": "code", 450 | "execution_count": 16, 451 | "metadata": {}, 452 | "outputs": [], 453 | "source": [ 454 | "class MyObject(event.Component):\n", 455 | "\n", 456 | " @event.reaction('!foo:bb')\n", 457 | " def foo_handler1(self, *events):\n", 458 | " print('foo B')\n", 459 | "\n", 460 | " @event.reaction('!foo:cc')\n", 461 | " def foo_handler2(self, *events):\n", 462 | " print('foo C')\n", 463 | " \n", 464 | " @event.reaction('!foo:aa')\n", 465 | " def foo_handler3(self, *events):\n", 466 | " print('foo A')\n", 467 | "\n", 468 | "ob = MyObject()" 469 | ] 470 | }, 471 | { 472 | "cell_type": "code", 473 | "execution_count": 17, 474 | "metadata": {}, 475 | "outputs": [ 476 | { 477 | "data": { 478 | "text/plain": [ 479 | "Dict([('type', 'foo'), ('source', )])" 480 | ] 481 | }, 482 | "execution_count": 17, 483 | "metadata": {}, 484 | "output_type": "execute_result" 485 | }, 486 | { 487 | "name": "stdout", 488 | "output_type": "stream", 489 | "text": [ 490 | "foo A\n", 491 | "foo B\n", 492 | "foo C\n" 493 | ] 494 | } 495 | ], 496 | "source": [ 497 | "ob.emit('foo', {})" 498 | ] 499 | }, 500 | { 501 | "cell_type": "code", 502 | "execution_count": 18, 503 | "metadata": {}, 504 | "outputs": [ 505 | { 506 | "data": { 507 | "text/plain": [ 508 | "Dict([('type', 'foo'), ('source', )])" 509 | ] 510 | }, 511 | "execution_count": 18, 512 | "metadata": {}, 513 | "output_type": "execute_result" 514 | }, 515 | { 516 | "name": "stdout", 517 | "output_type": "stream", 518 | "text": [ 519 | "foo A\n", 520 | "foo C\n" 521 | ] 522 | } 523 | ], 524 | "source": [ 525 | "ob.disconnect('foo:bb')\n", 526 | "ob.emit('foo', {})" 527 | ] 528 | }, 529 | { 530 | "cell_type": "markdown", 531 | "metadata": {}, 532 | "source": [ 533 | "### Dynamism\n", 534 | "\n", 535 | "Dynamism is a concept that allows one to connect to events for which the source can change. It essentially allows events to be connected automatically, which greatly reduced boilerplate code. I makes it easy to connect different parts of an application in a robust way." 536 | ] 537 | }, 538 | { 539 | "cell_type": "code", 540 | "execution_count": 19, 541 | "metadata": {}, 542 | "outputs": [ 543 | { 544 | "data": { 545 | "text/plain": [ 546 | "" 547 | ] 548 | }, 549 | "execution_count": 19, 550 | "metadata": {}, 551 | "output_type": "execute_result" 552 | }, 553 | { 554 | "name": "stdout", 555 | "output_type": "stream", 556 | "text": [ 557 | "total count is 6\n" 558 | ] 559 | } 560 | ], 561 | "source": [ 562 | "class Root(event.Component):\n", 563 | "\n", 564 | " children = event.TupleProp([], settable=True)\n", 565 | " \n", 566 | " @event.reaction('children', 'children*.count')\n", 567 | " def update_total_count(self, *events):\n", 568 | " total_count = sum([child.count for child in self.children])\n", 569 | " print('total count is', total_count)\n", 570 | "\n", 571 | "class Sub(event.Component): \n", 572 | " count = event.IntProp(0, settable=True)\n", 573 | "\n", 574 | "root = Root()\n", 575 | "sub1, sub2, sub3 = Sub(count=1), Sub(count=2), Sub(count=3)\n", 576 | "root.set_children([sub1, sub2, sub3])" 577 | ] 578 | }, 579 | { 580 | "cell_type": "markdown", 581 | "metadata": {}, 582 | "source": [ 583 | "Updating the `count` property on any of its children will invoke the callback:" 584 | ] 585 | }, 586 | { 587 | "cell_type": "code", 588 | "execution_count": 20, 589 | "metadata": {}, 590 | "outputs": [ 591 | { 592 | "data": { 593 | "text/plain": [ 594 | "" 595 | ] 596 | }, 597 | "execution_count": 20, 598 | "metadata": {}, 599 | "output_type": "execute_result" 600 | }, 601 | { 602 | "name": "stdout", 603 | "output_type": "stream", 604 | "text": [ 605 | "total count is 105\n" 606 | ] 607 | } 608 | ], 609 | "source": [ 610 | "sub1.set_count(100)" 611 | ] 612 | }, 613 | { 614 | "cell_type": "markdown", 615 | "metadata": {}, 616 | "source": [ 617 | "We also connected to the `children` property, so that the handler is also invoked when the children are added/removed:" 618 | ] 619 | }, 620 | { 621 | "cell_type": "code", 622 | "execution_count": 21, 623 | "metadata": {}, 624 | "outputs": [ 625 | { 626 | "data": { 627 | "text/plain": [ 628 | "" 629 | ] 630 | }, 631 | "execution_count": 21, 632 | "metadata": {}, 633 | "output_type": "execute_result" 634 | }, 635 | { 636 | "name": "stdout", 637 | "output_type": "stream", 638 | "text": [ 639 | "total count is 5\n" 640 | ] 641 | } 642 | ], 643 | "source": [ 644 | "root.set_children([sub2, sub3])" 645 | ] 646 | }, 647 | { 648 | "cell_type": "markdown", 649 | "metadata": {}, 650 | "source": [ 651 | "Naturally, when the count on new children changes ..." 652 | ] 653 | }, 654 | { 655 | "cell_type": "code", 656 | "execution_count": 22, 657 | "metadata": {}, 658 | "outputs": [ 659 | { 660 | "data": { 661 | "text/plain": [ 662 | "" 663 | ] 664 | }, 665 | "execution_count": 22, 666 | "metadata": {}, 667 | "output_type": "execute_result" 668 | }, 669 | { 670 | "name": "stdout", 671 | "output_type": "stream", 672 | "text": [ 673 | "total count is 3\n" 674 | ] 675 | } 676 | ], 677 | "source": [ 678 | "sub4 = Sub()\n", 679 | "root.set_children([sub3, sub4])" 680 | ] 681 | }, 682 | { 683 | "cell_type": "code", 684 | "execution_count": 23, 685 | "metadata": {}, 686 | "outputs": [ 687 | { 688 | "data": { 689 | "text/plain": [ 690 | "" 691 | ] 692 | }, 693 | "execution_count": 23, 694 | "metadata": {}, 695 | "output_type": "execute_result" 696 | }, 697 | { 698 | "name": "stdout", 699 | "output_type": "stream", 700 | "text": [ 701 | "total count is 13\n" 702 | ] 703 | } 704 | ], 705 | "source": [ 706 | "sub4.set_count(10)" 707 | ] 708 | }, 709 | { 710 | "cell_type": "code", 711 | "execution_count": 24, 712 | "metadata": {}, 713 | "outputs": [ 714 | { 715 | "data": { 716 | "text/plain": [ 717 | "" 718 | ] 719 | }, 720 | "execution_count": 24, 721 | "metadata": {}, 722 | "output_type": "execute_result" 723 | } 724 | ], 725 | "source": [ 726 | "sub1.set_count(1000) # no update, sub1 is not part of root's children" 727 | ] 728 | }, 729 | { 730 | "cell_type": "markdown", 731 | "metadata": { 732 | "collapsed": true 733 | }, 734 | "source": [ 735 | "Note that the above example is a great candidate for using an implicit reaction. Try it!" 736 | ] 737 | }, 738 | { 739 | "cell_type": "code", 740 | "execution_count": null, 741 | "metadata": {}, 742 | "outputs": [], 743 | "source": [] 744 | } 745 | ], 746 | "metadata": { 747 | "anaconda-cloud": {}, 748 | "kernelspec": { 749 | "display_name": "Python [default]", 750 | "language": "python", 751 | "name": "python3" 752 | }, 753 | "language_info": { 754 | "codemirror_mode": { 755 | "name": "ipython", 756 | "version": 3 757 | }, 758 | "file_extension": ".py", 759 | "mimetype": "text/x-python", 760 | "name": "python", 761 | "nbconvert_exporter": "python", 762 | "pygments_lexer": "ipython3", 763 | "version": "3.6.1" 764 | } 765 | }, 766 | "nbformat": 4, 767 | "nbformat_minor": 1 768 | } 769 | -------------------------------------------------------------------------------- /flexx_tutorial_pscript.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Tutorial for PScript - Python to JavaScript transpilation" 8 | ] 9 | }, 10 | { 11 | "cell_type": "code", 12 | "execution_count": 1, 13 | "metadata": {}, 14 | "outputs": [], 15 | "source": [ 16 | "from pscript import py2js, evalpy" 17 | ] 18 | }, 19 | { 20 | "cell_type": "markdown", 21 | "metadata": {}, 22 | "source": [ 23 | "We can transpile strings of Python code:" 24 | ] 25 | }, 26 | { 27 | "cell_type": "code", 28 | "execution_count": 2, 29 | "metadata": {}, 30 | "outputs": [ 31 | { 32 | "name": "stdout", 33 | "output_type": "stream", 34 | "text": [ 35 | "var _pyfunc_range = function (start, end, step) {\n", 36 | "var i, res = [];\n", 37 | " var val = start;\n", 38 | " var n = (end - start) / step;\n", 39 | " for (i=0; i" 36 | ] 37 | }, 38 | "execution_count": 2, 39 | "metadata": {}, 40 | "output_type": "execute_result" 41 | } 42 | ], 43 | "source": [ 44 | "launch('http://flexx.rtfd.org', title='Flexx website')" 45 | ] 46 | }, 47 | { 48 | "cell_type": "markdown", 49 | "metadata": {}, 50 | "source": [ 51 | "You can also specify the runtime. When this is a browser, you can (of course) not set the title, size etc." 52 | ] 53 | }, 54 | { 55 | "cell_type": "code", 56 | "execution_count": 3, 57 | "metadata": {}, 58 | "outputs": [ 59 | { 60 | "data": { 61 | "text/plain": [ 62 | "" 63 | ] 64 | }, 65 | "execution_count": 3, 66 | "metadata": {}, 67 | "output_type": "execute_result" 68 | } 69 | ], 70 | "source": [ 71 | "launch('http://flexx.rtfd.org', 'firefox-browser')" 72 | ] 73 | }, 74 | { 75 | "cell_type": "code", 76 | "execution_count": null, 77 | "metadata": {}, 78 | "outputs": [], 79 | "source": [] 80 | } 81 | ], 82 | "metadata": { 83 | "anaconda-cloud": {}, 84 | "kernelspec": { 85 | "display_name": "Python [default]", 86 | "language": "python", 87 | "name": "python3" 88 | }, 89 | "language_info": { 90 | "codemirror_mode": { 91 | "name": "ipython", 92 | "version": 3 93 | }, 94 | "file_extension": ".py", 95 | "mimetype": "text/x-python", 96 | "name": "python", 97 | "nbconvert_exporter": "python", 98 | "pygments_lexer": "ipython3", 99 | "version": "3.6.1" 100 | } 101 | }, 102 | "nbformat": 4, 103 | "nbformat_minor": 1 104 | } 105 | -------------------------------------------------------------------------------- /remove_output.py: -------------------------------------------------------------------------------- 1 | """ 2 | Usage: python remove_output.py notebook.ipynb [ > without_output.ipynb ] 3 | Modified from remove_output by Minrk 4 | """ 5 | import sys 6 | import io 7 | import os 8 | from IPython.nbformat.current import read, write 9 | 10 | 11 | def remove_outputs(nb): 12 | """remove the outputs from a notebook""" 13 | for ws in nb.worksheets: 14 | for cell in ws.cells: 15 | if cell.cell_type == 'code': 16 | cell.outputs = [] 17 | 18 | if __name__ == '__main__': 19 | fname = sys.argv[1] 20 | with io.open(fname, 'r') as f: 21 | nb = read(f, 'json') 22 | remove_outputs(nb) 23 | base, ext = os.path.splitext(fname) 24 | new_ipynb = "%s%s" % (base, ext) # "%s_removed%s" % (base, ext) 25 | with io.open(new_ipynb, 'w', encoding='utf8') as f: 26 | write(nb, f, 'json') 27 | print("wrote %s" % new_ipynb) 28 | --------------------------------------------------------------------------------