A reusable Jupyter Widget
Project description
anywidget
simple, custom jupyter widgets that "just work"
- create widgets without complicated cookiecutter templates
- publish to PyPI like any other Python package
- prototype within
.ipynb
or.py
files - run in Jupyter, JupyterLab, Google Colab, VSCode, and more
- develop (optionally) with Vite for instant HMR
Installation
pip install anywidget
Usage
import anywidget
import traitlets
CSS = """
.counter-btn {
background-image: linear-gradient(to right, #a1c4fd, #c2e9fb);
border: 0;
border-radius: 10px;
padding: 10px 50px;
}
"""
ESM = """
export function render(view) {
let counter = Object.assign(document.createElement("button"), {
className: "counter-btn",
innerHTML: `count is ${view.model.get("count")}`,
onclick: () => {
view.model.set("count", view.model.get("count") + 1);
view.model.save_changes();
},
});
view.model.on("change:count", () => {
counter.innerHTML = `count is ${view.model.get("count")}`;
});
view.el.appendChild(counter);
}
"""
class CounterWidget(anywidget.AnyWidget):
_esm = ESM # required, must export `render`
_css = CSS # optional
count = traitlets.Int(0).tag(sync=True)
CounterWidget()
Why
anywidget simplifies the creation of custom Jupyter widgets – no complicated build steps or bundling required.
Official cookiecutter templates provide the defacto approach for creating custom Jupyter widgets, but derived projects are bootstrapped with complicated packaging and distribution scripts which must be maintained by the widget author. While the cookiecutters initially ensure compatability with various notebook or notebook-like environments, substantial developer effort is required to keep the vendored tooling from breaking over time.
anywidget reduces this burden and improves the Jupyter widget developer experience. It ensures your widget's compatability with the fractured Jupyter ecosystem rather than requiring each author to solve this same multi-platform packaging problem. Creating custom widgets with anywidget is fun and easy. You can start prototyping within a notebook and publish on PyPI like any other Python module. No need to create a new cookiecutter repo, maintain complicated build scripts, or understand JavaScript dependency/build tooling to get started.
How
Widgets are defined by combining an
ECMAScript Module with a Python class which
derives from anywidget.AnyWidget
. The provided ECMAScript Module must
include a named export called render
to render the corresponding view. You can
add and sync new properties by adding traitlets
in your derived Python classes
and subscribing and publishing changes via view.model
in the client render
function.
Note Your JS code must be vaid ESM. You can import dependencies from a CDN (e.g.,
import * as d3 from "https://esm.sh/d3"
) or use a bundler targeting ESM.
Advanced
Bundling
Often the ESM required to connect a JavaScript library to Python with
anywidget is minimal and can easily be inlined within the Python module as
a string. This feature allows Python developers to be productive with
anywidget without requiring intimite knowledge or the overhead of frontend
tooling (e.g., npm
/yarn
/pnpm
, Webpack/Vite/esbuild).
As anywidget projects mature, however, it is recommended to organize the JavaScript source into separate files which can be merged together into a single optimized ESM file. This merging process is called bundling and is required by popular fontend frameworks (e.g., React, Vue, Solid, Svelte) which use unsupported browser syntax.
If using a bundler, make sure to load the final bundled assets in your widget and not the original/untransformed JavaScript source files.
Example (esbuild)
Setup
The following project structure contains a python package (hello_widget
) with
separate JS/CSS source code under src/
:
hello_widget/
├── pyproject.toml
├── hello_widget/
│ └── __init__.py
└── src/
├── index.js
└── styles.css
Build
We can bundle these assets into hello_widget/static
with
esbuild without any configuration
(or install):
npx esbuild --bundle --format=esm --outdir=hello_widget/static src/index.js
#
# hello_widget/static/index.js 150b
# hello_widget/static/index.css 81b
#
# ⚡ Done in 2ms
Make sure the final bundled assets are loaded by the Python module:
# hello_widget/__init__.py
import pathlib
import anywidget
import traitlets
# bundler yields hello_widget/static/{index.js,styles.css}
bundler_output_dir = pathlib.Path(__file__).parent / "static"
class HelloWidget(anywidget.AnyWidget):
_esm = (bundler_output_dir / "index.js").read()
_css = (bundler_output_dir / "styles.css").read()
name = traitlets.Unicode().tag(sync=True)
Note
esbuild
is the most simple option for bundling, but it must be executed manually each time changes are made to the JS or CSS source files in order to see changes in Python. See the Vite example for a better developer experience.
Example (Vite, recommended)
We recommend using Vite while developing your widget at this stage. It is simple to configure, and our custom plugin allows for best-in-class developer experience.
Setup
From the root of the project structure above.
npm install -D vite
Create a vite.config.js
file with the following configuration:
// vite.config.js
import { defineConfig } from "vite";
export default defineConfig({
build: {
outDir: "hello_widget/static",
lib: {
entry: ["src/index.js"],
formats: ["es"],
},
},
});
Your project structure should now look like:
hello_widget/
├── pyproject.toml
├── hello_widget/
│ └── __init__.py
├── node_modules/
├── package-lock.json
├── package.json
├── src/
│ ├── index.js
│ └── styles.css
└── vite.config.js
Build
We can now bundle the assets into hellow_widget/static
with Vite, just like
esbuild. Again, make sure the final bundled assets are loaded by the Python
module.
npx vite build
# vite v4.0.4 building for production...
# ✓ 2 modules transformed.
# hello_widget/static/style.css 0.05 kB │ gzip: 0.06 kB
# hello_widget/static/index.mjs 0.13 kB │ gzip: 0.14 kB
So far, this example demonstrates Vite's bundling capabilities which are similar to esbuild. However, Vite really shines during development with its second major feature: a modern development server that enables extremely fast Hot Module Replacement (HMR).
Dev
Frontend development tools have evolved rapidly in the last few years to enable instant, precise updates to client code without reloading the page or blowing away application state. Unfortunately, the cookiecutter templates for custom Jupyter Widgets have not caught up with the times and still require full page loads and re-bundles to see new changes.
This all changes with anywidget
The Vite plugin for anywidget extends its dev server with precise HMR support for Jupyter Widgets. During development, changes made to your widget view are instantly reflected in all active output cells without a full page refresh. Model state is additionally preserved so you do not need to re-run the Python cell to setup state.
To get started with HMR for your widget, install the anywidget
Plugin and add
the following to your vite.config.js
:
npm install -D anywidget
// vite.config.js
import { defineConfig } from "vite";
+ import anywidget from "anywidget/vite";
export default defineConfig({
build: {
outDir: "hello_widget/static",
lib: {
entry: ["src/index.js"],
formats: ["es"],
},
},
+ plugin: [anywidget()],
});
Start the development server from the root of your project:
npx vite
#
# VITE v4.0.4 ready in 321 ms
#
# ➜ Local: http://localhost:5173/
# ➜ Network: use --host to expose
# ➜ press h to show help
#
Finally, link your Python widget to the dev server during development.
# hello_widget/__init__.py
import pathlib
import anywidget
import traitlets
_DEV = True # switch to False for production
if _DEV:
# from `npx vite`
ESM = "http://localhost:5173/src/index.js?anywidget"
CSS = ""
else:
# from `npx vite build`
bundled_assets_dir = pathlib.Path(__file__).parent / "static"
ESM = (bundled_assets_dir / "index.mjs").read()
CSS = (bundled_assets_dir / "styles.css").read()
class HelloWidget(anywidget.AnyWidget):
_esm = ESM
_css = CSS
name = traitlets.Unicode().tag(sync=True)
Any changes to src/*
will now be updated immediate in active output cells with
for this widget. Happy coding!
Development
pip install -e .
If you are using the classic Jupyter Notebook you need to install the nbextension:
jupyter nbextension install --py --symlink --sys-prefix anywidget
jupyter nbextension enable --py --sys-prefix anywidget
Note for developers:
- the
-e
pip option allows one to modify the Python code in-place. Restart the kernel in order to see the changes. - the
--symlink
argument on Linux or OS X allows one to modify the JavaScript code in-place. This feature is not available with Windows.
For developing with JupyterLab:
jupyter labextension develop --overwrite anywidget
Release
npm version [major|minor|patch]
git tag -a vX.X.X -m "vX.X.X"
git push --follow-tags
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
File details
Details for the file anywidget-0.0.4.tar.gz
.
File metadata
- Download URL: anywidget-0.0.4.tar.gz
- Upload date:
- Size: 17.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/4.0.2 CPython/3.11.1
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | 9463474e5054b184df579e59d812eeb712acda469633c4eaf468834796420235 |
|
MD5 | 1349d76106ce576d92c9e43a825654c6 |
|
BLAKE2b-256 | 1a3eb881c3dd56beb276c702943450f8974211a19cc4419b7815a7d70b5cf09b |
File details
Details for the file anywidget-0.0.4-py3-none-any.whl
.
File metadata
- Download URL: anywidget-0.0.4-py3-none-any.whl
- Upload date:
- Size: 24.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/4.0.2 CPython/3.11.1
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | 982747374fa4b9578654c28831ddde1ed5b5ff5ca60b3e0e0bbb3fb5f6d329ba |
|
MD5 | 84e24064396d2fe96b8b70e6391c36aa |
|
BLAKE2b-256 | 7d376abd48ad849bf321dce644d394a0e85d8051daf5bb6f0c911c77a6a12742 |