iruby 0.2.8 → 0.6.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.github/workflows/ubuntu.yml +62 -0
- data/CHANGES +76 -0
- data/Gemfile +3 -1
- data/LICENSE +1 -1
- data/README.md +130 -82
- data/Rakefile +36 -10
- data/ci/Dockerfile.base.erb +41 -0
- data/ci/Dockerfile.main.erb +7 -0
- data/ci/requirements.txt +1 -0
- data/docker/setup.sh +15 -0
- data/docker/test.sh +7 -0
- data/iruby.gemspec +13 -17
- data/lib/iruby.rb +14 -6
- data/lib/iruby/backend.rb +41 -7
- data/lib/iruby/command.rb +68 -12
- data/lib/iruby/display.rb +80 -39
- data/lib/iruby/event_manager.rb +40 -0
- data/lib/iruby/formatter.rb +5 -4
- data/lib/iruby/input.rb +41 -0
- data/lib/iruby/input/README.ipynb +502 -0
- data/lib/iruby/input/README.md +299 -0
- data/lib/iruby/input/autoload.rb +25 -0
- data/lib/iruby/input/builder.rb +67 -0
- data/lib/iruby/input/button.rb +47 -0
- data/lib/iruby/input/cancel.rb +32 -0
- data/lib/iruby/input/checkbox.rb +74 -0
- data/lib/iruby/input/date.rb +37 -0
- data/lib/iruby/input/field.rb +31 -0
- data/lib/iruby/input/file.rb +57 -0
- data/lib/iruby/input/form.rb +77 -0
- data/lib/iruby/input/label.rb +27 -0
- data/lib/iruby/input/multiple.rb +76 -0
- data/lib/iruby/input/popup.rb +41 -0
- data/lib/iruby/input/radio.rb +59 -0
- data/lib/iruby/input/select.rb +59 -0
- data/lib/iruby/input/textarea.rb +23 -0
- data/lib/iruby/input/widget.rb +34 -0
- data/lib/iruby/jupyter.rb +77 -0
- data/lib/iruby/kernel.rb +106 -27
- data/lib/iruby/ostream.rb +29 -8
- data/lib/iruby/session.rb +116 -0
- data/lib/iruby/session/{rbczmq.rb → cztop.rb} +25 -13
- data/lib/iruby/session/ffi_rzmq.rb +15 -2
- data/lib/iruby/session_adapter.rb +72 -0
- data/lib/iruby/session_adapter/cztop_adapter.rb +45 -0
- data/lib/iruby/session_adapter/ffirzmq_adapter.rb +55 -0
- data/lib/iruby/session_adapter/pyzmq_adapter.rb +77 -0
- data/lib/iruby/session_adapter/test_adapter.rb +49 -0
- data/lib/iruby/utils.rb +5 -2
- data/lib/iruby/version.rb +1 -1
- data/run-test.sh +12 -0
- data/tasks/ci.rake +65 -0
- data/test/helper.rb +133 -0
- data/test/integration_test.rb +22 -11
- data/test/iruby/backend_test.rb +37 -0
- data/test/iruby/command_test.rb +207 -0
- data/test/iruby/event_manager_test.rb +92 -0
- data/test/iruby/jupyter_test.rb +27 -0
- data/test/iruby/kernel_test.rb +153 -0
- data/test/iruby/mime_test.rb +43 -0
- data/test/iruby/multi_logger_test.rb +1 -2
- data/test/iruby/session_adapter/cztop_adapter_test.rb +20 -0
- data/test/iruby/session_adapter/ffirzmq_adapter_test.rb +20 -0
- data/test/iruby/session_adapter/session_adapter_test_base.rb +27 -0
- data/test/iruby/session_adapter_test.rb +91 -0
- data/test/iruby/session_test.rb +48 -0
- data/test/run-test.rb +19 -0
- metadata +132 -43
- data/.travis.yml +0 -16
- data/CONTRIBUTORS +0 -19
- data/test/test_helper.rb +0 -5
@@ -0,0 +1,59 @@
|
|
1
|
+
module IRuby
|
2
|
+
module Input
|
3
|
+
class Select < Label
|
4
|
+
needs :options, :default
|
5
|
+
|
6
|
+
builder :select do |*args, **params|
|
7
|
+
key = :select
|
8
|
+
key, *args = args if args.first.is_a? Symbol
|
9
|
+
|
10
|
+
params[:key] = unique_key(key)
|
11
|
+
params[:options] = args
|
12
|
+
params[:default] ||= false
|
13
|
+
|
14
|
+
unless params[:options].include? params[:default]
|
15
|
+
params[:options] = [nil, *params[:options].compact]
|
16
|
+
end
|
17
|
+
|
18
|
+
add_field Select.new(**params)
|
19
|
+
end
|
20
|
+
|
21
|
+
def widget_css
|
22
|
+
<<-CSS
|
23
|
+
.iruby-select {
|
24
|
+
min-width: 25%;
|
25
|
+
margin-left: 0 !important;
|
26
|
+
}
|
27
|
+
CSS
|
28
|
+
end
|
29
|
+
|
30
|
+
def widget_js
|
31
|
+
<<-JS
|
32
|
+
$('.iruby-select').change(function(){
|
33
|
+
$(this).data('iruby-value',
|
34
|
+
$(this).find('option:selected').text()
|
35
|
+
);
|
36
|
+
});
|
37
|
+
JS
|
38
|
+
end
|
39
|
+
|
40
|
+
def widget_html
|
41
|
+
widget_label do
|
42
|
+
div class: 'form-control' do
|
43
|
+
params = {
|
44
|
+
class: 'iruby-select',
|
45
|
+
:'data-iruby-key' => @key,
|
46
|
+
:'data-iruby-value' => @default
|
47
|
+
}
|
48
|
+
|
49
|
+
select **params do
|
50
|
+
@options.each do |o|
|
51
|
+
option o, selected: @default == o
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module IRuby
|
2
|
+
module Input
|
3
|
+
class Textarea < Field
|
4
|
+
needs rows: 5
|
5
|
+
|
6
|
+
builder :textarea do |key='textarea', **params|
|
7
|
+
params[:key] = unique_key key
|
8
|
+
add_field Textarea.new(**params)
|
9
|
+
end
|
10
|
+
|
11
|
+
def widget_html
|
12
|
+
widget_label do
|
13
|
+
textarea(
|
14
|
+
@default,
|
15
|
+
rows: @rows,
|
16
|
+
:'data-iruby-key' => @key,
|
17
|
+
class: 'form-control iruby-field'
|
18
|
+
)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module IRuby
|
2
|
+
module Input
|
3
|
+
class Widget < Erector::Widget
|
4
|
+
needs key: nil
|
5
|
+
|
6
|
+
def widget_js; end
|
7
|
+
def widget_css; end
|
8
|
+
def widget_html; end
|
9
|
+
def content; widget_html; end
|
10
|
+
|
11
|
+
def self.builder method, &block
|
12
|
+
Builder.instance_eval do
|
13
|
+
define_method method, &block
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def widget_join method, *args
|
18
|
+
strings = args.map do |arg|
|
19
|
+
arg.is_a?(String) ? arg : arg.send(method)
|
20
|
+
end
|
21
|
+
strings.uniq.join("\n")
|
22
|
+
end
|
23
|
+
|
24
|
+
def widget_display
|
25
|
+
IRuby.display(IRuby.html(
|
26
|
+
Erector.inline{ style raw(widget_css) }.to_html
|
27
|
+
))
|
28
|
+
|
29
|
+
IRuby.display(IRuby.html(to_html))
|
30
|
+
IRuby.display(IRuby.javascript(widget_js))
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
module IRuby
|
2
|
+
module Jupyter
|
3
|
+
class << self
|
4
|
+
# User's default kernelspec directory is described here:
|
5
|
+
# https://jupyter.readthedocs.io/en/latest/projects/jupyter-directories.html
|
6
|
+
def default_data_dir
|
7
|
+
case
|
8
|
+
when windows?
|
9
|
+
appdata = windows_user_appdata
|
10
|
+
if !appdata.empty?
|
11
|
+
File.join(appdata, 'jupyter')
|
12
|
+
else
|
13
|
+
jupyter_config_dir = ENV.fetch('JUPYTER_CONFIG_DIR', File.expand_path('~/.jupyter'))
|
14
|
+
File.join(jupyter_config_dir, 'data')
|
15
|
+
end
|
16
|
+
when apple?
|
17
|
+
File.expand_path('~/Library/Jupyter')
|
18
|
+
else
|
19
|
+
xdg_data_home = ENV.fetch('XDG_DATA_HOME', '')
|
20
|
+
data_home = xdg_data_home[0] ? xdg_data_home : File.expand_path('~/.local/share')
|
21
|
+
File.join(data_home, 'jupyter')
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def kernelspec_dir(data_dir=nil)
|
26
|
+
data_dir ||= default_data_dir
|
27
|
+
File.join(data_dir, 'kernels')
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
# returns %APPDATA%
|
33
|
+
def windows_user_appdata
|
34
|
+
require 'fiddle/import'
|
35
|
+
check_windows
|
36
|
+
path = Fiddle::Pointer.malloc(2 * 300) # uint16_t[300]
|
37
|
+
csidl_appdata = 0x001a
|
38
|
+
case call_SHGetFolderPathW(Fiddle::NULL, csidl_appdata, Fiddle::NULL, 0, path)
|
39
|
+
when 0
|
40
|
+
len = (1 ... (path.size/2)).find {|i| path[2*i, 2] == "\0\0" }
|
41
|
+
path = path.to_str(2*len).encode(Encoding::UTF_8, Encoding::UTF_16LE)
|
42
|
+
else
|
43
|
+
ENV.fetch('APPDATA', '')
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def call_SHGetFolderPathW(hwnd, csidl, hToken, dwFlags, pszPath)
|
48
|
+
require 'fiddle/import'
|
49
|
+
shell32 = Fiddle::Handle.new('shell32')
|
50
|
+
func = Fiddle::Function.new(
|
51
|
+
shell32['SHGetFolderPathW'],
|
52
|
+
[
|
53
|
+
Fiddle::TYPE_VOIDP,
|
54
|
+
Fiddle::TYPE_INT,
|
55
|
+
Fiddle::TYPE_VOIDP,
|
56
|
+
Fiddle::TYPE_INT,
|
57
|
+
Fiddle::TYPE_VOIDP
|
58
|
+
],
|
59
|
+
Fiddle::TYPE_INT,
|
60
|
+
Fiddle::Importer.const_get(:CALL_TYPE_TO_ABI)[:stdcall])
|
61
|
+
func.(hwnd, csidl, hToken, dwFlags, pszPath)
|
62
|
+
end
|
63
|
+
|
64
|
+
def check_windows
|
65
|
+
raise 'the current platform is not Windows' unless windows?
|
66
|
+
end
|
67
|
+
|
68
|
+
def windows?
|
69
|
+
/mingw|mswin/ =~ RUBY_PLATFORM
|
70
|
+
end
|
71
|
+
|
72
|
+
def apple?
|
73
|
+
/darwin/ =~ RUBY_PLATFORM
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
data/lib/iruby/kernel.rb
CHANGED
@@ -1,29 +1,47 @@
|
|
1
1
|
module IRuby
|
2
|
+
ExecutionInfo = Struct.new(:raw_cell, :store_history, :silent)
|
3
|
+
|
2
4
|
class Kernel
|
3
5
|
RED = "\e[31m"
|
4
|
-
WHITE = "\e[37m"
|
5
6
|
RESET = "\e[0m"
|
6
7
|
|
7
|
-
|
8
|
+
@events = EventManager.new([:initialized])
|
9
|
+
|
10
|
+
class << self
|
11
|
+
attr_reader :events
|
8
12
|
attr_accessor :instance
|
9
13
|
end
|
10
14
|
|
11
15
|
attr_reader :session
|
12
16
|
|
13
|
-
|
17
|
+
EVENTS = [
|
18
|
+
:pre_execute,
|
19
|
+
:pre_run_cell,
|
20
|
+
:post_run_cell,
|
21
|
+
:post_execute
|
22
|
+
].freeze
|
23
|
+
|
24
|
+
def initialize(config_file, session_adapter_name=nil)
|
14
25
|
@config = MultiJson.load(File.read(config_file))
|
15
26
|
IRuby.logger.debug("IRuby kernel start with config #{@config}")
|
16
27
|
Kernel.instance = self
|
17
28
|
|
18
|
-
@session = Session.new(@config)
|
29
|
+
@session = Session.new(@config, session_adapter_name)
|
19
30
|
$stdout = OStream.new(@session, :stdout)
|
20
31
|
$stderr = OStream.new(@session, :stderr)
|
21
32
|
|
33
|
+
init_parent_process_poller
|
34
|
+
|
35
|
+
@events = EventManager.new(EVENTS)
|
22
36
|
@execution_count = 0
|
23
37
|
@backend = create_backend
|
24
38
|
@running = true
|
39
|
+
|
40
|
+
self.class.events.trigger(:initialized, self)
|
25
41
|
end
|
26
42
|
|
43
|
+
attr_reader :events
|
44
|
+
|
27
45
|
def create_backend
|
28
46
|
PryBackend.new
|
29
47
|
rescue Exception => e
|
@@ -40,6 +58,7 @@ module IRuby
|
|
40
58
|
|
41
59
|
def dispatch
|
42
60
|
msg = @session.recv(:reply)
|
61
|
+
IRuby.logger.debug "Kernel#dispatch: msg = #{msg}"
|
43
62
|
type = msg[:header]['msg_type']
|
44
63
|
raise "Unknown message type: #{msg.inspect}" unless type =~ /comm_|_request\Z/ && respond_to?(type)
|
45
64
|
begin
|
@@ -50,31 +69,51 @@ module IRuby
|
|
50
69
|
end
|
51
70
|
rescue Exception => e
|
52
71
|
IRuby.logger.debug "Kernel error: #{e.message}\n#{e.backtrace.join("\n")}"
|
53
|
-
@session.send(:publish, :error,
|
72
|
+
@session.send(:publish, :error, error_content(e))
|
54
73
|
end
|
55
74
|
|
56
75
|
def kernel_info_request(msg)
|
57
76
|
@session.send(:reply, :kernel_info_reply,
|
58
77
|
protocol_version: '5.0',
|
59
78
|
implementation: 'iruby',
|
60
|
-
banner: "IRuby #{IRuby::VERSION}",
|
61
79
|
implementation_version: IRuby::VERSION,
|
62
80
|
language_info: {
|
63
81
|
name: 'ruby',
|
64
82
|
version: RUBY_VERSION,
|
65
83
|
mimetype: 'application/x-ruby',
|
66
84
|
file_extension: '.rb'
|
67
|
-
}
|
85
|
+
},
|
86
|
+
banner: "IRuby #{IRuby::VERSION} (with #{@session.description})",
|
87
|
+
help_links: [
|
88
|
+
{
|
89
|
+
text: "Ruby Documentation",
|
90
|
+
url: "https://ruby-doc.org/"
|
91
|
+
}
|
92
|
+
],
|
93
|
+
status: :ok)
|
68
94
|
end
|
69
95
|
|
70
96
|
def send_status(status)
|
97
|
+
IRuby.logger.debug "Send status: #{status}"
|
71
98
|
@session.send(:publish, :status, execution_state: status)
|
72
99
|
end
|
73
100
|
|
74
101
|
def execute_request(msg)
|
75
102
|
code = msg[:content]['code']
|
76
|
-
|
77
|
-
|
103
|
+
store_history = msg[:content]['store_history']
|
104
|
+
silent = msg[:content]['silent']
|
105
|
+
|
106
|
+
@execution_count += 1 if store_history
|
107
|
+
|
108
|
+
unless silent
|
109
|
+
@session.send(:publish, :execute_input, code: code, execution_count: @execution_count)
|
110
|
+
end
|
111
|
+
|
112
|
+
events.trigger(:pre_execute)
|
113
|
+
unless silent
|
114
|
+
exec_info = ExecutionInfo.new(code, store_history, silent)
|
115
|
+
events.trigger(:pre_run_cell, exec_info)
|
116
|
+
end
|
78
117
|
|
79
118
|
content = {
|
80
119
|
status: :ok,
|
@@ -82,15 +121,22 @@ module IRuby
|
|
82
121
|
user_expressions: {},
|
83
122
|
execution_count: @execution_count
|
84
123
|
}
|
124
|
+
|
85
125
|
result = nil
|
86
126
|
begin
|
87
|
-
result = @backend.eval(code,
|
127
|
+
result = @backend.eval(code, store_history)
|
88
128
|
rescue SystemExit
|
89
129
|
content[:payload] << { source: :ask_exit }
|
90
130
|
rescue Exception => e
|
91
|
-
content =
|
131
|
+
content = error_content(e)
|
92
132
|
@session.send(:publish, :error, content)
|
133
|
+
content[:status] = :error
|
134
|
+
content[:execution_count] = @execution_count
|
93
135
|
end
|
136
|
+
|
137
|
+
events.trigger(:post_execute)
|
138
|
+
events.trigger(:post_run_cell, result) unless silent
|
139
|
+
|
94
140
|
@session.send(:reply, :execute_reply, content)
|
95
141
|
@session.send(:publish, :execute_result,
|
96
142
|
data: Display.display(result),
|
@@ -98,26 +144,33 @@ module IRuby
|
|
98
144
|
execution_count: @execution_count) unless result.nil? || msg[:content]['silent']
|
99
145
|
end
|
100
146
|
|
101
|
-
def
|
102
|
-
{
|
103
|
-
|
147
|
+
def error_content(e)
|
148
|
+
rindex = e.backtrace.rindex{|line| line.start_with?(@backend.eval_path)} || -1
|
149
|
+
backtrace = SyntaxError === e && rindex == -1 ? [] : e.backtrace[0..rindex]
|
150
|
+
{ ename: e.class.to_s,
|
104
151
|
evalue: e.message,
|
105
|
-
traceback: ["#{RED}#{e.class}#{RESET}: #{e.message}", *
|
106
|
-
|
152
|
+
traceback: ["#{RED}#{e.class}#{RESET}: #{e.message}", *backtrace] }
|
153
|
+
end
|
154
|
+
|
155
|
+
def is_complete_request(msg)
|
156
|
+
# FIXME: the code completeness should be judged by using ripper or other Ruby parser
|
157
|
+
@session.send(:reply, :is_complete_reply,
|
158
|
+
status: :unknown)
|
107
159
|
end
|
108
160
|
|
109
161
|
def complete_request(msg)
|
110
162
|
# HACK for #26, only complete last line
|
111
163
|
code = msg[:content]['code']
|
112
|
-
if start = code.rindex(
|
164
|
+
if start = code.rindex(/\s|\R/)
|
113
165
|
code = code[start+1..-1]
|
114
166
|
start += 1
|
115
167
|
end
|
116
168
|
@session.send(:reply, :complete_reply,
|
117
169
|
matches: @backend.complete(code),
|
118
|
-
status: :ok,
|
119
170
|
cursor_start: start.to_i,
|
120
|
-
cursor_end: msg[:content]['cursor_pos']
|
171
|
+
cursor_end: msg[:content]['cursor_pos'],
|
172
|
+
metadata: {},
|
173
|
+
status: :ok)
|
121
174
|
end
|
122
175
|
|
123
176
|
def connect_request(msg)
|
@@ -136,14 +189,8 @@ module IRuby
|
|
136
189
|
end
|
137
190
|
|
138
191
|
def inspect_request(msg)
|
139
|
-
|
140
|
-
@session.send(:reply, :inspect_reply,
|
141
|
-
status: :ok,
|
142
|
-
data: Display.display(result),
|
143
|
-
metadata: {})
|
144
|
-
rescue Exception => e
|
145
|
-
IRuby.logger.warn "Inspection error: #{e.message}\n#{e.backtrace.join("\n")}"
|
146
|
-
@session.send(:reply, :inspect_reply, status: :error)
|
192
|
+
# not yet implemented. See (#119).
|
193
|
+
@session.send(:reply, :inspect_reply, status: :ok, found: false, data: {}, metadata: {})
|
147
194
|
end
|
148
195
|
|
149
196
|
def comm_open(msg)
|
@@ -161,5 +208,37 @@ module IRuby
|
|
161
208
|
Comm.comm[comm_id].handle_close(msg[:content]['data'])
|
162
209
|
Comm.comm.delete(comm_id)
|
163
210
|
end
|
211
|
+
|
212
|
+
private
|
213
|
+
|
214
|
+
def init_parent_process_poller
|
215
|
+
pid = ENV.fetch('JPY_PARENT_PID', 0).to_i
|
216
|
+
return unless pid > 1
|
217
|
+
|
218
|
+
case RUBY_PLATFORM
|
219
|
+
when /mswin/, /mingw/
|
220
|
+
# TODO
|
221
|
+
else
|
222
|
+
@parent_poller = start_parent_process_pollar_unix
|
223
|
+
end
|
224
|
+
end
|
225
|
+
|
226
|
+
def start_parent_process_pollar_unix
|
227
|
+
Thread.start do
|
228
|
+
IRuby.logger.warn("parent process poller thread started.")
|
229
|
+
loop do
|
230
|
+
begin
|
231
|
+
current_ppid = Process.ppid
|
232
|
+
if current_ppid == 1
|
233
|
+
IRuby.logger.warn("parent process appears to exited, shutting down.")
|
234
|
+
exit!(1)
|
235
|
+
end
|
236
|
+
sleep 1
|
237
|
+
rescue Errno::EINTR
|
238
|
+
# ignored
|
239
|
+
end
|
240
|
+
end
|
241
|
+
end
|
242
|
+
end
|
164
243
|
end
|
165
244
|
end
|
data/lib/iruby/ostream.rb
CHANGED
@@ -25,22 +25,43 @@ module IRuby
|
|
25
25
|
alias_method :next, :read
|
26
26
|
alias_method :readline, :read
|
27
27
|
|
28
|
-
def write(
|
29
|
-
|
30
|
-
|
31
|
-
nil
|
28
|
+
def write(*obj)
|
29
|
+
str = build_string { |sio| sio.write(*obj) }
|
30
|
+
session_send(str)
|
32
31
|
end
|
33
32
|
alias_method :<<, :write
|
34
33
|
alias_method :print, :write
|
35
34
|
|
36
|
-
def
|
37
|
-
|
38
|
-
|
39
|
-
|
35
|
+
def printf(format, *obj)
|
36
|
+
str = build_string { |sio| sio.printf(format, *obj) }
|
37
|
+
session_send(str)
|
38
|
+
end
|
39
|
+
|
40
|
+
def puts(*obj)
|
41
|
+
str = build_string { |sio| sio.puts(*obj) }
|
42
|
+
session_send(str)
|
40
43
|
end
|
41
44
|
|
42
45
|
def writelines(lines)
|
43
46
|
lines.each { |s| write(s) }
|
44
47
|
end
|
48
|
+
|
49
|
+
# Called by irb
|
50
|
+
def set_encoding(extern, intern)
|
51
|
+
a = extern
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
def build_string
|
57
|
+
StringIO.open { |sio| yield(sio); sio.string }
|
58
|
+
end
|
59
|
+
|
60
|
+
def session_send(str)
|
61
|
+
raise 'I/O operation on closed file' unless @session
|
62
|
+
|
63
|
+
@session.send(:publish, :stream, name: @name, text: str)
|
64
|
+
nil
|
65
|
+
end
|
45
66
|
end
|
46
67
|
end
|