iruby 0.0.1 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +1 -64
- data/Ruby Notebook.ipynb +6 -69
- data/{test → attic/test}/helper.rb +0 -0
- data/{test → attic/test}/html_test.rb +0 -0
- data/{test → attic/test}/output_maps_test.rb +0 -0
- data/lib/iruby.rb +1 -2
- data/lib/iruby/command.rb +2 -33
- data/lib/iruby/kernel.rb +87 -146
- data/lib/iruby/ostream.rb +42 -0
- data/lib/iruby/session.rb +54 -268
- data/lib/iruby/version.rb +1 -1
- data/static/base/images/favicon.ico +0 -0
- metadata +7 -11
- data/lib/iruby/display_hook.rb +0 -29
- data/lib/iruby/out_stream.rb +0 -93
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8b8da3f0c37d62309c5a113b463ab8567b1c49b9
|
4
|
+
data.tar.gz: 36649a74bcb7dbcd7c91628efc8fd37c8a6bf211
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1d1d55e0f01d3ddc87cde6a3d6a610265c010ac8c4b0cd22b18d326c85c783e462e23258e32ab6f3afaebbf83c19b0737c75793bcc2968e7b70d96377c243aa2
|
7
|
+
data.tar.gz: 98f46f4e30e7c8f745c783ffeaa89e16c6a79f71b18abc097a6b45c8ad50a48fe94927f11b5f52d545884201f45df59e831c98eaf1587cb6d3c21352a14ed7f7
|
data/README.md
CHANGED
@@ -2,69 +2,6 @@
|
|
2
2
|
|
3
3
|
This is a Ruby kernel for IPython.
|
4
4
|
|
5
|
-
It adds a special `iruby_profile` command for staging some customization
|
6
|
-
that enables the Ruby kernel by default, and sets syntax-highlighting in the notebook
|
7
|
-
to Ruby mode.
|
8
|
-
|
9
5
|
### Usage
|
10
6
|
|
11
|
-
|
12
|
-
use IPython as usual:
|
13
|
-
|
14
|
-
```bash
|
15
|
-
git clone git://github.com/minrk/iruby
|
16
|
-
cd iruby
|
17
|
-
# build and install IRuby
|
18
|
-
gem build iruby.gemspec
|
19
|
-
$ gem install iruby-*.gem
|
20
|
-
# Create an IPython profile with default config
|
21
|
-
$ iruby_profile --create
|
22
|
-
$ ipython notebook --profile=ruby
|
23
|
-
```
|
24
|
-
|
25
|
-
|
26
|
-
## Background
|
27
|
-
|
28
|
-
### Building an in-browser REPL for Ruby (IRuby)
|
29
|
-
|
30
|
-
Hey, I'm Josh Adams. I'm a partner and CTO at isotope|eleven. We alo host
|
31
|
-
Birmingham, AL's Open Source Software meetup - BOSS.
|
32
|
-
|
33
|
-
At one of these sessions in early 2012, Tom Brander did a presentation and used
|
34
|
-
IPython in his browser to manage it (there was much code and it was executed
|
35
|
-
live). This was the first time I'd seen IPython in the browser where it
|
36
|
-
actually worked like it was supposed to, and I was extremely impressed.
|
37
|
-
|
38
|
-
If you've not seen IPython, it looks like this <* Insert Screenshot Here *> in
|
39
|
-
its web-browser mode. It also manages a lot of console-basd REPLs.
|
40
|
-
|
41
|
-
Anyway, it has notebooks that you can save out to execute later, and you can
|
42
|
-
pass them around as little code snippets for other people to check out. It's
|
43
|
-
very impressive.
|
44
|
-
|
45
|
-
But I'm primarily a Rubyist, and I'm happy that way :) I couldn't sit by while
|
46
|
-
Python had this awesome tool that we lacked. I looked around for a bit, and
|
47
|
-
there was nothing like IPython in our ecosystem. There were, however, quite a
|
48
|
-
few people asking about it. So I figured I'd do something about it.
|
49
|
-
|
50
|
-
#### The Architecture
|
51
|
-
|
52
|
-
So the IPython guys did a great job explaining their core architecture, both in
|
53
|
-
words and in pared-down code, in a blog post they wrote concerning it. In
|
54
|
-
general, it works like this <* Diagram *>
|
55
|
-
|
56
|
-
There's a kernel that runs in the background and gets connected to by a
|
57
|
-
frontend. They communicate using zeromq, and they send json formatted messages
|
58
|
-
back and forth. These messages are in a very well defined structure. Anyway,
|
59
|
-
this way the frontend of the repl is disconnected from the environment that's
|
60
|
-
running it.
|
61
|
-
|
62
|
-
So the code repository they linked to in their blog post included the kernel and
|
63
|
-
the frontend as small-ish python files - around 300 and 200 lines respectively.
|
64
|
-
We had a hack weekend at isotope|eleven where myself and Robby Clements got
|
65
|
-
together and (when we weren't playing Counterstrike1.6) did the closest thing to
|
66
|
-
a straight port that we could swing. Within about 2 hours of work, we had a
|
67
|
-
working proof of concept that was primarily a 1 to 1 port.
|
68
|
-
|
69
|
-
The next move was to build the web frontend. This just consists of a websocket
|
70
|
-
server and a fairly basic frontend webpage.
|
7
|
+
Install the rubygem using `gem install iruby` and then run `iruby notebook`.
|
data/Ruby Notebook.ipynb
CHANGED
@@ -1,47 +1,12 @@
|
|
1
1
|
{
|
2
2
|
"metadata": {
|
3
|
-
"name": ""
|
3
|
+
"name": "Ruby Notebook"
|
4
4
|
},
|
5
5
|
"nbformat": 3,
|
6
6
|
"nbformat_minor": 0,
|
7
7
|
"worksheets": [
|
8
8
|
{
|
9
9
|
"cells": [
|
10
|
-
{
|
11
|
-
"cell_type": "code",
|
12
|
-
"collapsed": false,
|
13
|
-
"input": [
|
14
|
-
"puts \"hi\"\n",
|
15
|
-
"raise \"zofe\""
|
16
|
-
],
|
17
|
-
"language": "python",
|
18
|
-
"metadata": {},
|
19
|
-
"outputs": [
|
20
|
-
{
|
21
|
-
"output_type": "stream",
|
22
|
-
"stream": "stdout",
|
23
|
-
"text": [
|
24
|
-
"hi\n"
|
25
|
-
]
|
26
|
-
},
|
27
|
-
{
|
28
|
-
"ename": "RuntimeError",
|
29
|
-
"evalue": "zofe",
|
30
|
-
"output_type": "pyerr",
|
31
|
-
"traceback": [
|
32
|
-
"\u001b[31mRuntimeError\u001b[0m: zofe",
|
33
|
-
"\u001b[37m<main>:1:in `<main>'\u001b[0m",
|
34
|
-
"\u001b[37m/home/minad/code/git/iruby/lib/iruby/kernel.rb:116:in `eval'\u001b[0m",
|
35
|
-
"\u001b[37m/home/minad/code/git/iruby/lib/iruby/kernel.rb:116:in `execute_request'\u001b[0m",
|
36
|
-
"\u001b[37m/home/minad/code/git/iruby/lib/iruby/kernel.rb:176:in `start'\u001b[0m",
|
37
|
-
"\u001b[37m/home/minad/code/git/iruby/lib/iruby/command.rb:60:in `run_kernel'\u001b[0m",
|
38
|
-
"\u001b[37m/home/minad/code/git/iruby/lib/iruby/command.rb:15:in `run'\u001b[0m",
|
39
|
-
"\u001b[37m/home/minad/code/git/iruby/bin/iruby:6:in `<main>'\u001b[0m"
|
40
|
-
]
|
41
|
-
}
|
42
|
-
],
|
43
|
-
"prompt_number": 3
|
44
|
-
},
|
45
10
|
{
|
46
11
|
"cell_type": "markdown",
|
47
12
|
"metadata": {},
|
@@ -65,22 +30,11 @@
|
|
65
30
|
" end\n",
|
66
31
|
"end\n",
|
67
32
|
"\n",
|
68
|
-
"Neat.new.eh
|
69
|
-
"\n",
|
70
|
-
"puts \"hi\""
|
33
|
+
"Neat.new.eh?"
|
71
34
|
],
|
72
|
-
"language": "
|
35
|
+
"language": "ruby",
|
73
36
|
"metadata": {},
|
74
|
-
"outputs": [
|
75
|
-
{
|
76
|
-
"output_type": "stream",
|
77
|
-
"stream": "stdout",
|
78
|
-
"text": [
|
79
|
-
"hi\n"
|
80
|
-
]
|
81
|
-
}
|
82
|
-
],
|
83
|
-
"prompt_number": 2
|
37
|
+
"outputs": []
|
84
38
|
},
|
85
39
|
{
|
86
40
|
"cell_type": "markdown",
|
@@ -106,26 +60,9 @@
|
|
106
60
|
"\n",
|
107
61
|
"ThatsPrettyCool.new.how_does_it_work?"
|
108
62
|
],
|
109
|
-
"language": "
|
63
|
+
"language": "ruby",
|
110
64
|
"metadata": {},
|
111
|
-
"outputs": [
|
112
|
-
{
|
113
|
-
"output_type": "stream",
|
114
|
-
"stream": "stdout",
|
115
|
-
"text": [
|
116
|
-
"I'm glad you asked!\n"
|
117
|
-
]
|
118
|
-
},
|
119
|
-
{
|
120
|
-
"metadata": {},
|
121
|
-
"output_type": "pyout",
|
122
|
-
"prompt_number": 4,
|
123
|
-
"text": [
|
124
|
-
"see_below"
|
125
|
-
]
|
126
|
-
}
|
127
|
-
],
|
128
|
-
"prompt_number": 4
|
65
|
+
"outputs": []
|
129
66
|
},
|
130
67
|
{
|
131
68
|
"cell_type": "markdown",
|
File without changes
|
File without changes
|
File without changes
|
data/lib/iruby.rb
CHANGED
data/lib/iruby/command.rb
CHANGED
@@ -25,39 +25,8 @@ module IRuby
|
|
25
25
|
Dir.chdir(working_dir) if working_dir
|
26
26
|
require boot_file if boot_file
|
27
27
|
require 'iruby'
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
puts 'Starting the kernel'
|
32
|
-
puts config
|
33
|
-
|
34
|
-
c = ZMQ::Context.new
|
35
|
-
|
36
|
-
connection = "#{config['transport']}://#{config['ip']}:%d"
|
37
|
-
|
38
|
-
session = IRuby::Session.new('kernel', config['key'], config['signature_scheme'])
|
39
|
-
|
40
|
-
reply_socket = c.socket(ZMQ::XREP)
|
41
|
-
reply_socket.bind(connection % config['shell_port'])
|
42
|
-
|
43
|
-
pub_socket = c.socket(ZMQ::PUB)
|
44
|
-
pub_socket.bind(connection % config['iopub_port'])
|
45
|
-
|
46
|
-
hb_thread = Thread.new do
|
47
|
-
hb_socket = c.socket(ZMQ::REP)
|
48
|
-
hb_socket.bind(connection % config['hb_port'])
|
49
|
-
ZMQ::Device.new(ZMQ::FORWARDER, hb_socket, hb_socket)
|
50
|
-
end
|
51
|
-
|
52
|
-
puts "Use Ctrl-\\ (NOT Ctrl-C!) to terminate."
|
53
|
-
|
54
|
-
$stdout = IRuby::OutStream.new(session, pub_socket, 'stdout')
|
55
|
-
#$stderr = OutStream.new(session, pub_socket, 'stderr')
|
56
|
-
|
57
|
-
kernel = IRuby::Kernel.new(session, reply_socket, pub_socket)
|
58
|
-
$displayhook = IRuby::DisplayHook.new(kernel, session, pub_socket)
|
59
|
-
|
60
|
-
kernel.start
|
28
|
+
$iruby_kernel = Kernel.new(config_file)
|
29
|
+
$iruby_kernel.run
|
61
30
|
end
|
62
31
|
|
63
32
|
def run_ipython
|
data/lib/iruby/kernel.rb
CHANGED
@@ -1,193 +1,134 @@
|
|
1
1
|
module IRuby
|
2
|
+
class Kernel
|
3
|
+
RED = "\e[31m"
|
4
|
+
WHITE = "\e[37m"
|
5
|
+
RESET = "\e[0m"
|
2
6
|
|
3
|
-
|
4
|
-
|
5
|
-
@response = response
|
6
|
-
@mime = mime
|
7
|
-
end
|
8
|
-
attr_reader :mime
|
9
|
-
def inspect
|
10
|
-
@response.inspect
|
11
|
-
end
|
12
|
-
def to_s
|
13
|
-
@response.to_s
|
14
|
-
end
|
15
|
-
end
|
7
|
+
def initialize(config_file)
|
8
|
+
config = MultiJson.load(File.read(config_file))
|
16
9
|
|
17
|
-
|
18
|
-
|
19
|
-
@output_mime = "text/html"
|
20
|
-
s
|
21
|
-
end
|
10
|
+
puts 'Starting the kernel'
|
11
|
+
puts config
|
22
12
|
|
23
|
-
|
24
|
-
@execution_count
|
25
|
-
end
|
13
|
+
c = ZMQ::Context.new
|
26
14
|
|
27
|
-
|
28
|
-
@
|
29
|
-
|
15
|
+
connection = "#{config['transport']}://#{config['ip']}:%d"
|
16
|
+
@reply_socket = c.socket(ZMQ::XREP)
|
17
|
+
@reply_socket.bind(connection % config['shell_port'])
|
18
|
+
|
19
|
+
@pub_socket = c.socket(ZMQ::PUB)
|
20
|
+
@pub_socket.bind(connection % config['iopub_port'])
|
21
|
+
|
22
|
+
@hb_thread = Thread.new do
|
23
|
+
hb_socket = c.socket(ZMQ::REP)
|
24
|
+
hb_socket.bind(connection % config['hb_port'])
|
25
|
+
ZMQ::Device.new(ZMQ::FORWARDER, hb_socket, hb_socket)
|
26
|
+
end
|
27
|
+
|
28
|
+
puts 'Use Ctrl-\\ (NOT Ctrl-C!) to terminate.'
|
29
|
+
|
30
|
+
@session = Session.new('kernel', config['key'], config['signature_scheme'])
|
31
|
+
|
32
|
+
$stdout = OStream.new(@session, @pub_socket, 'stdout')
|
33
|
+
$stderr = OStream.new(@session, @pub_socket, 'stderr')
|
30
34
|
|
31
|
-
def initialize session, reply_socket, pub_socket
|
32
|
-
@session = session
|
33
|
-
@reply_socket = reply_socket
|
34
|
-
@pub_socket = pub_socket
|
35
|
-
@history = []
|
36
35
|
@execution_count = 0
|
37
36
|
@completer = Completer.new
|
37
|
+
end
|
38
38
|
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
39
|
+
def run
|
40
|
+
send_status('starting')
|
41
|
+
while true
|
42
|
+
ident, msg = @session.recv(@reply_socket, 0)
|
43
|
+
type = msg[:header]['msg_type']
|
44
|
+
if type =~ /_request\Z/ && respond_to?(type)
|
45
|
+
send(type, ident, msg)
|
46
|
+
else
|
47
|
+
STDERR.puts "Unknown message type: #{msg[:header]['msg_type']} #{msg.inspect}"
|
48
|
+
end
|
43
49
|
end
|
44
50
|
end
|
45
51
|
|
46
|
-
def
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
#break
|
53
|
-
#else
|
54
|
-
#assert self.reply_socket.rcvmore(), "Unexpected missing message part."
|
55
|
-
#msg = self.reply_socket.recv_json()
|
56
|
-
#end
|
57
|
-
#end
|
58
|
-
msg_type = msg['header']['msg_type']
|
59
|
-
reply_type = msg_type.split('_')[0] + '_reply'
|
60
|
-
@session.send(@reply_socket, reply_type, {status: 'aborted'}, msg)
|
61
|
-
# reply_msg = @session.msg(reply_type, {status: 'aborted'}, msg)
|
62
|
-
# @reply_socket.send(ident,ZMQ::SNDMORE)
|
63
|
-
# @reply_socket.send(reply_msg.to_json)
|
64
|
-
# We need to wait a bit for requests to come in. This can probably
|
65
|
-
# be set shorter for true asynchronous clients.
|
66
|
-
sleep(0.1)
|
52
|
+
def display(obj, options={})
|
53
|
+
if obj
|
54
|
+
data = {}
|
55
|
+
data[obj.respond_to?(:mime) ? obj.mime : (options[:mime] || 'text/plain')] = obj.to_s
|
56
|
+
content = { data: data, metadata: {}, execution_count: @execution_count }
|
57
|
+
@session.send(@pub_socket, 'pyout', content)
|
67
58
|
end
|
59
|
+
nil
|
68
60
|
end
|
69
61
|
|
70
|
-
def kernel_info_request(ident,
|
71
|
-
|
62
|
+
def kernel_info_request(ident, msg)
|
63
|
+
content = {
|
72
64
|
protocol_version: [4, 0],
|
73
65
|
|
74
66
|
# Language version number (mandatory).
|
75
67
|
# It is Python version number (e.g., [2, 7, 3]) for the kernel
|
76
68
|
# included in IPython.
|
77
|
-
language_version: RUBY_VERSION.split('.').map
|
69
|
+
language_version: RUBY_VERSION.split('.').map(&:to_i),
|
78
70
|
|
79
71
|
# Programming language in which kernel is implemented (mandatory).
|
80
72
|
# Kernel included in IPython returns 'python'.
|
81
|
-
language:
|
73
|
+
language: 'ruby'
|
82
74
|
}
|
83
|
-
|
84
|
-
reply_content, parent, ident
|
85
|
-
)
|
75
|
+
@session.send(@reply_socket, 'kernel_info_reply', content, ident)
|
86
76
|
end
|
87
77
|
|
88
|
-
def send_status(status
|
89
|
-
@session.send(@pub_socket,
|
78
|
+
def send_status(status)
|
79
|
+
@session.send(@pub_socket, 'status', execution_state: status)
|
90
80
|
end
|
91
81
|
|
92
|
-
def execute_request(ident,
|
82
|
+
def execute_request(ident, msg)
|
93
83
|
begin
|
94
|
-
code =
|
84
|
+
code = msg[:content]['code']
|
95
85
|
rescue
|
96
|
-
STDERR.puts "Got bad
|
97
|
-
STDERR.puts parent
|
86
|
+
STDERR.puts "Got bad message: #{msg.inspect}"
|
98
87
|
return
|
99
88
|
end
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
user_expressions: {},
|
110
|
-
}
|
89
|
+
@execution_count += 1 unless msg[:content].fetch('silent', false)
|
90
|
+
send_status('busy')
|
91
|
+
@session.send(@pub_socket, 'pyin', code: code)
|
92
|
+
content = {
|
93
|
+
status: 'ok',
|
94
|
+
payload: [],
|
95
|
+
user_variables: {},
|
96
|
+
user_expressions: {},
|
97
|
+
}
|
111
98
|
result = nil
|
112
99
|
begin
|
113
|
-
$displayhook.parent = parent
|
114
|
-
$stdout.parent = parent
|
115
|
-
|
116
100
|
result = TOPLEVEL_BINDING.eval(code)
|
117
101
|
rescue Exception => e
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
exc_content = {
|
124
|
-
ename: ename,
|
125
|
-
evalue: evalue,
|
126
|
-
traceback: tb,
|
127
|
-
#etype: etype,
|
128
|
-
#status: 'error',
|
102
|
+
content = {
|
103
|
+
ename: e.class.to_s,
|
104
|
+
evalue: e.message,
|
105
|
+
etype: e.class.to_s,
|
106
|
+
traceback: ["#{RED}#{e.class}#{RESET}: #{e.message}", *e.backtrace.map { |l| "#{WHITE}#{l}#{RESET}" }],
|
129
107
|
}
|
130
|
-
|
131
|
-
@session.send(@pub_socket, 'pyerr', exc_content, parent)
|
132
|
-
|
133
|
-
reply_content = exc_content
|
108
|
+
@session.send(@pub_socket, 'pyerr', content)
|
134
109
|
end
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
#@session.send(@reply_socket, ident + reply_msg)
|
141
|
-
reply_msg = @session.send(@reply_socket, 'execute_reply', reply_content, parent, ident)
|
142
|
-
if reply_msg['content']['status'] == 'error'
|
143
|
-
abort_queue
|
144
|
-
end
|
145
|
-
if ! result.nil? and ! parent['content']['silent']
|
146
|
-
output = ResponseWithMime.new(result, TOPLEVEL_BINDING.eval("$mime_type"))
|
147
|
-
$displayhook.display(output)
|
148
|
-
end
|
149
|
-
self.send_status("idle", parent)
|
110
|
+
content[:execution_count] = @execution_count
|
111
|
+
|
112
|
+
@session.send(@reply_socket, 'execute_reply', content, ident)
|
113
|
+
display(result) if result && !msg[:content]['silent']
|
114
|
+
send_status('idle')
|
150
115
|
end
|
151
116
|
|
152
|
-
def complete_request(ident,
|
153
|
-
|
154
|
-
matches: @completer.complete(
|
117
|
+
def complete_request(ident, msg)
|
118
|
+
content = {
|
119
|
+
matches: @completer.complete(msg[:content]['line'], msg[:content]['text']),
|
155
120
|
status: 'ok',
|
156
|
-
matched_text:
|
121
|
+
matched_text: msg[:content]['line'],
|
157
122
|
}
|
158
|
-
|
159
|
-
matches, parent, ident)
|
160
|
-
return nil
|
123
|
+
@session.send(@reply_socket, 'complete_reply', content, ident)
|
161
124
|
end
|
125
|
+
end
|
162
126
|
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
ident, msg = @session.recv(@reply_socket, 0)
|
167
|
-
begin
|
168
|
-
handler = @handlers[msg['header']['msg_type']]
|
169
|
-
rescue
|
170
|
-
handler = nil
|
171
|
-
end
|
172
|
-
if handler.nil?
|
173
|
-
STDERR.puts "UNKNOWN MESSAGE TYPE: #{msg['header']['msg_type']} #{msg}"
|
174
|
-
else
|
175
|
-
# STDERR.puts 'handling ' + omsg.inspect
|
176
|
-
send(handler, ident, msg)
|
177
|
-
end
|
178
|
-
end
|
179
|
-
end
|
180
|
-
|
181
|
-
private
|
182
|
-
RED = "\e[31m"
|
183
|
-
WHITE = "\e[37m"
|
184
|
-
RESET = "\e[0m"
|
185
|
-
|
186
|
-
def format_exception(name, value, backtrace)
|
187
|
-
tb = []
|
188
|
-
tb << "#{RED}#{name}#{RESET}: #{value}"
|
189
|
-
tb.concat(backtrace.map { |l| "#{WHITE}#{l}#{RESET}" })
|
190
|
-
tb
|
127
|
+
module Hooks
|
128
|
+
def display(obj, options={})
|
129
|
+
$iruby_kernel.display(obj, options)
|
191
130
|
end
|
192
131
|
end
|
193
132
|
end
|
133
|
+
|
134
|
+
include IRuby::Hooks
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module IRuby
|
2
|
+
# IO-like object that publishes to 0MQ socket.
|
3
|
+
class OStream
|
4
|
+
def initialize(session, socket, name)
|
5
|
+
@session = session
|
6
|
+
@socket = socket
|
7
|
+
@name = name
|
8
|
+
end
|
9
|
+
|
10
|
+
def close
|
11
|
+
@socket = nil
|
12
|
+
end
|
13
|
+
|
14
|
+
def flush
|
15
|
+
end
|
16
|
+
|
17
|
+
def isatty
|
18
|
+
false
|
19
|
+
end
|
20
|
+
alias tty? isatty
|
21
|
+
|
22
|
+
def read(*args)
|
23
|
+
raise IOError, 'not opened for reading'
|
24
|
+
end
|
25
|
+
alias next read
|
26
|
+
alias readline read
|
27
|
+
|
28
|
+
def write(s)
|
29
|
+
raise 'I/O operation on closed file' unless @socket
|
30
|
+
@session.send(@socket, 'stream', { name: @name, data: s.to_s })
|
31
|
+
nil
|
32
|
+
end
|
33
|
+
|
34
|
+
def puts(s)
|
35
|
+
write "#{s}\n"
|
36
|
+
end
|
37
|
+
|
38
|
+
def writelines(lines)
|
39
|
+
lines.each { |s| write(s) }
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
data/lib/iruby/session.rb
CHANGED
@@ -6,295 +6,81 @@ module IRuby
|
|
6
6
|
@username = username
|
7
7
|
@session = SecureRandom.uuid
|
8
8
|
@msg_id = 0
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
@key = key
|
13
|
-
end
|
14
|
-
|
15
|
-
# Sign a message with HMAC digest. If no auth, return b''.
|
16
|
-
|
17
|
-
# Parameters
|
18
|
-
# ----------
|
19
|
-
# msg_list : list
|
20
|
-
# The [p_header,p_parent,p_content] part of the message list.
|
21
|
-
def sign(msg_list)
|
22
|
-
return '' unless @key
|
23
|
-
hmac = OpenSSL::HMAC.new(@key, @digest)
|
24
|
-
msg_list.each do |m|
|
25
|
-
hmac.update(m)
|
9
|
+
if key && sign_scheme
|
10
|
+
raise 'Unknown signature scheme' unless sign_scheme =~ /\Ahmac-(.*)\Z/
|
11
|
+
@hmac = OpenSSL::HMAC.new(key, OpenSSL::Digest::Digest.new($1))
|
26
12
|
end
|
27
|
-
hmac.hexdigest
|
28
13
|
end
|
29
14
|
|
30
|
-
|
31
|
-
|
32
|
-
|
15
|
+
# Build and send a message
|
16
|
+
def send(socket, type, content, ident=nil)
|
17
|
+
header = {
|
18
|
+
msg_type: type,
|
19
|
+
msg_id: @msg_id,
|
33
20
|
username: @username,
|
34
|
-
session:
|
21
|
+
session: @session
|
35
22
|
}
|
36
23
|
@msg_id += 1
|
37
|
-
h
|
38
|
-
end
|
39
|
-
|
40
|
-
def msg(msg_type, content=nil, parent=nil)
|
41
|
-
msg = {}
|
42
|
-
msg['header'] = msg_header
|
43
|
-
msg['parent_header'] = parent ? parent['header'] : {}
|
44
|
-
msg['metadata'] = {}
|
45
|
-
msg['header']['msg_type'] = msg_type
|
46
|
-
msg['content'] = content || {}
|
47
|
-
return msg
|
48
|
-
end
|
49
|
-
|
50
|
-
# Build and send a message via stream or socket.
|
51
|
-
#
|
52
|
-
# The message format used by this function internally is as follows:
|
53
|
-
#
|
54
|
-
# [ident1,ident2,...,DELIM,HMAC,p_header,p_parent,p_content,
|
55
|
-
# buffer1,buffer2,...]
|
56
|
-
#
|
57
|
-
# The serialize/unserialize methods convert the nested message dict into this
|
58
|
-
# format.
|
59
|
-
#
|
60
|
-
# Parameters
|
61
|
-
# ----------
|
62
|
-
#
|
63
|
-
# stream : zmq.Socket or ZMQStream
|
64
|
-
# The socket-like object used to send the data.
|
65
|
-
# msg_or_type : str or Message/dict
|
66
|
-
# Normally, msg_or_type will be a msg_type unless a message is being
|
67
|
-
# sent more than once. If a header is supplied, this can be set to
|
68
|
-
# None and the msg_type will be pulled from the header.
|
69
|
-
#
|
70
|
-
# content : dict or None
|
71
|
-
# The content of the message (ignored if msg_or_type is a message).
|
72
|
-
# header : dict or None
|
73
|
-
# The header dict for the message (ignores if msg_to_type is a message).
|
74
|
-
# parent : Message or dict or None
|
75
|
-
# The parent or parent header describing the parent of this message
|
76
|
-
# (ignored if msg_or_type is a message).
|
77
|
-
# ident : bytes or list of bytes
|
78
|
-
# The zmq.IDENTITY routing path.
|
79
|
-
# subheader : dict or None
|
80
|
-
# Extra header keys for this message's header (ignored if msg_or_type
|
81
|
-
# is a message).
|
82
|
-
# buffers : list or None
|
83
|
-
# The already-serialized buffers to be appended to the message.
|
84
|
-
# track : bool
|
85
|
-
# Whether to track. Only for use with Sockets, because ZMQStream
|
86
|
-
# objects cannot track messages.
|
87
|
-
#
|
88
|
-
# Returns
|
89
|
-
# -------
|
90
|
-
# msg : dict
|
91
|
-
# The constructed message.
|
92
|
-
# (msg,tracker) : (dict, MessageTracker)
|
93
|
-
# if track=True, then a 2-tuple will be returned,
|
94
|
-
# the first element being the constructed
|
95
|
-
# message, and the second being the MessageTracker
|
96
|
-
def send(stream, msg_or_type, content=nil, parent=nil, ident=nil, buffers=nil, subheader=nil, track=false, header=nil)
|
97
|
-
if !stream.is_a?(ZMQ::Socket)
|
98
|
-
raise "stream must be Socket or ZMQSocket, not %r"%stream.class
|
99
|
-
end
|
100
|
-
|
101
|
-
if msg_or_type.is_a?(Hash)
|
102
|
-
msg = msg_or_type
|
103
|
-
else
|
104
|
-
msg = self.msg(msg_or_type, content, parent)
|
105
|
-
end
|
106
24
|
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
if buffers.any?
|
111
|
-
flag = ZMQ::SNDMORE
|
112
|
-
_track = false
|
113
|
-
else
|
114
|
-
_track=track
|
115
|
-
end
|
116
|
-
if track
|
117
|
-
to_send.each_with_index do |part, i|
|
118
|
-
if i == to_send.length - 1
|
119
|
-
flag = 0
|
120
|
-
else
|
121
|
-
flag = ZMQ::SNDMORE
|
122
|
-
end
|
123
|
-
stream.send_string(part, flag)
|
124
|
-
end
|
125
|
-
else
|
126
|
-
to_send.each_with_index do |part, i|
|
127
|
-
if i == to_send.length - 1
|
128
|
-
flag = 0
|
129
|
-
else
|
130
|
-
flag = ZMQ::SNDMORE
|
131
|
-
end
|
132
|
-
stream.send_string(part, flag)
|
133
|
-
end
|
25
|
+
list = serialize(header, content, ident)
|
26
|
+
list.each_with_index do |part, i|
|
27
|
+
socket.send_string(part, i == list.size - 1 ? 0 : ZMQ::SNDMORE)
|
134
28
|
end
|
135
|
-
# STDOUT.puts '-'*30
|
136
|
-
# STDOUT.puts "SENDING"
|
137
|
-
# STDOUT.puts to_send
|
138
|
-
# STDOUT.puts to_send.length
|
139
|
-
# STDOUT.puts '-'*30
|
140
|
-
|
141
|
-
#buffers.each do |b|
|
142
|
-
#stream.send(b, flag, copy=False)
|
143
|
-
#end
|
144
|
-
#if buffers:
|
145
|
-
#if track:
|
146
|
-
#tracker = stream.send(buffers[-1], copy=False, track=track)
|
147
|
-
#else:
|
148
|
-
#tracker = stream.send(buffers[-1], copy=False)
|
149
|
-
|
150
|
-
# omsg = Message(msg)
|
151
|
-
#if self.debug:
|
152
|
-
#pprint.pprint(msg)
|
153
|
-
#pprint.pprint(to_send)
|
154
|
-
#pprint.pprint(buffers)
|
155
|
-
|
156
|
-
#msg['tracker'] = tracker
|
157
|
-
|
158
|
-
return msg
|
159
29
|
end
|
160
30
|
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
frame = ""
|
172
|
-
rc = socket.recv_string(frame, mode)
|
173
|
-
ZMQ::Util.error_check("zmq_msg_send", rc)
|
174
|
-
msg << frame
|
175
|
-
rescue
|
176
|
-
end
|
31
|
+
# Receive a message and decode it
|
32
|
+
def recv(socket, mode)
|
33
|
+
msg = []
|
34
|
+
while msg.empty? || socket.more_parts?
|
35
|
+
begin
|
36
|
+
frame = ''
|
37
|
+
rc = socket.recv_string(frame, mode)
|
38
|
+
ZMQ::Util.error_check('zmq_msg_send', rc)
|
39
|
+
msg << frame
|
40
|
+
rescue
|
177
41
|
end
|
178
|
-
# Skip everything before DELIM, then munge the three json objects into the
|
179
|
-
# one the rest of my code expects
|
180
|
-
i = msg.index(DELIM)
|
181
|
-
idents = msg[0..i-1]
|
182
|
-
msg_list = msg[i+1..-1]
|
183
|
-
end
|
184
|
-
return idents, unserialize(msg_list)
|
185
|
-
end
|
186
|
-
|
187
|
-
# Serialize the message components to bytes.
|
188
|
-
#
|
189
|
-
# This is roughly the inverse of unserialize. The serialize/unserialize
|
190
|
-
# methods work with full message lists, whereas pack/unpack work with
|
191
|
-
# the individual message parts in the message list.
|
192
|
-
#
|
193
|
-
# Parameters
|
194
|
-
# ----------
|
195
|
-
# msg : dict or Message
|
196
|
-
# The nexted message dict as returned by the self.msg method.
|
197
|
-
#
|
198
|
-
# Returns
|
199
|
-
# -------
|
200
|
-
# msg_list : list
|
201
|
-
# The list of bytes objects to be sent with the format:
|
202
|
-
# [ident1,ident2,...,DELIM,HMAC,p_header,p_parent,p_content,
|
203
|
-
# buffer1,buffer2,...]. In this list, the p_* entities are
|
204
|
-
# the packed or serialized versions, so if JSON is used, these
|
205
|
-
# are utf8 encoded JSON strings.
|
206
|
-
def serialize(msg, ident=nil)
|
207
|
-
content = msg.fetch('content', {})
|
208
|
-
if content.nil?
|
209
|
-
content = {}.to_json
|
210
|
-
elsif content.is_a?(Hash)
|
211
|
-
content = content.to_json
|
212
|
-
#elsif isinstance(content, bytes):
|
213
|
-
# content is already packed, as in a relayed message
|
214
|
-
#pass
|
215
|
-
#elsif isinstance(content, unicode):
|
216
|
-
# should be bytes, but JSON often spits out unicode
|
217
|
-
#content = content.encode('utf8')
|
218
|
-
else
|
219
|
-
raise "Content incorrect type: %s"%type(content)
|
220
|
-
end
|
221
|
-
|
222
|
-
real_message = [MultiJson.dump(msg['header']),
|
223
|
-
MultiJson.dump(msg['parent_header']),
|
224
|
-
MultiJson.dump(msg['metadata']),
|
225
|
-
MultiJson.dump(msg['content']),
|
226
|
-
]
|
227
|
-
|
228
|
-
to_send = []
|
229
|
-
|
230
|
-
if ident.is_a?(Array)
|
231
|
-
# accept list of idents
|
232
|
-
to_send += ident
|
233
|
-
elsif !ident.nil?
|
234
|
-
to_send << ident
|
235
42
|
end
|
236
|
-
to_send << DELIM
|
237
43
|
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
# STDOUT.puts to_send.length
|
244
|
-
|
245
|
-
return to_send
|
44
|
+
i = msg.index(DELIM)
|
45
|
+
idents, msg_list = msg[0..i-1], msg[i+1..-1]
|
46
|
+
msg = unserialize(msg_list)
|
47
|
+
@last_received_header = msg[:header]
|
48
|
+
return idents, msg
|
246
49
|
end
|
247
50
|
|
248
|
-
|
249
|
-
# This is roughly the inverse of serialize. The serialize/unserialize
|
250
|
-
# methods work with full message lists, whereas pack/unpack work with
|
251
|
-
# the individual message parts in the message list.
|
51
|
+
private
|
252
52
|
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
# (False).
|
261
|
-
# copy : bool (True)
|
262
|
-
# Whether to return the bytes (True), or the non-copying Message
|
263
|
-
# object in each place (False).
|
53
|
+
def serialize(header, content, ident)
|
54
|
+
msg = [MultiJson.dump(header),
|
55
|
+
MultiJson.dump(@last_received_header || {}),
|
56
|
+
'{}',
|
57
|
+
MultiJson.dump(content || {})]
|
58
|
+
([ident].flatten.compact << DELIM << sign(msg)) + msg
|
59
|
+
end
|
264
60
|
|
265
|
-
|
266
|
-
# -------
|
267
|
-
# msg : dict
|
268
|
-
# The nested message dict with top-level keys [header, parent_header,
|
269
|
-
# content, buffers].
|
270
|
-
def unserialize(msg_list, content=true, copy=true)
|
61
|
+
def unserialize(msg_list)
|
271
62
|
minlen = 5
|
272
|
-
message
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
63
|
+
raise 'malformed message, must have at least #{minlen} elements' unless msg_list.length >= minlen
|
64
|
+
s, header, parent_header, metadata, content, buffers = *msg_list
|
65
|
+
raise 'Invalid signature' unless s == sign(msg_list[1..-1])
|
66
|
+
{
|
67
|
+
header: MultiJson.load(header),
|
68
|
+
parent_header: MultiJson.load(parent_header),
|
69
|
+
metadata: MultiJson.load(metadata),
|
70
|
+
content: MultiJson.load(content),
|
71
|
+
buffers: buffers
|
72
|
+
}
|
73
|
+
end
|
283
74
|
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
if content
|
291
|
-
message['content'] = MultiJson.load(msg_list[4])
|
75
|
+
# Sign using HMAC
|
76
|
+
def sign(list)
|
77
|
+
if @hmac
|
78
|
+
@hmac.reset
|
79
|
+
list.each {|m| @hmac.update(m) }
|
80
|
+
@hmac.hexdigest
|
292
81
|
else
|
293
|
-
|
82
|
+
''
|
294
83
|
end
|
295
|
-
|
296
|
-
message['buffers'] = msg_list[4..-1]
|
297
|
-
return message
|
298
84
|
end
|
299
85
|
end
|
300
86
|
end
|
data/lib/iruby/version.rb
CHANGED
Binary file
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: iruby
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 0.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Damián Silvani
|
@@ -85,14 +85,16 @@ files:
|
|
85
85
|
- Rakefile
|
86
86
|
- Ruby Notebook.ipynb
|
87
87
|
- attic/output/html.rb
|
88
|
+
- attic/test/helper.rb
|
89
|
+
- attic/test/html_test.rb
|
90
|
+
- attic/test/output_maps_test.rb
|
88
91
|
- bin/iruby
|
89
92
|
- iruby.gemspec
|
90
93
|
- lib/iruby.rb
|
91
94
|
- lib/iruby/command.rb
|
92
95
|
- lib/iruby/completer.rb
|
93
|
-
- lib/iruby/display_hook.rb
|
94
96
|
- lib/iruby/kernel.rb
|
95
|
-
- lib/iruby/
|
97
|
+
- lib/iruby/ostream.rb
|
96
98
|
- lib/iruby/session.rb
|
97
99
|
- lib/iruby/version.rb
|
98
100
|
- static/base/images/favicon.ico
|
@@ -101,9 +103,6 @@ files:
|
|
101
103
|
- static/base/images/src/ruby.svg
|
102
104
|
- static/custom/custom.css
|
103
105
|
- static/custom/custom.js
|
104
|
-
- test/helper.rb
|
105
|
-
- test/html_test.rb
|
106
|
-
- test/output_maps_test.rb
|
107
106
|
homepage: https://github.com/minad/iruby
|
108
107
|
licenses:
|
109
108
|
- MIT
|
@@ -124,11 +123,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
124
123
|
version: '0'
|
125
124
|
requirements: []
|
126
125
|
rubyforge_project:
|
127
|
-
rubygems_version: 2.0.
|
126
|
+
rubygems_version: 2.0.3
|
128
127
|
signing_key:
|
129
128
|
specification_version: 4
|
130
129
|
summary: A Ruby kernel for IPython frontends (notebook console, etc.)
|
131
|
-
test_files:
|
132
|
-
- test/helper.rb
|
133
|
-
- test/html_test.rb
|
134
|
-
- test/output_maps_test.rb
|
130
|
+
test_files: []
|
data/lib/iruby/display_hook.rb
DELETED
@@ -1,29 +0,0 @@
|
|
1
|
-
module IRuby
|
2
|
-
class DisplayHook
|
3
|
-
attr_writer :parent
|
4
|
-
|
5
|
-
def initialize kernel, session, pub_socket
|
6
|
-
@kernel = kernel
|
7
|
-
@session = session
|
8
|
-
@pub_socket = pub_socket
|
9
|
-
@parent = {}
|
10
|
-
end
|
11
|
-
|
12
|
-
def display(obj)
|
13
|
-
if obj.nil?
|
14
|
-
return
|
15
|
-
end
|
16
|
-
# STDERR.puts @kernel.user_ns
|
17
|
-
# @user_ns._ = obj
|
18
|
-
# STDERR.puts "displayhook call:"
|
19
|
-
# STDERR.puts @parent_header.inspect
|
20
|
-
#@pub_socket.send(msg.to_json)
|
21
|
-
data = {}
|
22
|
-
output = obj
|
23
|
-
data['text/plain'] = output
|
24
|
-
data[obj.mime] = output
|
25
|
-
content = {data: data, metadata: {}, execution_count: @kernel.execution_count}
|
26
|
-
@session.send(@pub_socket, 'pyout', content, @parent)
|
27
|
-
end
|
28
|
-
end
|
29
|
-
end
|
data/lib/iruby/out_stream.rb
DELETED
@@ -1,93 +0,0 @@
|
|
1
|
-
module IRuby
|
2
|
-
class OutStream
|
3
|
-
#A file like object that publishes the stream to a 0MQ PUB socket.
|
4
|
-
|
5
|
-
attr_writer :parent
|
6
|
-
|
7
|
-
def initialize session, pub_socket, name, max_buffer=200
|
8
|
-
@session = session
|
9
|
-
@pub_socket = pub_socket
|
10
|
-
@name = name
|
11
|
-
@buffer = []
|
12
|
-
@buffer_len = 0
|
13
|
-
@max_buffer = max_buffer
|
14
|
-
@parent = {}
|
15
|
-
end
|
16
|
-
|
17
|
-
def close
|
18
|
-
@pub_socket = nil
|
19
|
-
end
|
20
|
-
|
21
|
-
def flush
|
22
|
-
# STDERR.puts("flushing, parent to follow")
|
23
|
-
# STDERR.puts @parent.inspect
|
24
|
-
if @pub_socket.nil?
|
25
|
-
raise 'I/O operation on closed file'
|
26
|
-
else
|
27
|
-
if @buffer
|
28
|
-
data = @buffer.join('')
|
29
|
-
content = { name: @name, data: data }
|
30
|
-
if ! @session
|
31
|
-
return
|
32
|
-
end
|
33
|
-
# msg = @session.msg('stream', content, @parent) if @session
|
34
|
-
# FIXME: Wha?
|
35
|
-
# STDERR.puts msg.to_json
|
36
|
-
#@pub_socket.send(msg.to_json)
|
37
|
-
@session.send(@pub_socket, 'stream', content, @parent)
|
38
|
-
@buffer_len = 0
|
39
|
-
@buffer = []
|
40
|
-
nil
|
41
|
-
end
|
42
|
-
end
|
43
|
-
end
|
44
|
-
|
45
|
-
def isatty
|
46
|
-
false
|
47
|
-
end
|
48
|
-
alias tty? isatty
|
49
|
-
|
50
|
-
def next
|
51
|
-
raise IOError, 'not opened for reading'
|
52
|
-
end
|
53
|
-
|
54
|
-
def read(*args)
|
55
|
-
raise IOError, 'not opened for reading'
|
56
|
-
end
|
57
|
-
alias readline read
|
58
|
-
|
59
|
-
def write(s)
|
60
|
-
if @pub_socket.nil?
|
61
|
-
raise 'I/O operation on closed file'
|
62
|
-
else
|
63
|
-
s = s.to_s
|
64
|
-
@buffer << s
|
65
|
-
@buffer_len += s.length
|
66
|
-
_maybe_send
|
67
|
-
end
|
68
|
-
end
|
69
|
-
|
70
|
-
def puts(s)
|
71
|
-
write "#{s}\n"
|
72
|
-
end
|
73
|
-
|
74
|
-
def _maybe_send
|
75
|
-
#if self._buffer[-1].include?('\n')
|
76
|
-
flush
|
77
|
-
#end
|
78
|
-
#if @bufferlen > @max_buffer
|
79
|
-
#flush
|
80
|
-
#end
|
81
|
-
end
|
82
|
-
|
83
|
-
def writelines sequence
|
84
|
-
if @pub_socket.nil?
|
85
|
-
raise 'I/O operation on closed file'
|
86
|
-
else
|
87
|
-
sequence.each do |s|
|
88
|
-
write(s)
|
89
|
-
end
|
90
|
-
end
|
91
|
-
end
|
92
|
-
end
|
93
|
-
end
|