Skip to content

Commit 578675b

Browse files
authored
Merge pull request #46 from TK-21st/sessionContext
Exposing `session` information to kernel (fix a commit error in #44)
2 parents fc51499 + 6a60acb commit 578675b

File tree

6 files changed

+317
-1
lines changed

6 files changed

+317
-1
lines changed

examples/sessions.ipynb

+131
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
{
2+
"cells": [
3+
{
4+
"cell_type": "markdown",
5+
"metadata": {},
6+
"source": [
7+
"# Session Manager"
8+
]
9+
},
10+
{
11+
"cell_type": "code",
12+
"execution_count": null,
13+
"metadata": {},
14+
"outputs": [],
15+
"source": [
16+
"from ipylab import JupyterFrontEnd\n",
17+
"app = JupyterFrontEnd()"
18+
]
19+
},
20+
{
21+
"cell_type": "markdown",
22+
"metadata": {},
23+
"source": [
24+
"## show all sessions from the global `SessionManager` instance"
25+
]
26+
},
27+
{
28+
"cell_type": "code",
29+
"execution_count": null,
30+
"metadata": {},
31+
"outputs": [],
32+
"source": [
33+
"app.sessions.running()"
34+
]
35+
},
36+
{
37+
"cell_type": "markdown",
38+
"metadata": {},
39+
"source": [
40+
"## Show current session"
41+
]
42+
},
43+
{
44+
"cell_type": "code",
45+
"execution_count": null,
46+
"metadata": {},
47+
"outputs": [],
48+
"source": [
49+
"app.sessions.current_session"
50+
]
51+
},
52+
{
53+
"cell_type": "markdown",
54+
"metadata": {},
55+
"source": [
56+
"## Example: Create Console with current session\n",
57+
"The following two commands should both create a console panel sharing the same `session` as the current notebook."
58+
]
59+
},
60+
{
61+
"cell_type": "code",
62+
"execution_count": null,
63+
"metadata": {},
64+
"outputs": [],
65+
"source": [
66+
"app.commands.execute(\n",
67+
" 'console:create', \n",
68+
" app.sessions.current_session)"
69+
]
70+
},
71+
{
72+
"cell_type": "code",
73+
"execution_count": null,
74+
"metadata": {},
75+
"outputs": [],
76+
"source": [
77+
"app.commands.execute(\n",
78+
" 'notebook:create-console', \n",
79+
" {})"
80+
]
81+
},
82+
{
83+
"cell_type": "markdown",
84+
"metadata": {},
85+
"source": [
86+
"## Force update session (asynchronous)\n",
87+
"sessions should be updated automatically, you can force a call to `SessionManager.refreshRunning()`"
88+
]
89+
},
90+
{
91+
"cell_type": "code",
92+
"execution_count": null,
93+
"metadata": {},
94+
"outputs": [],
95+
"source": [
96+
"import asyncio\n",
97+
"from ipywidgets import Output\n",
98+
"\n",
99+
"out = Output()\n",
100+
"async def refresh_running():\n",
101+
" await app.sessions.refresh_running()\n",
102+
" sesssions = app.sessions.running()\n",
103+
" out.append_stdout('Session Refreshed')\n",
104+
"\n",
105+
"asyncio.create_task(refresh_running())\n",
106+
"out"
107+
]
108+
}
109+
],
110+
"metadata": {
111+
"kernelspec": {
112+
"display_name": "Python 3",
113+
"language": "python",
114+
"name": "python3"
115+
},
116+
"language_info": {
117+
"codemirror_mode": {
118+
"name": "ipython",
119+
"version": 3
120+
},
121+
"file_extension": ".py",
122+
"mimetype": "text/x-python",
123+
"name": "python",
124+
"nbconvert_exporter": "python",
125+
"pygments_lexer": "ipython3",
126+
"version": "3.7.7"
127+
}
128+
},
129+
"nbformat": 4,
130+
"nbformat_minor": 4
131+
}

ipylab/jupyterfrontend.py

+9-1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
from .commands import CommandRegistry
1414
from .shell import Shell
15+
from .sessions import SessionManager
1516

1617

1718
@register
@@ -23,9 +24,16 @@ class JupyterFrontEnd(Widget):
2324
version = Unicode(read_only=True).tag(sync=True)
2425
shell = Instance(Shell).tag(sync=True, **widget_serialization)
2526
commands = Instance(CommandRegistry).tag(sync=True, **widget_serialization)
27+
sessions = Instance(SessionManager).tag(sync=True, **widget_serialization)
2628

2729
def __init__(self, *args, **kwargs):
28-
super().__init__(*args, shell=Shell(), commands=CommandRegistry(), **kwargs)
30+
super().__init__(
31+
*args,
32+
shell=Shell(),
33+
commands=CommandRegistry(),
34+
sessions=SessionManager(),
35+
**kwargs
36+
)
2937
self._ready_event = asyncio.Event()
3038
self._on_ready_callbacks = CallbackDispatcher()
3139
self.on_msg(self._on_frontend_msg)

ipylab/sessions.py

+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
"""Expose current and all sessions to python kernel
2+
"""
3+
4+
import asyncio
5+
6+
from ipywidgets import CallbackDispatcher, Widget, register, widget_serialization
7+
from traitlets import List, Unicode, Dict
8+
9+
from ._frontend import module_name, module_version
10+
11+
12+
@register
13+
class SessionManager(Widget):
14+
"""Expose JupyterFrontEnd.serviceManager.sessions"""
15+
16+
_model_name = Unicode("SessionManagerModel").tag(sync=True)
17+
_model_module = Unicode(module_name).tag(sync=True)
18+
_model_module_version = Unicode(module_version).tag(sync=True)
19+
20+
# information of the current session
21+
current_session = Dict(read_only=True).tag(sync=True)
22+
# keeps track of the list of sessions
23+
sessions = List([], read_only=True).tag(sync=True)
24+
25+
def __init__(self, *args, **kwargs):
26+
super().__init__(*args, **kwargs)
27+
self._refreshed_event = None
28+
self._on_refresh_callbacks = CallbackDispatcher()
29+
self.on_msg(self._on_frontend_msg)
30+
31+
def _on_frontend_msg(self, _, content, buffers):
32+
if content.get("event", "") == "sessions_refreshed":
33+
self._refreshed_event.set()
34+
self._on_refresh_callbacks()
35+
36+
async def refresh_running(self):
37+
"""Force a call to refresh running sessions
38+
39+
Resolved when `SessionManager.runnigSession` resolves
40+
"""
41+
self.send({"func": "refreshRunning"})
42+
self._refreshed_event = asyncio.Event()
43+
await self._refreshed_event.wait()
44+
45+
def running(self):
46+
"""List all running sessions managed in the manager"""
47+
return self.sessions

src/plugin.ts

+2
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ const extension: JupyterFrontEndPlugin<void> = {
3636
widgetExports.ShellModel.shell = shell;
3737
widgetExports.CommandRegistryModel.commands = app.commands;
3838
widgetExports.CommandPaletteModel.palette = palette;
39+
widgetExports.SessionManagerModel.sessions = app.serviceManager.sessions;
40+
widgetExports.SessionManagerModel.shell = shell;
3941

4042
registry.registerWidget({
4143
name: MODULE_NAME,

src/widget.ts

+2
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import '../css/widget.css';
66

77
import { CommandRegistryModel } from './widgets/commands';
88
import { CommandPaletteModel } from './widgets/palette';
9+
import { SessionManagerModel } from './widgets/sessions';
910
import { JupyterFrontEndModel } from './widgets/frontend';
1011
import { PanelModel } from './widgets/panel';
1112
import { ShellModel } from './widgets/shell';
@@ -21,4 +22,5 @@ export {
2122
SplitPanelModel,
2223
SplitPanelView,
2324
TitleModel,
25+
SessionManagerModel,
2426
};

src/widgets/sessions.ts

+126
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
// SessionManager exposes `JupyterLab.serviceManager.sessions` to user python kernel
2+
3+
import { SessionManager } from '@jupyterlab/services';
4+
import { ISerializers, WidgetModel } from '@jupyter-widgets/base';
5+
import { toArray } from '@lumino/algorithm';
6+
import { MODULE_NAME, MODULE_VERSION } from '../version';
7+
import { Session } from '@jupyterlab/services';
8+
import { ILabShell } from '@jupyterlab/application';
9+
10+
/**
11+
* The model for a Session Manager
12+
*/
13+
export class SessionManagerModel extends WidgetModel {
14+
/**
15+
* The default attributes.
16+
*/
17+
defaults(): any {
18+
return {
19+
...super.defaults(),
20+
_model_name: SessionManagerModel.model_name,
21+
_model_module: SessionManagerModel.model_module,
22+
_model_module_version: SessionManagerModel.model_module_version,
23+
current_session: null,
24+
sessions: [],
25+
};
26+
}
27+
28+
/**
29+
* Initialize a SessionManagerModel instance.
30+
*
31+
* @param attributes The base attributes.
32+
* @param options The initialization options.
33+
*/
34+
initialize(attributes: any, options: any): void {
35+
const { sessions, shell } = SessionManagerModel;
36+
this._sessions = sessions;
37+
this._shell = shell;
38+
sessions.runningChanged.connect(this._sendSessions, this);
39+
shell.currentChanged.connect(this._currentChanged, this);
40+
41+
super.initialize(attributes, options);
42+
this.on('msg:custom', this._onMessage.bind(this));
43+
this._shell.activeChanged.connect(this._currentChanged, this);
44+
this._sendSessions();
45+
this._sendCurrent();
46+
this.send({ event: 'sessions_initialized' }, {});
47+
}
48+
49+
/**
50+
* Handle a custom message from the backend.
51+
*
52+
* @param msg The message to handle.
53+
*/
54+
private _onMessage(msg: any): void {
55+
switch (msg.func) {
56+
case 'refreshRunning':
57+
this._sessions.refreshRunning().then(() => {
58+
this.send({ event: 'sessions_refreshed' }, {});
59+
});
60+
break;
61+
default:
62+
break;
63+
}
64+
}
65+
66+
/**
67+
* get sessionContext from a given widget instance
68+
*
69+
* @param widget widget tracked by app.shell._track (FocusTracker)
70+
*/
71+
private _getSessionContext(widget: any): Session.IModel | {} {
72+
return widget?.sessionContext?.session?.model ?? {};
73+
}
74+
75+
/**
76+
* Handle focus change in JLab
77+
*
78+
* NOTE: currentChange fires on two situations that we are concerned about here:
79+
* 1. when user focuses on a widget in browser, which the `change.newValue` will
80+
* be the current Widget
81+
* 2. when user executes a code in console/notebook, where the `changed.newValue` will be null since
82+
* we lost focus due to execution.
83+
* To solve this problem, we interrogate `this._tracker.currentWidget` directly.
84+
* We also added a simple fencing to reduce the number of Comm sync calls between Python/JS
85+
*/
86+
private _currentChanged(): void {
87+
this._current_session = this._getSessionContext(this._shell.currentWidget);
88+
this.set('current_session', this._current_session);
89+
this.set('sessions', toArray(this._sessions.running()));
90+
this.save_changes();
91+
}
92+
93+
/**
94+
* Send the list of sessions to the backend.
95+
*/
96+
private _sendSessions(): void {
97+
this.set('sessions', toArray(this._sessions.running()));
98+
this.save_changes();
99+
}
100+
101+
/**
102+
* send current session to backend
103+
*/
104+
private _sendCurrent(): void {
105+
this._current_session = this._getSessionContext(this._shell.currentWidget);
106+
this.set('current_session', this._current_session);
107+
this.save_changes();
108+
}
109+
110+
static serializers: ISerializers = {
111+
...WidgetModel.serializers,
112+
};
113+
114+
static model_name = 'SessionManagerModel';
115+
static model_module = MODULE_NAME;
116+
static model_module_version = MODULE_VERSION;
117+
static view_name: string = null;
118+
static view_module: string = null;
119+
static view_module_version = MODULE_VERSION;
120+
121+
private _current_session: Session.IModel | {};
122+
private _sessions: SessionManager;
123+
static sessions: SessionManager;
124+
private _shell: ILabShell;
125+
static shell: ILabShell;
126+
}

0 commit comments

Comments
 (0)