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.
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $:.unshift File.dirname(__FILE__) + '/../lib'
4
+ require 'iruby/command'
5
+
6
+ IRuby::Command.new(ARGV).run
@@ -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
@@ -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
@@ -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