翻译自: Making kernels for Jupyter

Making kernels for Jupyter

一个内核是运行和解析用户代码的程序。IPython包含了一个运行和解析Python代码的内核,而且人们已经写了多种语言的内核。

当Jupyter开始一个内核的时候,它会传递它一个连接文件。它指定了如何与前端开始通信。

关于写一个内核有两个选项:

  • 你可以重用IPython内核机制来处理通信,只需要描述如何运行你的代码就行了。如果你的目标语言可以被Python驱动的话,这种方式就比较简单。请查看Making simple Python wrapper kernels以获得详细细节。
  • 你可以使用目标语言了实现这个内核。开始这个工作会很多,但是后面如果有人开始用你的内核,就会有人来贡献代码。

Connection files

当内核开始的时候将会传入一个连接文件的路径(查看Kernel Specs 如何为你的内核指定命令行参数)。这个文件,只对当前用户可用,会包含类似下面的一个JSON字典。

1
2
3
4
5
6
7
8
9
10
11
{
"control_port": 50160,
"shell_port": 57503,
"transport": "tcp",
"signature_scheme": "hmac-sha256",
"stdin_port": 52597,
"hb_port": 42540,
"ip": "127.0.0.1",
"iopub_port": 40885,
"key": "a0436f6c-1916-498b-8eb9-e81ab9368e84"
}

transport, ip和制定了内核应该使用ZeroMQ绑定的五个_port。比如shell套接字的地址应该是:tcp://127.0.0.1:57503

在每个内核开始的时候会指定随意的端口。signature_schemekey用来加密信息,因此系统的其他用户不能发送代码来运行内核。

Handling messages

在读取连接文件后和绑定必须的套接字之后,内核应该进入一个事件循环,监听hb(heartbeat),control和shell套接字。

Heartbeat消息应该在同样的套接字上立即返回,前端使用这个消息来检查内核是否在线。

control和shell套接字的消息应该被解析,而且它们的签名应该是有效的。

内核会在iopub套接字上发送消息以展示输出,在stdin套接字上提示用户来输入。

Kernel specs

通过创建一个目录来让IPython识别一个内核,名字就是内核的标识符。目录可能在不同的位置

  • System:

    • /usr/share/jupyter/kernels
    • /usr/local/share/jupyter/kernels
  • Env:

    • {sys.prefix}/share/jupyter/kernels
  • User:

    • ~/.local/share/jupyter/kernels (Linux)
    • ~/Library/Jupyter/kernels (Mac)

用户位置的优先级高于系统级别的,忽略名字的大小写。因此不论系统是否大小写敏感,都可以以同样的烦噶事来获取内核。因为内核名字会在URL出现,因此内核名字需要是一个简单的,只使用ASCII字母,数字和简单的分隔符-, ., _

如果设置了JUPYTER_PATH环境变量的话,也会搜索其他位置。

在内核文件夹下,现在会使用三种类型的文件。kernel.json, kernel.js和log图片文件。目前,没有使用其他文件,但是将来可能会改变。

最重要的文件是kernel.json,应该是一个json序列化的字典包含以下字段

  • argv: 用来启动内核的命令行参数列表。{connection_file}将会被实际的连接文件的路径替换。
  • display_name: 在UI上展示的内核名字。不像在API中使用的内核名字,这里的名字可以包含任意字符。
  • language: 内核的语言名字。当载入notebook的时候,如果没有找到匹配的内核,那么匹配相应语言的内核将会被启动。这样允许一个写了任何Python或者julia内核的notebook可以与用户的Python或者julia内核合适的联系起来,即使它们没有在与用户内核同样的名字下。
  • interrupt_mode:可能是signal或者message指定了客户端如何在这个内核中停止单元运行。是通过发送一个信号呢,还是发送一个interrupt_request消息在control channel。如果没有指定,将默认使用signal模式。
  • env:为内核设置的环境变量。在内核启动前,会添加到当前的环境变量里。
  • metadata:关于这个内核的其他相关属性。帮助客户端选择内核。

比如:

1
2
3
4
5
6
{
"argv": ["python3", "-m", "IPython.kernel",
"-f", "{connection_file}"],
"display_name": "Python 3",
"language": "python"
}

查看有多少可用内核:jupyter kernelspec list

1
2
3
Available kernels:
python3 /Users/kevin/anaconda/lib/python3.5/site-packages/ipykernel/resources
ir /Users/kevin/Library/Jupyter/kernels/ir

Making simple Python wrapper kernels

你可以重用IPython的内核机制来非常容易的创建新内核。这对于Python绑定的语言来说非常有用。或者可以用pexpect来控制REPL的语言,比如bash

Required steps

子类化ipykernel.kernelbase.Kernel,然后实现下面的方法和属性

  • class MyKernel

    • implementation
    • implementation_version
    • banner

      Kernel info会返回的信息。Implementation指的是内核而不是语言,比如IPython而不是Python。banner是在控制UI上显示第一个提示符之前的东西。这些都是字符串

    • language_info

      Kernel info会返回的信息字典。应该包含mimetype键,值是目标语言的mimetype,比如text/x-python。name键是实现的语言比如pythonfile_extension比如.py,而且也可能根据不同语言包含codemirror_modepygments_lexer

    • do_execute(code, silent, store_history=True, user_expressions=None, allow_stdin=False)

      执行用户代码

      • code:要执行的代码
      • silent:是否展示输出
      • store_history: 是否在历史里记录代码,并且增加执行次数。
      • user_expressions:在代码被执行后对这些表达式求值
      • allow_stdin:前端是否提供输入请求

        你的方法应该返回一个字典,包含在Execution results规定的字典。为了展现输出,它可以使用send_response()来发送消息。

为了启动你的内核,在模块后面加上:

1
2
3
if __name__ == '__main__':
from ipykernel.kernelapp import IPKernelApp
IPKernelApp.launch_instance(kernel_class=MyKernel)

现在创建一个JSON的内核说明文件,然后通过jupyter kernelspec install </path/to/kernel>。将你的内核模块放在Python可以导入的地方,一般是当前目录(做测试)。最后,你可以使用jupyter console --kernel <mykernelname>来运行你的内核。

Example

echokernel.py会简单的将输入输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
from ipykernel.kernelbase import Kernel
class EchoKernel(Kernel):
implementation = 'Echo'
implementation_version = '1.0'
language = 'no-op'
language_version = '0.1'
language_info = {
'name': 'Any text',
'mimetype': 'text/plain',
'file_extension': '.txt',
}
banner = "Echo kernel - as useful as a parrot"
def do_execute(self, code, silent, store_history=True, user_expressions=None,
allow_stdin=False):
if not silent:
stream_content = {'name': 'stdout', 'text': code}
self.send_response(self.iopub_socket, 'stream', stream_content)
return {'status': 'ok',
# The base class increments the execution count
'execution_count': self.execution_count,
'payload': [],
'user_expressions': {},
}
if __name__ == '__main__':
from ipykernel.kernelapp import IPKernelApp
IPKernelApp.launch_instance(kernel_class=EchoKernel)

下面是内核说明文件kernel.json

1
2
3
{"argv":["python","-m","echokernel", "-f", "{connection_file}"],
"display_name":"Echo"
}