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

Add support for widgets in JupyterLab code consoles #3004

Merged
merged 3 commits into from
May 24, 2024

Conversation

jtpio
Copy link
Member

@jtpio jtpio commented Oct 30, 2020

Fixes #2370

lab-code-consoles-widgets

This mimics the same approach as for the notebooks for now, but it might be possible to have both the consoles and notebooks share more logic.

@jasongrout
Copy link
Member

Awesome!

Just curious, if a notebook and console share a kernel, do the widgets stay in sync?

@jtpio
Copy link
Member Author

jtpio commented Oct 30, 2020

Just curious, if a notebook and console share a kernel, do the widgets stay in sync?

Not for now:

lab-code-consoles-widgets-share

@jtpio
Copy link
Member Author

jtpio commented Nov 2, 2020

It should be possible to make it work by changing the way widget managers are stored here:

let wManager = Private.widgetManagerProperty.get(context);

export const widgetManagerProperty = new AttachedProperty<
DocumentRegistry.Context,
WidgetManager | undefined
>({
name: 'widgetManager',
create: (owner: DocumentRegistry.Context): undefined => undefined
});
}

So that instead of a DocumentRegistry.Context it would be something specific to a kernel (or something common to both a notebook and a code console).

@jtpio
Copy link
Member Author

jtpio commented Nov 2, 2020

It should be possible to make it work by changing the way widget managers are stored here:

Trying that out locally. for example by using the session context path (shared between the notebook and the console):

jlab-manager-shared-manager

@jasongrout
Copy link
Member

Nice! And it still works between two views of a notebook, or two views of an output?

@jtpio
Copy link
Member Author

jtpio commented Nov 2, 2020

In that case yes, as all the views share the same WidgetManager instance:

jlab-manager-sync-views

for example by using the session context path

But using the path is a bit too brittle. Maybe there should be a SessionContextManager or similar that could be leveraged for both the consoles and notebooks main area widgets. Or checking whether a manager already exists for a notebook when creating a new console.

@jasongrout
Copy link
Member

But using the path is a bit too brittle

Agreed. I haven't checked, but is there any object that the two KernelConnections share? Some reference to the underlying kernel representation, perhaps?

@jasongrout
Copy link
Member

Or checking whether a manager already exists for a notebook when creating a new console.

I'd rather avoid having the console know too much about the notebook, and vice versa, as hard-coding these checks doesn't scale to other extensions as well.

@tacaswell
Copy link

🥜 gallery here: I am very excited about the PR and wanted exactly this with ipympl figures today.

@caenrigen
Copy link

Great PR, looking forward to this feature as well!! Kind regards

@jtpio
Copy link
Member Author

jtpio commented Mar 15, 2021

Rebased so this branch can be tested on Binder:

Binder

image

@jtpio
Copy link
Member Author

jtpio commented Jul 8, 2021

Just rebased one onto the latest main.

Trying that out locally. for example by using the session context path (shared between the notebook and the console):

Here is a diff to use the session context path:

diff --git a/jupyterlab_widgets/package.json b/jupyterlab_widgets/package.json
index 9f22f8cb..e4095418 100644
--- a/jupyterlab_widgets/package.json
+++ b/jupyterlab_widgets/package.json
@@ -68,7 +68,6 @@
     "@lumino/coreutils": "^1.3.0",
     "@lumino/disposable": "^1.1.1",
     "@lumino/messaging": "^1.2.1",
-    "@lumino/properties": "^1.1.0",
     "@lumino/signaling": "^1.2.0",
     "@lumino/widgets": "^1.3.0",
     "@types/backbone": "1.4.1",
diff --git a/jupyterlab_widgets/src/plugin.ts b/jupyterlab_widgets/src/plugin.ts
index 10fafc8d..fe5bb93b 100644
--- a/jupyterlab_widgets/src/plugin.ts
+++ b/jupyterlab_widgets/src/plugin.ts
@@ -33,8 +33,6 @@ import { toArray, filter } from '@lumino/algorithm';

 import { DisposableDelegate } from '@lumino/disposable';

