Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extra non-critical revisions for the dual compatibility document #15866

Draft
wants to merge 59 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
4284060
First pass formatted compatibility document.
ericsnekbytes Jul 17, 2023
8c4ac12
Added interlinks.
ericsnekbytes Jul 17, 2023
918084f
Automatic application of license header
github-actions[bot] Jul 17, 2023
d04af80
Change link formatting
ericsnekbytes Jul 17, 2023
2d746e9
Merge branch 'dual_compat_extensions' of github.com:ericsnekbytes/jup…
ericsnekbytes Jul 17, 2023
0f79f21
Change link formatting.
ericsnekbytes Jul 18, 2023
7f907f1
Update docs/source/extension/extension_dual_compatibility.rst
ericsnekbytes Jul 18, 2023
c77238d
Changed Lab to JupyterLab
ericsnekbytes Jul 18, 2023
3183d32
Mofified main note.
ericsnekbytes Jul 18, 2023
737ab7a
Note for future frontends.
ericsnekbytes Jul 18, 2023
f42ce9e
Begin refactor.
ericsnekbytes Jul 26, 2023
8dceeae
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jul 26, 2023
c5c624a
Continue document refactor.
ericsnekbytes Jul 26, 2023
9028478
Merge branch 'dual_compat_extensions' of github.com:ericsnekbytes/jup…
ericsnekbytes Jul 26, 2023
0c109b6
Minor updates, formatting and some cleanup.
ericsnekbytes Jul 26, 2023
22863e5
Updated provider/consumer section and started a linked doc.
ericsnekbytes Aug 2, 2023
6203bd5
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Aug 2, 2023
78651bb
Automatic application of license header
github-actions[bot] Aug 2, 2023
62ae34c
Added more content.
ericsnekbytes Aug 3, 2023
4eb9dd7
Merge branch 'dual_compat_extensions' of github.com:ericsnekbytes/jup…
ericsnekbytes Aug 4, 2023
586f82d
Added content.
ericsnekbytes Aug 4, 2023
3f9e241
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Aug 4, 2023
e87ff22
Cleanup and formatting.
ericsnekbytes Aug 4, 2023
8704608
Finished draft of the remaining content.
ericsnekbytes Aug 4, 2023
7378ad1
Merge branch 'dual_compat_extensions' of github.com:ericsnekbytes/jup…
ericsnekbytes Aug 4, 2023
4f4b7e6
Began linked plugin system doc.
ericsnekbytes Aug 4, 2023
e6d1343
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Aug 4, 2023
4eeb752
Renamed plugin system doc.
ericsnekbytes Aug 4, 2023
098bcbc
Added content/code, formatting.
ericsnekbytes Aug 9, 2023
e4edd5f
Revised plugin system doc (WIP).
ericsnekbytes Aug 9, 2023
2097903
Updated (WIP) plugin system doc.
ericsnekbytes Aug 9, 2023
879afac
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Aug 9, 2023
c0fea09
Added content to plugin_system design doc.
ericsnekbytes Aug 17, 2023
9a9358f
Merge branch 'dual_compat_extensions' of github.com:ericsnekbytes/jup…
ericsnekbytes Aug 17, 2023
ab4759f
Added content to plugin_system.
ericsnekbytes Aug 17, 2023
55adb2c
Modified links and code references/related content.
ericsnekbytes Sep 27, 2023
708db91
Update links.
ericsnekbytes Sep 27, 2023
6e8fb0b
Added token link.
ericsnekbytes Sep 27, 2023
15c3e29
Update docs.
ericsnekbytes Sep 28, 2023
e42100e
Update links to production locations.
ericsnekbytes Sep 28, 2023
36aeb8c
Updated provider section.
ericsnekbytes Sep 28, 2023
df0abfb
Expanded provider explanation.
ericsnekbytes Oct 2, 2023
67a6f8c
Updated example code snippets.
ericsnekbytes Oct 4, 2023
a28a79b
Updated comments.
ericsnekbytes Oct 4, 2023
c56e097
Removed/split the plugin_system doc to a new PR.
ericsnekbytes Nov 15, 2023
2710d1c
Added explicit reference/linking for basic provider/consumer info.
ericsnekbytes Nov 20, 2023
6a6e684
Added compatibility guide to toctree.
ericsnekbytes Nov 20, 2023
08418da
Title renaming.
ericsnekbytes Nov 20, 2023
d7005fe
Removed parens.
ericsnekbytes Nov 20, 2023
cc3ab16
Changed titles, minor formatting.
ericsnekbytes Nov 21, 2023
54dc51e
Remove the new plugin system doc
fcollonval Dec 9, 2023
2db0aed
Improve wording
fcollonval Dec 9, 2023
35390cc
Dependency injection refactors, formatting and minor editing.
ericsnekbytes Jan 16, 2024
300ccf6
Remaining edits.
ericsnekbytes Jan 16, 2024
6273986
Edited section headings for better visibility.
ericsnekbytes Jan 16, 2024
d140558
Update docs/source/extension/extension_dev.rst
ericsnekbytes Jan 23, 2024
ef8f5ed
Update docs/source/extension/extension_multiple_ui.rst
ericsnekbytes Jan 23, 2024
6a49f78
Update docs/source/extension/extension_multiple_ui.rst
ericsnekbytes Jan 23, 2024
4ce86cf
Changed title.
ericsnekbytes Feb 23, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
11 changes: 9 additions & 2 deletions docs/source/extension/extension_dev.rst
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ See the sections below for more detailed information, or browse the rest of this
internationalization
identity
extension_tutorial
extension_multiple_ui
extension_migration

