Skip to content

Commit a84dc8f

Browse files
authored
jupyter display support (#5)
1 parent cdd2a1d commit a84dc8f

8 files changed

+370
-3
lines changed

.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
11
venv
2+
3+
**/*.pyc

EnableIPythonDisplay.swift

+82
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
// Copyright 2018 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
/// Hooks IPython to the KernelCommunicator, so that it can send display
16+
/// messages to Jupyter.
17+
18+
import Python
19+
20+
// Workaround SR-7757.
21+
#if canImport(Darwin)
22+
import func Darwin.C.dlopen
23+
#elseif canImport(Glibc)
24+
import func Glibc.dlopen
25+
#else
26+
#error("Cannot import Darwin or Glibc!")
27+
#endif
28+
dlopen("libpython2.7.so", RTLD_NOW | RTLD_GLOBAL)
29+
30+
enum IPythonDisplay {
31+
static var socket: PythonObject = Python.None
32+
static var shell: PythonObject = Python.None
33+
34+
}
35+
36+
extension IPythonDisplay {
37+
private static func bytes(_ py: PythonObject) -> [CChar] {
38+
// faster not-yet-introduced method
39+
// return py.swiftBytes!
40+
41+
// slow placeholder implementation
42+
return py.map { el in
43+
return CChar(bitPattern: UInt8(Python.ord(el))!)
44+
}
45+
}
46+
47+
private static func updateParentMessage(
48+
to parentMessage: KernelCommunicator.ParentMessage) {
49+
let json = Python.import("json")
50+
IPythonDisplay.shell.set_parent(json.loads(parentMessage.json))
51+
}
52+
53+
private static func consumeDisplayMessages()
54+
-> [KernelCommunicator.JupyterDisplayMessage] {
55+
let displayMessages = IPythonDisplay.socket.messages.map {
56+
KernelCommunicator.JupyterDisplayMessage(parts: $0.map { bytes($0) })
57+
}
58+
IPythonDisplay.socket.messages = []
59+
return displayMessages
60+
}
61+
62+
static func enable() {
63+
if IPythonDisplay.shell != Python.None {
64+
print("Warning: IPython display already enabled.")
65+
return
66+
}
67+
68+
let swift_shell = Python.import("swift_shell")
69+
let socketAndShell = swift_shell.create_shell(
70+
username: JupyterKernel.communicator.jupyterSession.username,
71+
session_id: JupyterKernel.communicator.jupyterSession.id,
72+
key: JupyterKernel.communicator.jupyterSession.key)
73+
IPythonDisplay.socket = socketAndShell[0]
74+
IPythonDisplay.shell = socketAndShell[1]
75+
76+
JupyterKernel.communicator.handleParentMessage(updateParentMessage)
77+
JupyterKernel.communicator.afterSuccessfulExecution(
78+
run: consumeDisplayMessages)
79+
}
80+
}
81+
82+
IPythonDisplay.enable()

KernelCommunicator.swift

+79
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
// Copyright 2018 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
/// A struct with functions that the kernel and the code running inside the
16+
/// kernel use to talk to each other.
17+
///
18+
/// Note that it would be more Jupyter-y for the communication to happen over
19+
/// ZeroMQ. This is not currently possible, because ZeroMQ sends messages
20+
/// asynchronously using IO threads, and LLDB pauses those IO threads, which
21+
/// prevents them from sending the messages.
22+
public struct KernelCommunicator {
23+
private var afterSuccessfulExecutionHandlers: [() -> [JupyterDisplayMessage]]
24+
private var parentMessageHandlers: [(ParentMessage) -> ()]
25+
26+
public let jupyterSession: JupyterSession
27+
28+
init(jupyterSession: JupyterSession) {
29+
self.afterSuccessfulExecutionHandlers = []
30+
self.parentMessageHandlers = []
31+
self.jupyterSession = jupyterSession
32+
}
33+
34+
/// Register a handler to run after the kernel successfully executes a cell
35+
/// of user code. The handler may return messages. These messages will be
36+
/// send to the Jupyter client.
37+
public mutating func afterSuccessfulExecution(
38+
run handler: @escaping () -> [JupyterDisplayMessage]) {
39+
afterSuccessfulExecutionHandlers.append(handler)
40+
}
41+
42+
/// Register a handler to run when the parent message changes.
43+
public mutating func handleParentMessage(
44+
_ handler: @escaping (ParentMessage) -> ()) {
45+
parentMessageHandlers.append(handler)
46+
}
47+
48+
/// The kernel calls this after successfully executing a cell of user code.
49+
public func triggerAfterSuccessfulExecution() -> [JupyterDisplayMessage] {
50+
return afterSuccessfulExecutionHandlers.flatMap { $0() }
51+
}
52+
53+
/// The kernel calls this when the parent message changes.
54+
public mutating func updateParentMessage(
55+
to parentMessage: ParentMessage) {
56+
for parentMessageHandler in parentMessageHandlers {
57+
parentMessageHandler(parentMessage)
58+
}
59+
}
60+
61+
/// A single serialized display message for the Jupyter client.
62+
public struct JupyterDisplayMessage {
63+
public let parts: [[CChar]]
64+
}
65+
66+
/// ParentMessage identifies the request that causes things to happen.
67+
/// This lets Jupyter, for example, know which cell to display graphics
68+
/// messages in.
69+
public struct ParentMessage {
70+
let json: String
71+
}
72+
73+
/// The data necessary to identify and sign outgoing jupyter messages.
74+
public struct JupyterSession {
75+
let id: String
76+
let key: String
77+
let username: String
78+
}
79+
}

README.md

+64-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Swift-Jupyter
22

3-
This is a Jupyter Kernel for Swift, intended to make it possible to use Juptyer
3+
This is a Jupyter Kernel for Swift, intended to make it possible to use Jupyter
44
with the [Swift for Tensorflow](https://github.com/tensorflow/swift) project.
55

66
This kernel is currently very barebones and experimental.
@@ -16,7 +16,9 @@ virtualenv venv
1616
pip2 install jupyter # Must use python2, because LLDB doesn't support python3.
1717
```
1818

19-
Install a Swift toolchain ([see instructions here](https://github.com/tensorflow/swift/blob/master/Installation.md)).
19+
Install a Swift toolchain. For example, install the [Swift for Tensorflow] toolchain.
20+
21+
[Swift for Tensorflow]: https://github.com/tensorflow/swift/blob/master/Installation.md
2022

2123
Optionally [install SourceKitten](https://github.com/jpsim/SourceKitten) (this enables code completion).
2224

@@ -30,6 +32,66 @@ Now run `jupyter notebook`, and it should have a Swift kernel.
3032

3133
# Usage Instructions
3234

35+
## Rich output
36+
37+
You can call Python libaries using [Swift's Python interop] to display rich
38+
output in your Swift notebooks. (Eventually, we'd like to support Swift
39+
libraries the produce rich output too!)
40+
41+
Prerequisites:
42+
43+
* You must use a Swift toolchain that has Python interop. As of July 2018,
44+
only the [Swift for Tensorflow] toolchain has Python interop.
45+
46+
* Install the `ipykernel` Python library, and any other Python libraries
47+
that you want output from (such as `matplotlib` or `pandas`) on your
48+
system Python. (Do not install them on the virtualenv from the Swift-Jupyter
49+
installation instructions. Swift's Python interop talks to your system
50+
Python.)
51+
52+
After taking care of the prerequisites, run
53+
`%include "EnableIPythonDisplay.swift"` in your Swift notebook. Now you should
54+
be able to display rich output! For example:
55+
56+
```swift
57+
let np = Python.import("numpy")
58+
let plt = Python.import("matplotlib.pyplot")
59+
IPythonDisplay.shell.enable_matplotlib("inline")
60+
```
61+
62+
```swift
63+
let time = np.arange(0, 10, 0.01)
64+
let amplitude = np.exp(-0.1 * time)
65+
let position = amplitude * np.sin(3 * time)
66+
67+
plt.figure(figsize: [15, 10])
68+
69+
plt.plot(time, position)
70+
plt.plot(time, amplitude)
71+
plt.plot(time, -amplitude)
72+
73+
plt.xlabel("time (s)")
74+
plt.ylabel("position (m)")
75+
plt.title("Oscillations")
76+
77+
plt.show()
78+
```
79+
80+
![Screenshot of running the above two snippets of code in Jupyter](./screenshots/display_matplotlib.png)
81+
82+
```swift
83+
let display = Python.import("IPython.display")
84+
let pd = Python.import("pandas")
85+
```
86+
87+
```swift
88+
display.display(pd.DataFrame.from_records([["col 1": 3, "col 2": 5], ["col 1": 8, "col 2": 2]]))
89+
```
90+
91+
![Screenshot of running the above two snippets of code in Jupyter](./screenshots/display_pandas.png)
92+
93+
[Swift's Python interop]: https://github.com/tensorflow/swift/blob/master/docs/PythonInteroperability.md
94+
3395
## %include directives
3496

3597
`%include` directives let you include code from files. To use them, put a line

screenshots/display_matplotlib.png

110 KB
Loading

screenshots/display_pandas.png

27.3 KB
Loading

0 commit comments

Comments
 (0)