-import { AttachedProperty } from '@lumino/properties';
-
 import { WidgetRenderer } from './renderer';

 import {
@@ -89,8 +87,7 @@ function* consoleWidgetRenderers(
 ): Generator<WidgetRenderer, void, unknown> {
   for (const cell of toArray(console.cells)) {
     if (cell.model.type === 'code') {
-      for (const codecell of ((cell as unknown) as CodeCell).outputArea
-        .widgets) {
+      for (const codecell of (cell as unknown as CodeCell).outputArea.widgets) {
         for (const output of toArray(codecell.children())) {
           if (output instanceof WidgetRenderer) {
             yield output;
@@ -136,11 +133,11 @@ export function registerWidgetManager(
   rendermime: IRenderMimeRegistry,
   renderers: IterableIterator<WidgetRenderer>
 ): DisposableDelegate {
-  let wManager = Private.widgetManagerProperty.get(context);
+  let wManager = Private.widgetManagerProperty.get(context.sessionContext.path);
   if (!wManager) {
     wManager = new WidgetManager(context, rendermime, SETTINGS);
     WIDGET_REGISTRY.forEach((data) => wManager!.register(data));
-    Private.widgetManagerProperty.set(context, wManager);
+    Private.widgetManagerProperty.set(context.sessionContext.path, wManager);
   }

   for (const r of renderers) {
@@ -172,14 +169,14 @@ export function registerConsoleWidgetManager(
   rendermime: IRenderMimeRegistry,
   renderers: IterableIterator<WidgetRenderer>
 ): DisposableDelegate {
-  let wManager = Private.widgetManagerProperty.get(console);
+  let wManager = Private.widgetManagerProperty.get(console.sessionContext.path);
   if (!wManager) {
     wManager = new KernelWidgetManager(
       console.sessionContext.session?.kernel!,
       rendermime
     );
     WIDGET_REGISTRY.forEach((data) => wManager!.register(data));
-    Private.widgetManagerProperty.set(console, wManager);
+    Private.widgetManagerProperty.set(console.sessionContext.path, wManager);
   }

   for (const r of renderers) {
@@ -249,7 +246,9 @@ function activateWidgetExtension(
       return;
     }

-    const wManager = Private.widgetManagerProperty.get(nb.context);
+    const wManager = Private.widgetManagerProperty.get(
+      nb.context.sessionContext.path
+    );
     if (wManager) {
       wManager.onUnhandledIOPubMessage.connect(
         (
@@ -414,12 +413,8 @@ namespace Private {
   /**
    * A private attached property for a widget manager.
    */
-  export const widgetManagerProperty = new AttachedProperty<
-    DocumentRegistry.Context | CodeConsole,
+  export const widgetManagerProperty = new Map<
+    string,
     WidgetManager | KernelWidgetManager | undefined
-  >({
-    name: 'widgetManager',
-    create: (owner: DocumentRegistry.Context | CodeConsole): undefined =>
-      undefined,
-  });
+  >();
 }
diff --git a/yarn.lock b/yarn.lock
index bae0d729..49a69346 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1551,7 +1551,7 @@
     "@lumino/disposable" "^1.7.0"
     "@lumino/signaling" "^1.7.0"

-"@lumino/properties@^1.1.0", "@lumino/properties@^1.2.3", "@lumino/properties@^1.5.0":
+"@lumino/properties@^1.2.3", "@lumino/properties@^1.5.0":
   version "1.5.0"
   resolved "https://registry.npmjs.org/@lumino/properties/-/properties-1.5.0.tgz#7e8638e84c51bb110c5a69f91ca8b0e40b2c3fca"
   integrity sha512-YqpJE6/1Wkjrie0E+ypu+yzd55B5RlvKYMnQs3Ox+SrJsnNBhA6Oj44EhVf8SUTuHgn1t/mm+LvbswKN5RM4+g==

cc @trungleduc who was interested in this

@trungleduc
Copy link
Contributor

Thank @jtpio, i'm trying to use kernel id from session context as map keys with handler for kernelChanged event. It works but is it a good approach?

@jtpio
Copy link
Member Author

jtpio commented Jul 9, 2021

Thanks @trungleduc for looking into it.

kernel id from session context

Sounds like this could work? How does it behave with kernel restarts?

@trungleduc
Copy link
Contributor

It works pretty well with kernel restart and kernel change events. I stored current id of kernel so inside the kernelChanged event handler i can update the map with new kernel id.

    sessionContext.kernelChanged.connect((_, args) => {
      const { newValue } = args;
      if (newValue) {
        const newKernelId = newValue.id;
        const oldwManager = Private.widgetManagerProperty.get(currentOwner);

        if (oldwManager) {
          Private.widgetManagerProperty.delete(currentOwner);
          Private.widgetManagerProperty.set(newKernelId, oldwManager);
        }
        currentOwner = newKernelId;
      }
    });

@jtpio
Copy link
Member Author

jtpio commented Jul 12, 2021

NIce, thanks @trungleduc for sharing.

Maybe you would like to open a PR with your additional commits, so it's easier to check the diff and try it on Binder?

@trungleduc
Copy link
Contributor

PR opened on jtpio#2

@jtpio
Copy link
Member Author

jtpio commented Jul 19, 2021

Thanks @trungleduc!

I just pushed a change to fix the integrity check.

@jtpio
Copy link
Member Author

jtpio commented Jul 19, 2021

For those who would like to test this PR on Binder:

https://mybinder.org/v2/gh/jtpio/ipywidgets/code-consoles-widgets?urlpath=lab

widgets-code-consoles.mp4

Marking as ready for review, so other folks can start testing the feature and reviewing the code. Thanks!

@jtpio jtpio marked this pull request as ready for review July 19, 2021 13:13
sessionContext: ISessionContext
): Promise<Private.IWidgetManagerOwner> {
await sessionContext.ready;
return sessionContext.session!.kernel!.id;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wondering whether we should use the non-null assertion operator here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A SessionContext can exist without having a kernel yet.

Copy link
Member

@jasongrout jasongrout Aug 19, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wondering whether we should use the non-null assertion operator here?

I think it makes more sense to abort if we don't have a kernel. When we eventually get a kernel in the session context, the kernel changed signal is triggered which calls this again.

Update: Never mind. I've thought more about it, and put some thoughts below.

@jtpio jtpio added this to the 8.0 milestone Jul 23, 2021
@jtpio
Copy link
Member Author

jtpio commented Jul 23, 2021

Adding to the 8.0 milestone for consideration, since this is changing a couple of interfaces.

@SylvainCorlay
Copy link
Member

This is great! Many thanks to @jtpio and @trungleduc!

I can't wait to see this land in the next 8.0 pre-release.

@jasongrout jasongrout self-requested a review August 10, 2021 17:17
@martinRenou
Copy link
Member

Looks good! This is really exciting, especially in combination with your recent work on replite

@jasongrout this PR is already a good improvement, maybe we could iterate later (in separate PRs) on making this more generic?

I don't want to be asking too much, but it would be amazing if this could be included in 8.0.

Copy link
Member

@martinRenou martinRenou left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good!

@martinRenou
Copy link
Member

This seems to need a rebase though

@jtpio
Copy link
Member Author

jtpio commented Feb 3, 2022

Thanks @martinRenou.

Starting the rebase now.

@jtpio
Copy link
Member Author

jtpio commented Feb 3, 2022

Arf looks like the rebase and auto yarn resolve brings some new dependencies and typing conflicts:

image

Will get back to it later unless someone wants to give it a try before.

@jtpio
Copy link
Member Author

jtpio commented Feb 7, 2022

For reference this means widgets also show up in the RetroLab (future Notebook v7) code consoles:

image

JoaoFelipe added a commit to EPICLab/DSChatbot that referenced this pull request Apr 8, 2022
Temporary - it will properly work on future ipywidgets release
  see jupyter-widgets/ipywidgets#3004)
@AckslD
Copy link

AckslD commented Oct 31, 2022

I wanted to give this PR a try to see if I can help with testing, rebasing or anything. However when I do

pip install jupyterlab
./dev-install.sh

in a fresh virtualenv I still get the "Loading widget..." when creating eg an IntSlider. Are there any additional steps to make this work or has there been changes in eg jupyterlab what requires updates here?

@davidbrochart
Copy link
Member

This PR is probably a big improvement for lots of reasons, but I have the feeling that consoles should not be a different beast than notebooks anyway. They should be a "restricted notebook", where one can only append new cells and not be able to change previous cells. If we based consoles on notebooks, then they would support widgets out of the box, right?

@martinRenou
Copy link
Member

I just squashed to make the rebase easier (so that I don't need to fix the conflicts for all commits)

@martinRenou
Copy link
Member

Tested with a custom widget, this seems to work like a charm:

Screenshot from 2024-05-23 15-04-51

@martinRenou
Copy link
Member

This is changing exported APIs, we should do some exported functions renaming in order to stay backward compatible (if we don't want to wait for 9.x)

@martinRenou
Copy link
Member

Docs build failure should be fixed by #3918

@martinRenou martinRenou merged commit b12dae7 into jupyter-widgets:main May 24, 2024
20 checks passed
@jtpio jtpio deleted the code-consoles-widgets branch May 28, 2024 06:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Feature request: display ipywidgets in JupyterLab code console
10 participants