iruby 0.2.8 → 0.6.0
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 +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
|