Create a new addon to extend the CLI#
While much of the behavior of a JupyterLite application can be configured or otherwise
modified by extensions, this may not be enough for all needs. It is also
possible to extend the underlying jupyter lite
CLI by
means of Addons.
A custom Addon can do anything to the output folder of a built lite application, as well as modify the behavior of other Addons, including the ones that comprise the core API.
Some use cases:
shipping a complex frontend extension
predictably patching files in the built application
linting, testing, compression or other validation and optimization techniques
Note
Addon was chosen to distinguish these pieces from browser-based Plugins and
Extensions for the frontend, and all jupyter lite
core
behavior is implemented as Addons.
CLI Architecture#
Before digging into building an Addon, it’s worth understanding where in the overall structure of the CLI they fit.
In order to download, unpack, and update static files and configurations from a number of sources, the CLI uses a number of layers.
Component |
Example |
Role |
---|---|---|
App |
load config and parse CLI parameters |
|
Manager |
load Addons, run |
|
Addon |
generate task plan, and implement actions |
|
|
collect logical lifecycle tasks |
|
|
fine-grained ordering for tasks |
|
|
set of actions with Task and file dependencies |
|
Action |
|
actually move and update files |
Structure of an Addon#
At its very simplest, an Addon is initialized with a signature like:
class MyAddon:
__all__ = ["status"]
def status(self, maanger):
yield dict(name="hello", actions=[lambda: print("world")])
the
__all__
member list the hooks the Addon implementshooks may also be prefixed with
pre_
andpost_
phase
hook implementations, as advertised
Of note:
The
status
phase should have no side-effectsThe
init
phase is mostly reserved for “gold master” contentThe
build
is mostly reserved for user-authored content
Hint
See the existing examples in this JupyterLite repo for other hook implementations.
Generating Tasks#
Each hook implementation is expected to return an iterable of doit
Tasks, of
the minimal form:
def post_build(manager):
yield dict(
name="a:unique:name", # will have the Addon, and maybe a prefix, prepended
actions=[["things", "to", "do"]],
file_dep=["a-file", Path("another-file")],
targets=["an-output-file"],
)
The App-level tasks already have doit.create_after
configured based on their hook
parent, which means a Task can confidently rely on files from its
parents (by any other addons) already existing.
While not required, having accurate file_dep
and targets
help ensure that the
built application is always in a consistent state, without substantial rework.
BaseAddon
#
A convenience class, jupyterlite_core.addons.base.BaseAddon
may be
extended to provide a number of useful features. It extends
traitlets.LoggingConfigurable
, and makes the LiteManager
the parent
of the
Addon, allowing it to be configured by name via jupyter_lite_config.json
:
{
"LiteBuildConfig": {
"ignore_sys_prefix": true
},
"MyAddon": {
"enable_some_feature": true
}
}
… or via the command line.
jupyter lite build --MyAddon.enable_some_feature=True
Short CLI#
An Addon which inherits from BaseAddon
(or traitlets.Configurable
some other way)
can tell the parent application that it exposes additional CLI aliases and
flags, both for execution, and when queried with --help
.
Hint
Addons authors are encouraged to group their aliases and flags by using a common prefix.
Aliases#
An alias maps a CLI argument to a single trait.
from traitlets import Int
class MyFooAddon(BaseAddon):
__all__ = ["status"]
aliases = {
"how-many-foos": "MyFooAddon.foo",
}
foo = Int(0, help="The number of foos").tag(config=True)
# ...
Warning
Addons may not overload core aliases, or the aliases of previously-loaded addons.
Flags#
A flag maps a CLI argument to any number of traits on any number of
traitlets.Configurable
classes:
from traitlets import Int, Bool
class MyFooBarAddon(BaseAddon):
__all__ = ["status"]
flags = {
"foo-bar": (
{"MyFooBarAddon": {"foo": 1, "bar": True}},
"Foo once, and bar",
)
}
foo = Int(0, help="The number of foos").tag(config=True)
bar = Bar(False, help="Whether to bar").tag(config=True)
# ...
Note
Addons may augment the behavior of existing flags, but not override previously-registered configuration values. Help text will be appended with a newline.
Packaging#
Addons are advertised via entry_points
e.g. in pyproject.toml
:
[project.entry-points."jupyterlite.addon.v0"]
my-unique-addon = "my_module:MyAddon"
General Guidance#
it’s worth looking at how what
BaseAddon
and its subclasses handle certain taskskeeping reproducibility in mind, cache liberally, and make use of
file_deps
,targets
, anduptodate
to keep builds snappy