Other resources
Expand All @@ -42,9 +43,11 @@ Before we get started, here are some resources for hands-on practice or more in-
Tutorials
^^^^^^^^^

We provide a set of guides to get started writing extensions for JupyterLab:
Learn how to write JupyterLab extensions with these guides:

- :ref:`extension_tutorial`: A tutorial to learn how to make a simple JupyterLab extension.
- :ref:`Making Extensions Compatible with Multiple Applications Tutorial <extension_dual_compatibility>`
A tutorial for making extensions that work in both JupyterLab, Jupyter Notebook 7+ and more
- The `JupyterLab Extension Examples Repository <https://github.com/jupyterlab/extension-examples>`_: A short tutorial series to learn how to develop extensions for JupyterLab by example.
- :ref:`developer-extension-points`: A list of the most common JupyterLab extension points.
- Another common pattern for extending JupyterLab document widgets with application plugins is covered in :ref:`documents`.
Expand Down Expand Up @@ -132,14 +135,18 @@ A Jupyter front-end application object is given to a plugin's ``activate`` funct

See the JupyterLab API reference documentation for the ``JupyterFrontEnd`` class for more details.

.. _dependency-injection-basic-info:

.. _services:

Plugins Interacting with Each Other
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

*(The Provider-Consumer pattern is explained here)*

One of the foundational features of the JupyterLab plugin system is that application plugins can interact with other plugins by providing a service to the system and requiring services provided by other plugins. A service can be any JavaScript value, and typically is a JavaScript object with methods and data attributes. For example, the core plugin that supplies the JupyterLab main menu provides a :ref:`mainmenu` service object to the system with a method to add a new top-level menu and attributes to interact with existing top-level application menus.

In the following discussion, the plugin that is providing a service to the system is the *provider* plugin, and the plugin that is requiring and using the service is the *consumer* plugin.
In the following discussion, the plugin that is providing a service to the system is the *provider* plugin, and the plugin that is requiring and using the service is the *consumer* plugin. Note that these kinds of *provider* and *consumer* plugins are fundamental parts of JupyterLab's Provider-Consumer pattern (which is a type of `dependency-injection <https://en.wikipedia.org/wiki/Dependency_injection>`_ pattern).

.. _tokens:

Expand Down
277 changes: 277 additions & 0 deletions docs/source/extension/extension_multiple_ui.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,277 @@
.. Copyright (c) Jupyter Development Team.
.. Distributed under the terms of the Modified BSD License.

.. _multiple_ui_extensions:

Targeting Multiple Applications
===============================

*Make your extensions work in JupyterLab, Notebook 7+ and more!*

Jupyter Notebook 7 is built with components from JupyterLab, and since
both use the same building blocks, that means your extension can work
on both (or any other frontend built with JupyterLab components) with
little or no modification depending on its design.

This guide will give you an overview of compatibility features, then a
tutorial and reference code covering some of the topics mentioned here.
If you don't know how to make extensions, you can read more about the
basics at :ref:`the Extension Tutorial <extension_tutorial>` or the
:ref:`the Extensions page <extension_dev>`.

How Compatibility Works
-----------------------

At a high level, extensions for JupyterLab and Jupyter Notebook both
typically start from a template project, which you can download and set up
using instructions from `the extension tutorial <https://jupyterlab.readthedocs.io/en/latest/extension/extension_tutorial.html>`_).
Once your template is ready, you can start adding components and features to build your extension.

An extension for JupyterLab (and for Notebook 7) is made up of a `series <https://jupyterlab.readthedocs.io/en/latest/extension/extension_dev.html>`_
of bundled `plugins <https://lumino.readthedocs.io/en/latest/api/interfaces/application.IPlugin.html#requires>`_,
and those plugins typically use components from the interface toolkit `Lumino <https://lumino.readthedocs.io/en/latest/api/index.html>`_
as well as the `JupyterLab API <https://jupyterlab.readthedocs.io/en/latest/api/index.html>`_
(among others) to help build your extension's look and behavior. Both
Lumino and JupyterLab API are written in Typescript.

