iruby 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +19 -0
- data/Gemfile +2 -0
- data/LICENSE +22 -0
- data/README.md +70 -0
- data/Rakefile +13 -0
- data/Ruby Notebook.ipynb +180 -0
- data/attic/output/html.rb +217 -0
- data/bin/iruby +6 -0
- data/iruby.gemspec +26 -0
- data/lib/iruby.rb +11 -0
- data/lib/iruby/command.rb +103 -0
- data/lib/iruby/completer.rb +19 -0
- data/lib/iruby/display_hook.rb +29 -0
- data/lib/iruby/kernel.rb +193 -0
- data/lib/iruby/out_stream.rb +93 -0
- data/lib/iruby/session.rb +300 -0
- data/lib/iruby/version.rb +3 -0
- data/static/base/images/favicon.ico +0 -0
- data/static/base/images/ipynblogo.png +0 -0
- data/static/base/images/src/ipynblogo.svg +768 -0
- data/static/base/images/src/ruby.svg +948 -0
- data/static/custom/custom.css +40 -0
- data/static/custom/custom.js +2 -0
- data/test/helper.rb +5 -0
- data/test/html_test.rb +17 -0
- data/test/output_maps_test.rb +110 -0
- metadata +134 -0
data/bin/iruby
ADDED
data/iruby.gemspec
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
require File.dirname(__FILE__) + '/lib/iruby/version'
|
3
|
+
require 'date'
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = 'iruby'
|
7
|
+
s.date = Date.today.to_s
|
8
|
+
s.version = IRuby::VERSION
|
9
|
+
s.authors = ['Damián Silvani', 'Min RK', 'martin sarsale', 'Josh Adams', 'Daniel Mendler']
|
10
|
+
s.email = ['benjaminrk@gmail.com']
|
11
|
+
s.description = 'Ruby Kernel for IPython'
|
12
|
+
s.summary = 'A Ruby kernel for IPython frontends (notebook console, etc.)'
|
13
|
+
s.homepage = 'https://github.com/minad/iruby'
|
14
|
+
s.license = 'MIT'
|
15
|
+
|
16
|
+
s.files = `git ls-files`.split($/)
|
17
|
+
s.executables = s.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
s.test_files = s.files.grep(%r{^test/})
|
19
|
+
s.require_paths = %w(lib)
|
20
|
+
|
21
|
+
s.add_development_dependency 'rake'
|
22
|
+
|
23
|
+
s.add_runtime_dependency 'bond'
|
24
|
+
s.add_runtime_dependency 'ffi-rzmq'
|
25
|
+
s.add_runtime_dependency 'multi_json'
|
26
|
+
end
|
data/lib/iruby.rb
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
require 'ffi-rzmq'
|
2
|
+
require 'securerandom'
|
3
|
+
require 'multi_json'
|
4
|
+
require 'bond'
|
5
|
+
require 'openssl'
|
6
|
+
require 'iruby/version'
|
7
|
+
require 'iruby/kernel'
|
8
|
+
require 'iruby/completer'
|
9
|
+
require 'iruby/session'
|
10
|
+
require 'iruby/out_stream'
|
11
|
+
require 'iruby/display_hook'
|
@@ -0,0 +1,103 @@
|
|
1
|
+
require 'shellwords'
|
2
|
+
|
3
|
+
module IRuby
|
4
|
+
class Command
|
5
|
+
IRUBYDIR = '~/.config/iruby'
|
6
|
+
|
7
|
+
def initialize(args)
|
8
|
+
@args = args
|
9
|
+
end
|
10
|
+
|
11
|
+
def run
|
12
|
+
raise 'Use --iruby-dir instead of --ipython-dir!' unless @args.grep(/\A--ipython-dir=.*\Z/).empty?
|
13
|
+
|
14
|
+
if @args.first == 'kernel'
|
15
|
+
run_kernel
|
16
|
+
else
|
17
|
+
run_ipython
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def run_kernel
|
24
|
+
config_file, boot_file, working_dir = @args[1..-1]
|
25
|
+
Dir.chdir(working_dir) if working_dir
|
26
|
+
require boot_file if boot_file
|
27
|
+
require 'iruby'
|
28
|
+
|
29
|
+
config = MultiJson.load(File.read(config_file))
|
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
|
61
|
+
end
|
62
|
+
|
63
|
+
def run_ipython
|
64
|
+
dir = @args.grep(/\A--iruby-dir=.*\Z/)
|
65
|
+
@args -= dir
|
66
|
+
dir = dir.last.to_s.sub(/\A--profile=/, '')
|
67
|
+
dir = ENV['IRUBYDIR'] || IRUBYDIR if dir.empty?
|
68
|
+
dir = File.expand_path(dir)
|
69
|
+
ENV['IPYTHONDIR'] = dir
|
70
|
+
|
71
|
+
if @args.size == 3 && @args[0] == 'profile' && @args[1] == 'create'
|
72
|
+
profile = @args[2]
|
73
|
+
else
|
74
|
+
profile = @args.grep(/\A--profile=.*\Z/).last.to_s.sub(/\A--profile=/, '')
|
75
|
+
profile = 'default' if profile.empty?
|
76
|
+
end
|
77
|
+
|
78
|
+
create_profile(dir, profile)
|
79
|
+
Kernel.exec('ipython', *@args)
|
80
|
+
end
|
81
|
+
|
82
|
+
def create_profile(dir, profile)
|
83
|
+
profile_dir = File.join(dir, "profile_#{profile}")
|
84
|
+
unless File.directory?(profile_dir)
|
85
|
+
puts "Creating profile directory #{profile_dir}"
|
86
|
+
system("ipython profile create #{Shellwords.escape profile}")
|
87
|
+
end
|
88
|
+
|
89
|
+
kernel_cmd = "c.KernelManager.kernel_cmd = ['#{File.expand_path $0}', 'kernel', '{connection_file}']"
|
90
|
+
Dir[File.join(profile_dir, '*_config.py')].each do |path|
|
91
|
+
File.open(path, 'r+') do |f|
|
92
|
+
content = f.read
|
93
|
+
content << kernel_cmd unless content.gsub!(/^c\.KernelManager\.kernel_cmd.*$/, kernel_cmd)
|
94
|
+
f.pos = 0
|
95
|
+
f.write(content)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
static_dir = File.join(profile_dir, 'static')
|
100
|
+
File.symlink(File.join(File.dirname(__FILE__), '..', '..', 'static'), static_dir) unless File.exists?(static_dir)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module IRuby
|
2
|
+
class Completer
|
3
|
+
module Readline
|
4
|
+
class<< self
|
5
|
+
attr_accessor :line_buffer
|
6
|
+
def setup(arg); end
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
Bond.start(readline: Readline, debug: true)
|
12
|
+
end
|
13
|
+
|
14
|
+
def complete(line, text)
|
15
|
+
Readline.line_buffer = line
|
16
|
+
Bond.agent.call(line)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,29 @@
|
|
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/kernel.rb
ADDED
@@ -0,0 +1,193 @@
|
|
1
|
+
module IRuby
|
2
|
+
|
3
|
+
class ResponseWithMime
|
4
|
+
def initialize(response, mime)
|
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
|
16
|
+
|
17
|
+
class Kernel
|
18
|
+
def self.html(s)
|
19
|
+
@output_mime = "text/html"
|
20
|
+
s
|
21
|
+
end
|
22
|
+
|
23
|
+
def execution_count
|
24
|
+
@execution_count
|
25
|
+
end
|
26
|
+
|
27
|
+
def output_mime
|
28
|
+
@output_mime
|
29
|
+
end
|
30
|
+
|
31
|
+
def initialize session, reply_socket, pub_socket
|
32
|
+
@session = session
|
33
|
+
@reply_socket = reply_socket
|
34
|
+
@pub_socket = pub_socket
|
35
|
+
@history = []
|
36
|
+
@execution_count = 0
|
37
|
+
@completer = Completer.new
|
38
|
+
|
39
|
+
# Build dict of handlers for message types
|
40
|
+
@handlers = {}
|
41
|
+
['execute_request', 'complete_request', 'kernel_info_request'].each do |msg_type|
|
42
|
+
@handlers[msg_type] = msg_type
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def abort_queue
|
47
|
+
while true
|
48
|
+
#begin
|
49
|
+
ident = @reply_socket.recv(ZMQ::NOBLOCK)
|
50
|
+
#rescue Exception => e
|
51
|
+
#if e.errno == ZMQ::EAGAIN
|
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)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def kernel_info_request(ident, parent)
|
71
|
+
reply_content = {
|
72
|
+
protocol_version: [4, 0],
|
73
|
+
|
74
|
+
# Language version number (mandatory).
|
75
|
+
# It is Python version number (e.g., [2, 7, 3]) for the kernel
|
76
|
+
# included in IPython.
|
77
|
+
language_version: RUBY_VERSION.split('.').map { |x| x.to_i },
|
78
|
+
|
79
|
+
# Programming language in which kernel is implemented (mandatory).
|
80
|
+
# Kernel included in IPython returns 'python'.
|
81
|
+
language: "ruby"
|
82
|
+
}
|
83
|
+
reply_msg = @session.send(@reply_socket, 'kernel_info_reply',
|
84
|
+
reply_content, parent, ident
|
85
|
+
)
|
86
|
+
end
|
87
|
+
|
88
|
+
def send_status(status, parent)
|
89
|
+
@session.send(@pub_socket, "status", {execution_state: status}, parent)
|
90
|
+
end
|
91
|
+
|
92
|
+
def execute_request(ident, parent)
|
93
|
+
begin
|
94
|
+
code = parent['content']['code']
|
95
|
+
rescue
|
96
|
+
STDERR.puts "Got bad msg: "
|
97
|
+
STDERR.puts parent
|
98
|
+
return
|
99
|
+
end
|
100
|
+
# pyin_msg = @session.msg()
|
101
|
+
if ! parent['content'].fetch('silent', false)
|
102
|
+
@execution_count += 1
|
103
|
+
end
|
104
|
+
self.send_status("busy", parent)
|
105
|
+
@session.send(@pub_socket, 'pyin', {code: code}, parent)
|
106
|
+
reply_content = {status: 'ok',
|
107
|
+
payload: [],
|
108
|
+
user_variables: {},
|
109
|
+
user_expressions: {},
|
110
|
+
}
|
111
|
+
result = nil
|
112
|
+
begin
|
113
|
+
$displayhook.parent = parent
|
114
|
+
$stdout.parent = parent
|
115
|
+
|
116
|
+
result = TOPLEVEL_BINDING.eval(code)
|
117
|
+
rescue Exception => e
|
118
|
+
# $stderr.puts e.inspect
|
119
|
+
#etype, evalue, tb = sys.exc_info()
|
120
|
+
ename, evalue, tb = e.class.to_s, e.message, e.backtrace
|
121
|
+
tb = format_exception(ename, evalue, tb)
|
122
|
+
#tb = "1, 2, 3"
|
123
|
+
exc_content = {
|
124
|
+
ename: ename,
|
125
|
+
evalue: evalue,
|
126
|
+
traceback: tb,
|
127
|
+
#etype: etype,
|
128
|
+
#status: 'error',
|
129
|
+
}
|
130
|
+
# STDERR.puts exc_content
|
131
|
+
@session.send(@pub_socket, 'pyerr', exc_content, parent)
|
132
|
+
|
133
|
+
reply_content = exc_content
|
134
|
+
end
|
135
|
+
reply_content['execution_count'] = @execution_count
|
136
|
+
|
137
|
+
# reply_msg = @session.msg('execute_reply', reply_content, parent)
|
138
|
+
#$stdout.puts reply_msg
|
139
|
+
#$stderr.puts reply_msg
|
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)
|
150
|
+
end
|
151
|
+
|
152
|
+
def complete_request(ident, parent)
|
153
|
+
matches = {
|
154
|
+
matches: @completer.complete(parent['content']['line'], parent['content']['text']),
|
155
|
+
status: 'ok',
|
156
|
+
matched_text: parent['content']['line'],
|
157
|
+
}
|
158
|
+
completion_msg = @session.send(@reply_socket, 'complete_reply',
|
159
|
+
matches, parent, ident)
|
160
|
+
return nil
|
161
|
+
end
|
162
|
+
|
163
|
+
def start()
|
164
|
+
self.send_status("starting", nil)
|
165
|
+
while true
|
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
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
@@ -0,0 +1,93 @@
|
|
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
|