JupyterLab 是一个基于web的交互式开发环境,比较方便基于 web 界面来开发、调试 python 等应用程序,在数据科学、科学计算和机器学习领域有广泛的应用。它的主要 github 仓库在这里

JupyterLab 本身是一个非常复杂的 web 项目,通过模块化的理念进行开发,方便新增各类插件,本文对 JupyterLab 的架构和插件开发做一个梳理。

整体启动流程

一般我们通过 jupyter-lab 或者 jupyter lab 来启动 JupyterLab,如果是前者,会调用到 python 环境下的 bin/jupyter-lab,如果是后者的方式启动,即先调用到 jupyter_core ,然后其实际上也会通过命令行调用到 jupyter-lab,脚本中的代码非常简单:

1
2
3
4
5
6
7
# -*- coding: utf-8 -*-
import re
import sys
from jupyterlab.labapp import main
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(main())

jupyterlab.labapp 是基于 tornado 构建的 web 服务器。

实际上 jupyterlab 本身的内容并不多,其内部依赖了 jupyter_serverjupyter_core,对于前端页面部分,其通过将产物目录配置成静态目录的方式提供服务

labextension

对于大多数开发阶段的命令,都是 jupyter labextension 开头的,这部分实际上是调用到了 jupyterlab.labextensions:

1
2
3
4
5
6
7
8
#!/Users/aircloud/work2/myPythonEnv/venv/bin/python
# -*- coding: utf-8 -*-
import re
import sys
from jupyterlab.labextensions import main
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(main())

实际上,最终的 build 指令会走到 builder/src/build-labextension.ts 这里,这里会调用 webpack 的 js 接口来进行构建。

lumino

lumino 提供了一些构建交互式 web 应用的组件,也在 jupyter 这个组织里面,虽然 jupyterlab 使用了它,但是它也可以脱离 jupyter 使用。

lumio 里面提供了一些示例,包括面板、数据表格、数据存储、dock模式的数据面板等,可以让我们初步了解它所做的事情。

代码主流程

所有插件会被收归到 dev_mode/static/index.out.js 这里。

最后被调用到 node_modules/@lumino/application/src/index.tsregisterPlugins 函数。

然后 index.out.js 会调用 lab.start({ ignorePlugins });,这个 start 实际上就是 node_modules/@lumino/application/src/index.ts 这里的方法,start 阶段主要做的一个事情是 activatePlugin

  • 自定义的组件如何完成?

通过渲染的时候在 html 注入 page_config 的方式:

1
2
3
4
5
6
7
8
{% set page_config_full = page_config.copy() %}

{# Set a dummy variable - we just want the side effect of the update. #}
{% set _ = page_config_full.update(baseUrl=base_url, wsUrl=ws_url) %}

<script id="jupyter-config-data" type="application/json">
{{ page_config_full | tojson }}
</script>

其中的 federated_extensions 字段是有关自定义插件的信息:

1
2
3
4
extension: "./extension"
load: "static/remoteEntry.417ed90585e6295dc378.js"
name: "jupyterlab_apod"
style: "./style"

page_config 的注入逻辑,在 jupyterlab_server/handlers.py 渲染到 html 中,具体找插件的逻辑实际上是从路径中寻找的,这部分代码在 jupyterlab_server/config.pyget_federated_extensions

  • 如何实现懒加载?

利用 webpack 打包生成模块,主要的构建配置的示例:

1
2
3
4
5
6
7
"library": {
"type": "var",
"name": [
"_JUPYTERLAB",
"jupyterlab_apod"
]
},

然后代码在在一开始就加载进来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// dev_mode/bootstrap.js
async function loadComponent(url, scope) {
await loadScript(url);
// From https://webpack.js.org/concepts/module-federation/#dynamic-remote-containers
// eslint-disable-next-line no-undef
await __webpack_init_sharing__('default');
const container = window._JUPYTERLAB[scope];
// Initialize the container, it may provide shared modules and may need ours
// eslint-disable-next-line no-undef
await container.init(__webpack_share_scopes__.default);
}

void (async function bootstrap() {
const extension_data = getOption('federated_extensions');
let labExtensionUrl = getOption('fullLabextensionsUrl');
const extensions = await Promise.allSettled(
extension_data.map(async data => {
await loadComponent(
`${labExtensionUrl}/${data.name}/${data.load}`,
data.name
);
})
);

加载代码之后初始化模块的示例:

1
2
3
4
5
6
7
8
9
10
11
// dev_mode/index.js
async function createModule(scope, module) {
try {
console.info('staging index.out createModule:', scope, module); // staging index.out createModule: jupyterlab_apod ./extension
const factory = await window._JUPYTERLAB[scope].get(module);
return factory();
} catch(e) {
console.warn(`Failed to create module: package: ${scope}; module: ${module}`);
throw e;
}
}

dev_mode 下面的几个文件的关系:

  • bootstrap.js 被配置为 webpack 的 entry。
  • index.js 会经过模版替换变成 index.out.js,后者被 bootstrap.js 引用。

jupyterhub

最基本的可以看官方的 getstart
https://jupyterhub.readthedocs.io/en/latest/getting-started/index.html

这篇讲的是基本的部署,讲的还可以的:
https://ojerk.cn/Jupyterhub%E6%9C%8D%E5%8A%A1%E5%99%A8%E9%83%A8%E7%BD%B2%E4%B8%8D%E5%AE%8C%E5%85%A8%E6%8C%87%E5%8C%97/

jupyterhub 默认采用 PAM 进行鉴权,因此如果你有系统的用户和权限,才能在 jupyterhub 进行登录。

server 插件

的确是直接 import 进来的

1
2
// jupyter_server/extension/manager.py
self._module = importlib.import_module(self._module_name)

所以我们使用这种类似 python path 的方式应该还是管用的:

1
export PYTHONPATH=$PYTHONPATH:/Users/aircloud/work2/myPythonEnv/test_python_path/path1:/Users/aircloud/work2/myPythonEnv/test_python_path/path2 && jupyter-lab

client 插件,默认路径

1
2
3
4
5
6
7
8
9
10
11
if os.name == 'nt':
programdata = os.environ.get('PROGRAMDATA', None)
if programdata:
SYSTEM_JUPYTER_PATH = [pjoin(programdata, 'jupyter')]
else: # PROGRAMDATA is not defined by default on XP.
SYSTEM_JUPYTER_PATH = [os.path.join(sys.prefix, 'share', 'jupyter')]
else:
SYSTEM_JUPYTER_PATH = [
"/usr/local/share/jupyter",
"/usr/share/jupyter",
]