This is how basic compatibility features work: both apps use the same building
blocks and methods. For instance, both JupyterLab and Notebook 7 accept Lumino widgets
as interface components, both apps allow you to add them to the interface by
specifying an "area" to place them into, and extensions for both use the same
basic ``JupyterFrontendPlugin`` class.

How to Achieve Compatibility
----------------------------

Compatibility can be achieved with literally no effort in some simple
cases. But for more complex cases, where the extension uses features in
one app that are not available in other apps, you will need to decide
how to handle those features in the other apps.

The technical solutions to compatibility basically offer ways to disable
features for some apps, or to check for the presence of a particular app
or feature and then adjust behaviors accordingly.

There are also design patterns and approaches that will make it easier to
achieve compatibility in your extensions.

You can read more about those below. Here are some general tips for making
your extensions compatible:

- Use common features where possible, these offer compatibility without
any extra effort
- Avoid using app-specific features (like the JupyterLab status bar) without
first checking if they are available either when your extension loads, or
while it's running (more on that further down)
- Try to separate app-specific features from common features that are
supported by both apps
- Decide what approach to take with any extension features that rely on
app-specific capabilities (like the JupyterLab status bar). You can disable
or modify those features in other apps using the techniques listed further
down in this document.

Using Only Common Features
^^^^^^^^^^^^^^^^^^^^^^^^^^

If your extension only uses features that both JupyterLab and Notebook 7
have, you can simply install it and it will seamlessly work in both JupyterLab
and Notebook 7.

An extension that adds a single self-contained text widget to the top bar
of the UI, for instance, doesn't need to do anything at all to be compatible
with both JupyterLab and Notebook 7 (both apps have a top area that can hold the
widget, so it will be visible in both JupyterLab and Notebook 7 upon install and
after launch).

See `this example video <https://www.youtube.com/watch?v=mqotG1MkHa4>`_ of a
compatible top-bar-text-widget that works in both JupyterLab and Notebook 7
out of the box, and `read the full extension example code here <https://github.com/jupyterlab/extension-examples/tree/main/toparea-text-widget>`_.

Note that using features that are not common to both JupyterLab and Notebook 7 (or
other apps) will break compatibility in apps where that feature is not available
unless special steps are taken (more on these further down). The status bar in
JupyterLab is an example of a feature that is available in JupyterLab but not in
Notebook 7.

Using Plugin Metadata to Drive Compatibility
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

JupyterLab's extension system is designed so that plugins can depend on and
reuse features from one another. A key part of this approach is :ref:`JupyterLab's
Provider-Consumer pattern <dependency-injection-basic-info>` (a type of `dependency-injection <https://en.wikipedia.org/wiki/Dependency_injection>`_
pattern), and it is what enables the compatibility solutions discussed here.

Each plugin uses some properties (the ``requires`` and ``optional`` properties) to
request features it wants which are provided by other plugins that have been
loaded into JupyterLab. When your plugin requests features, the system sends
them to your plugin's activate function if they are available.

You can build compatible extensions by taking advantage of these plugin
properties, and how the plugin system uses them:

- When you designate a feature in the ``requires`` list of your
plugin, JupyterLab will only load your plugin if that feature is available.
- By designating a feature in the ``optional`` list, JupyterLab will pass you
an object for it (if it's available) or ``null`` if it's not.

So, these capabilities form the backbone of extension compatibility. You can
use them to make checks in your extensions that will allow them to function in
both JupyterLab and Jupyter Notebook 7 (and others).

JupyterLab itself is a :ref:`provider <dependency-injection-basic-info>` of many features through its built-in plugins,
which you can read more about in the `Common Extension Points document <https://jupyterlab.readthedocs.io/en/latest/extension/extension_points.html>`_.
It's a good idea to use these extension points while you're building your extensions (and
by doing so you are acting as the *consumer* in JupyterLab's :ref:`Provider-Consumer pattern <dependency-injection-basic-info>`.

Testing for Optional Features
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Making an app-specific feature optional and checking if it is available before
using it, is one technique you can use to make your extensions compatible.

Take a look at a snippet from `this example extension <https://github.com/jupyterlab/extension-examples/tree/main/shout-button-message>`_
in the examples repo (you can read the full extension example code there):

..
TODO: use a pointer/reference to the code with the docs toolkit

.. code:: TypeScript

const plugin: JupyterFrontEndPlugin<void> = {
id: '@jupyterlab-examples/shout-button:plugin',
description:
'An extension that adds a button and message to the right toolbar, with optional status bar widget in JupyterLab.',
autoStart: true,
// The IStatusBar is marked optional here. If it's available, it will
// be provided to the plugin as an argument to the activate function
// (shown below), and if not it will be null.
optional: [IStatusBar],
// Make sure to list any 'requires' and 'optional' features as arguments
// to your activate function (activate is always passed an Application,
// then required arguments, then optional arguments)
activate: (app: JupyterFrontEnd, statusBar: IStatusBar | null) => {
// ... Extension code ...
}
};

This plugin marks ``IStatusBar`` as optional, and adds an argument for it to the
plugin's ``activate`` function (which will be called by JupyterLab when the extension
loads). If ``IStatusBar`` is not available, the second argument to the ``activate``
function will be ``null``, as is the case when the extension is loaded in Jupyter
Notebook 7.

The extension always creates a common main widget, but when it comes time to use the
status bar, the extension first checks if the ``IStatusBar`` is available, and only then
proceeds to create a status bar item. This allows the extension to run successfully in both
JupyterLab and Jupyter Notebook 7:

.. code:: TypeScript

// Create a ShoutWidget and add it to the interface in the right sidebar
const shoutWidget: ShoutWidget = new ShoutWidget();
shoutWidget.id = 'JupyterShoutWidget'; // Widgets need an id

app.shell.add(shoutWidget, 'right');

// Check if the status bar is available, and if so, make
// a status bar widget to hold some information
if (statusBar) {
const statusBarWidget = new ShoutStatusBarSummary();

statusBar.registerStatusItem('shoutStatusBarSummary', {
item: statusBarWidget
});

// Connect to the messageShouted to be notified when a new message
// is published and react to it by updating the status bar widget.
shoutWidget.messageShouted.connect((widget: ShoutWidget, time: Date) => {
statusBarWidget.setSummary(
'Last Shout: ' + widget.lastShoutTime?.toString() ?? '(None)'
);
});
}

Using Required Features to Switch Behaviors
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Another pattern you can follow is to export a list of plugins from your
extension, then use different "requires" features to select different
behaviors based on which app the extension is currently running in.

Here is a snippet from `this sample extension <https://github.com/jupyterlab/extension-examples/tree/main/clap-button-message>`_
which adds a *clap* button to the top area in JupyterLab, or to the
right sidebar in Jupyter Notebook 7 (you can read the full extension
example code there):

.. code:: TypeScript

/**
* Initialization data for the @jupyterlab-examples/clap-button JupyterLab extension.
*/
const pluginJupyterLab: JupyterFrontEndPlugin<void> = {
id: '@jupyterlab-examples/clap-button:pluginLab',
description: 'Adds a clap button to the top area JupyterLab',
autoStart: true,
requires: [ILabShell],
activate: (app: JupyterFrontEnd, labShell: ILabShell) => {
console.log(
'JupyterLab extension @jupyterlab-examples/clap-button is activated!'
);

// Create a ClapWidget and add it to the interface in the top area
const clapWidget = new ClapWidget();
clapWidget.id = 'JupyterLabClapWidgetLab'; // Widgets need an id
app.shell.add(clapWidget, 'top');
}
};

/**
* Initialization data for the @jupyterlab-examples/clap-button Jupyter Notebook extension.
*/
const pluginJupyterNotebook: JupyterFrontEndPlugin<void> = {
id: '@jupyterlab-examples/clap-button:pluginNotebook',
description: 'Adds a clap button to the right sidebar of Jupyter Notebook 7',
autoStart: true,
requires: [INotebookShell],
activate: (app: JupyterFrontEnd, notebookShell: INotebookShell) => {
console.log(
'Jupyter Notebook extension @jupyterlab-examples/clap-button is activated!'
);

// Create a ClapWidget and add it to the interface in the right area
const clapWidget = new ClapWidget();
clapWidget.id = 'JupyterNotebookClapWidgetNotebook'; // Widgets need an id
app.shell.add(clapWidget, 'right');
}
};

/**
* Gather all plugins defined by this extension
*/
const plugins: JupyterFrontEndPlugin<void>[] = [
pluginJupyterLab,
pluginJupyterNotebook
];

export default plugins;

As you can see above, this extension exports multiple plugins in a list,
and each plugin uses different *requires* features to switch between
different behaviors (in this case, different layout areas) depending on
the app it is being loaded into. The first plugin requires ``ILabShell``
(available in JupyterLab), and the second plugin requires ``INotebookShell``
(available in Jupyter Notebook 7).

This approach (testing the shell at plugin load time) is not the preferred
method for making compatible extensions since it is less granular, less
universal (as the shell is specific to a given app generally) and offers
only very broad behavior switching, though it can be used to make specialized
features that target one particular app in your extensions. In general, you
should prefer the "Testing for Optional Features" approach and target the
"Common Extension Points" mentioned above.

Further Reading
---------------

For an explanation of JupyterLab's plugin system and JupyterLab's Provider-Consumer pattern (a type of `dependency-injection <https://en.wikipedia.org/wiki/Dependency_injection>`_
pattern), read the :ref:`Extension Development document <dependency-injection-basic-info>`.