iruby 0.2.9 → 0.6.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.
Files changed (72) hide show
  1. checksums.yaml +5 -5
  2. data/.github/workflows/ubuntu.yml +62 -0
  3. data/CHANGES +64 -0
  4. data/Gemfile +3 -1
  5. data/LICENSE +1 -1
  6. data/README.md +120 -92
  7. data/Rakefile +36 -10
  8. data/ci/Dockerfile.base.erb +41 -0
  9. data/ci/Dockerfile.main.erb +7 -0
  10. data/ci/requirements.txt +1 -0
  11. data/docker/setup.sh +15 -0
  12. data/docker/test.sh +7 -0
  13. data/iruby.gemspec +13 -18
  14. data/lib/iruby.rb +13 -6
  15. data/lib/iruby/backend.rb +38 -9
  16. data/lib/iruby/command.rb +68 -12
  17. data/lib/iruby/display.rb +81 -41
  18. data/lib/iruby/event_manager.rb +40 -0
  19. data/lib/iruby/formatter.rb +3 -3
  20. data/lib/iruby/input.rb +6 -6
  21. data/lib/iruby/input/README.ipynb +55 -3
  22. data/lib/iruby/input/README.md +299 -0
  23. data/lib/iruby/input/autoload.rb +3 -2
  24. data/lib/iruby/input/builder.rb +4 -4
  25. data/lib/iruby/input/button.rb +2 -2
  26. data/lib/iruby/input/cancel.rb +1 -1
  27. data/lib/iruby/input/checkbox.rb +22 -4
  28. data/lib/iruby/input/date.rb +17 -6
  29. data/lib/iruby/input/field.rb +5 -4
  30. data/lib/iruby/input/file.rb +3 -3
  31. data/lib/iruby/input/form.rb +6 -6
  32. data/lib/iruby/input/label.rb +9 -3
  33. data/lib/iruby/input/multiple.rb +76 -0
  34. data/lib/iruby/input/popup.rb +5 -2
  35. data/lib/iruby/input/radio.rb +18 -6
  36. data/lib/iruby/input/select.rb +26 -12
  37. data/lib/iruby/input/textarea.rb +2 -1
  38. data/lib/iruby/input/widget.rb +2 -2
  39. data/lib/iruby/jupyter.rb +77 -0
  40. data/lib/iruby/kernel.rb +171 -31
  41. data/lib/iruby/ostream.rb +29 -8
  42. data/lib/iruby/session.rb +116 -0
  43. data/lib/iruby/session/{rbczmq.rb → cztop.rb} +21 -19
  44. data/lib/iruby/session/ffi_rzmq.rb +1 -1
  45. data/lib/iruby/session_adapter.rb +72 -0
  46. data/lib/iruby/session_adapter/cztop_adapter.rb +45 -0
  47. data/lib/iruby/session_adapter/ffirzmq_adapter.rb +55 -0
  48. data/lib/iruby/session_adapter/pyzmq_adapter.rb +77 -0
  49. data/lib/iruby/session_adapter/test_adapter.rb +49 -0
  50. data/lib/iruby/utils.rb +13 -2
  51. data/lib/iruby/version.rb +1 -1
  52. data/run-test.sh +12 -0
  53. data/tasks/ci.rake +65 -0
  54. data/test/helper.rb +133 -0
  55. data/test/integration_test.rb +22 -11
  56. data/test/iruby/backend_test.rb +37 -0
  57. data/test/iruby/command_test.rb +207 -0
  58. data/test/iruby/event_manager_test.rb +92 -0
  59. data/test/iruby/jupyter_test.rb +27 -0
  60. data/test/iruby/kernel_test.rb +153 -0
  61. data/test/iruby/mime_test.rb +43 -0
  62. data/test/iruby/multi_logger_test.rb +1 -2
  63. data/test/iruby/session_adapter/cztop_adapter_test.rb +20 -0
  64. data/test/iruby/session_adapter/ffirzmq_adapter_test.rb +20 -0
  65. data/test/iruby/session_adapter/session_adapter_test_base.rb +27 -0
  66. data/test/iruby/session_adapter_test.rb +91 -0
  67. data/test/iruby/session_test.rb +48 -0
  68. data/test/run-test.rb +19 -0
  69. metadata +107 -50
  70. data/.travis.yml +0 -16
  71. data/CONTRIBUTORS +0 -19
  72. data/test/test_helper.rb +0 -5
@@ -1,24 +1,36 @@
1
1
  module IRuby
2
2
  module Input
3
3
  class Select < Label
4
- needs :options
4
+ needs :options, :default
5
5
 
6
6
  builder :select do |*args, **params|
7
7
  key = :select
8
8
  key, *args = args if args.first.is_a? Symbol
9
9
 
10
- key = unique_key(key)
11
- add_field Select.new(key: key, options: args)
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)
12
19
  end
13
20
 
14
21
  def widget_css
15
- '.iruby-select { margin-left: 0 !important }'
22
+ <<-CSS
23
+ .iruby-select {
24
+ min-width: 25%;
25
+ margin-left: 0 !important;
26
+ }
27
+ CSS
16
28
  end
17
29
 
18
30
  def widget_js
19
31
  <<-JS
20
32
  $('.iruby-select').change(function(){
21
- $(this).data('iruby-value',
33
+ $(this).data('iruby-value',
22
34
  $(this).find('option:selected').text()
23
35
  );
24
36
  });
@@ -26,16 +38,18 @@ module IRuby
26
38
  end
27
39
 
28
40
  def widget_html
29
- widget_label do
30
- div class: 'form-control' do
41
+ widget_label do
42
+ div class: 'form-control' do
31
43
  params = {
32
- class: 'iruby-select',
44
+ class: 'iruby-select',
33
45
  :'data-iruby-key' => @key,
34
- :'data-iruby-value' => @options.first
46
+ :'data-iruby-value' => @default
35
47
  }
36
-
37
- select **params do
38
- @options.each {|o| option o }
48
+
49
+ select **params do
50
+ @options.each do |o|
51
+ option o, selected: @default == o
52
+ end
39
53
  end
40
54
  end
41
55
  end
@@ -9,8 +9,9 @@ module IRuby
9
9
  end
10
10
 
11
11
  def widget_html
12
- widget_label do
12
+ widget_label do
13
13
  textarea(
14
+ @default,
14
15
  rows: @rows,
15
16
  :'data-iruby-key' => @key,
16
17
  class: 'form-control iruby-field'
@@ -9,13 +9,13 @@ module IRuby
9
9
  def content; widget_html; end
10
10
 
11
11
  def self.builder method, &block
12
- Builder.instance_eval do
12
+ Builder.instance_eval do
13
13
  define_method method, &block
14
14
  end
15
15
  end
16
16
 
17
17
  def widget_join method, *args
18
- strings = args.map do |arg|
18
+ strings = args.map do |arg|
19
19
  arg.is_a?(String) ? arg : arg.send(method)
20
20
  end
21
21
  strings.uniq.join("\n")
@@ -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,90 @@
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
- class<< self
8
+ @events = EventManager.new([:initialized])
9
+
10
+ class << self
11
+ # Return the event manager defined in the `IRuby::Kernel` class.
12
+ # This event manager can handle the following event:
13
+ #
14
+ # - `initialized`: The event occurred after the initialization of
15
+ # a `IRuby::Kernel` instance is finished
16
+ #
17
+ # @example Registering initialized event
18
+ # IRuby::Kernel.events.register(:initialized) do |result|
19
+ # STDERR.puts "IRuby kernel has been initialized"
20
+ # end
21
+ #
22
+ # @see IRuby::EventManager
23
+ # @see IRuby::Kernel#events
24
+ attr_reader :events
25
+
26
+ # Returns the singleton kernel instance
8
27
  attr_accessor :instance
9
28
  end
10
29
 
30
+ # Returns a session object
11
31
  attr_reader :session
12
32
 
13
- def initialize(config_file)
33
+ EVENTS = [
34
+ :pre_execute,
35
+ :pre_run_cell,
36
+ :post_run_cell,
37
+ :post_execute
38
+ ].freeze
39
+
40
+ def initialize(config_file, session_adapter_name=nil)
14
41
  @config = MultiJson.load(File.read(config_file))
15
42
  IRuby.logger.debug("IRuby kernel start with config #{@config}")
16
43
  Kernel.instance = self
17
44
 
18
- @session = Session.new(@config)
45
+ @session = Session.new(@config, session_adapter_name)
19
46
  $stdout = OStream.new(@session, :stdout)
20
47
  $stderr = OStream.new(@session, :stderr)
21
48
 
49
+ init_parent_process_poller
50
+
51
+ @events = EventManager.new(EVENTS)
22
52
  @execution_count = 0
23
53
  @backend = create_backend
24
54
  @running = true
55
+
56
+ self.class.events.trigger(:initialized, self)
25
57
  end
26
58
 
59
+ # Returns the event manager defined in a `IRuby::Kernel` instance.
60
+ # This event manager can handle the following events:
61
+ #
62
+ # - `pre_execute`: The event occurred before running the code
63
+ #
64
+ # - `pre_run_cell`: The event occurred before running the code and
65
+ # if the code execution is not silent
66
+ #
67
+ # - `post_execute`: The event occurred after running the code
68
+ #
69
+ # - `post_run_cell`: The event occurred after running the code and
70
+ # if the code execution is not silent
71
+ #
72
+ # The callback functions of `pre_run_cell` event must take one argument
73
+ # to get an `ExecutionInfo` object.
74
+ # The callback functions of `post_run_cell` event must take one argument
75
+ # to get the result of the code execution.
76
+ #
77
+ # @example Registering post_run_cell event
78
+ # IRuby::Kernel.instance.events.register(:post_run_cell) do |result|
79
+ # STDERR.puts "The result of the last execution: %p" % result
80
+ # end
81
+ #
82
+ # @see IRuby::EventManager
83
+ # @see IRuby::ExecutionInfo
84
+ # @see IRuby::Kernel.events
85
+ attr_reader :events
86
+
87
+ # @private
27
88
  def create_backend
28
89
  PryBackend.new
29
90
  rescue Exception => e
@@ -31,6 +92,7 @@ module IRuby
31
92
  PlainBackend.new
32
93
  end
33
94
 
95
+ # @private
34
96
  def run
35
97
  send_status :starting
36
98
  while @running
@@ -38,8 +100,10 @@ module IRuby
38
100
  end
39
101
  end
40
102
 
103
+ # @private
41
104
  def dispatch
42
105
  msg = @session.recv(:reply)
106
+ IRuby.logger.debug "Kernel#dispatch: msg = #{msg}"
43
107
  type = msg[:header]['msg_type']
44
108
  raise "Unknown message type: #{msg.inspect}" unless type =~ /comm_|_request\Z/ && respond_to?(type)
45
109
  begin
@@ -50,31 +114,54 @@ module IRuby
50
114
  end
51
115
  rescue Exception => e
52
116
  IRuby.logger.debug "Kernel error: #{e.message}\n#{e.backtrace.join("\n")}"
53
- @session.send(:publish, :error, error_message(e))
117
+ @session.send(:publish, :error, error_content(e))
54
118
  end
55
119
 
120
+ # @private
56
121
  def kernel_info_request(msg)
57
122
  @session.send(:reply, :kernel_info_reply,
58
123
  protocol_version: '5.0',
59
124
  implementation: 'iruby',
60
- banner: "IRuby #{IRuby::VERSION}",
61
125
  implementation_version: IRuby::VERSION,
62
126
  language_info: {
63
127
  name: 'ruby',
64
128
  version: RUBY_VERSION,
65
129
  mimetype: 'application/x-ruby',
66
130
  file_extension: '.rb'
67
- })
131
+ },
132
+ banner: "IRuby #{IRuby::VERSION} (with #{@session.description})",
133
+ help_links: [
134
+ {
135
+ text: "Ruby Documentation",
136
+ url: "https://ruby-doc.org/"
137
+ }
138
+ ],
139
+ status: :ok)
68
140
  end
69
141
 
142
+ # @private
70
143
  def send_status(status)
144
+ IRuby.logger.debug "Send status: #{status}"
71
145
  @session.send(:publish, :status, execution_state: status)
72
146
  end
73
147
 
148
+ # @private
74
149
  def execute_request(msg)
75
150
  code = msg[:content]['code']
76
- @execution_count += 1 if msg[:content]['store_history']
77
- @session.send(:publish, :execute_input, code: code, execution_count: @execution_count)
151
+ store_history = msg[:content]['store_history']
152
+ silent = msg[:content]['silent']
153
+
154
+ @execution_count += 1 if store_history
155
+
156
+ unless silent
157
+ @session.send(:publish, :execute_input, code: code, execution_count: @execution_count)
158
+ end
159
+
160
+ events.trigger(:pre_execute)
161
+ unless silent
162
+ exec_info = ExecutionInfo.new(code, store_history, silent)
163
+ events.trigger(:pre_run_cell, exec_info)
164
+ end
78
165
 
79
166
  content = {
80
167
  status: :ok,
@@ -82,84 +169,137 @@ module IRuby
82
169
  user_expressions: {},
83
170
  execution_count: @execution_count
84
171
  }
172
+
85
173
  result = nil
86
174
  begin
87
- result = @backend.eval(code, msg[:content]['store_history'])
175
+ result = @backend.eval(code, store_history)
88
176
  rescue SystemExit
89
177
  content[:payload] << { source: :ask_exit }
90
178
  rescue Exception => e
91
- content = error_message(e)
179
+ content = error_content(e)
92
180
  @session.send(:publish, :error, content)
181
+ content[:status] = :error
182
+ content[:execution_count] = @execution_count
183
+ end
184
+
185
+ unless result.nil? || silent
186
+ @session.send(:publish, :execute_result,
187
+ data: Display.display(result),
188
+ metadata: {},
189
+ execution_count: @execution_count)
93
190
  end
191
+
192
+ events.trigger(:post_execute)
193
+ events.trigger(:post_run_cell, result) unless silent
194
+
94
195
  @session.send(:reply, :execute_reply, content)
95
- @session.send(:publish, :execute_result,
96
- data: Display.display(result),
97
- metadata: {},
98
- execution_count: @execution_count) unless result.nil? || msg[:content]['silent']
99
196
  end
100
197
 
101
- def error_message(e)
102
- { status: :error,
103
- ename: e.class.to_s,
198
+ # @private
199
+ def error_content(e)
200
+ rindex = e.backtrace.rindex{|line| line.start_with?(@backend.eval_path)} || -1
201
+ backtrace = SyntaxError === e && rindex == -1 ? [] : e.backtrace[0..rindex]
202
+ { ename: e.class.to_s,
104
203
  evalue: e.message,
105
- traceback: ["#{RED}#{e.class}#{RESET}: #{e.message}", *e.backtrace.map { |l| "#{WHITE}#{l}#{RESET}" }],
106
- execution_count: @execution_count }
204
+ traceback: ["#{RED}#{e.class}#{RESET}: #{e.message}", *backtrace] }
205
+ end
206
+
207
+ # @private
208
+ def is_complete_request(msg)
209
+ # FIXME: the code completeness should be judged by using ripper or other Ruby parser
210
+ @session.send(:reply, :is_complete_reply,
211
+ status: :unknown)
107
212
  end
108
213
 
214
+ # @private
109
215
  def complete_request(msg)
110
216
  # HACK for #26, only complete last line
111
217
  code = msg[:content]['code']
112
- if start = code.rindex("\n")
218
+ if start = code.rindex(/\s|\R/)
113
219
  code = code[start+1..-1]
114
220
  start += 1
115
221
  end
116
222
  @session.send(:reply, :complete_reply,
117
223
  matches: @backend.complete(code),
118
- status: :ok,
119
224
  cursor_start: start.to_i,
120
- cursor_end: msg[:content]['cursor_pos'])
225
+ cursor_end: msg[:content]['cursor_pos'],
226
+ metadata: {},
227
+ status: :ok)
121
228
  end
122
229
 
230
+ # @private
123
231
  def connect_request(msg)
124
232
  @session.send(:reply, :connect_reply, Hash[%w(shell_port iopub_port stdin_port hb_port).map {|k| [k, @config[k]] }])
125
233
  end
126
234
 
235
+ # @private
127
236
  def shutdown_request(msg)
128
237
  @session.send(:reply, :shutdown_reply, msg[:content])
129
238
  @running = false
130
239
  end
131
240
 
241
+ # @private
132
242
  def history_request(msg)
133
243
  # we will just send back empty history for now, pending clarification
134
244
  # as requested in ipython/ipython#3806
135
245
  @session.send(:reply, :history_reply, history: [])
136
246
  end
137
247
 
248
+ # @private
138
249
  def inspect_request(msg)
139
- result = @backend.eval(msg[:content]['code'])
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)
250
+ # not yet implemented. See (#119).
251
+ @session.send(:reply, :inspect_reply, status: :ok, found: false, data: {}, metadata: {})
147
252
  end
148
253
 
254
+ # @private
149
255
  def comm_open(msg)
150
256
  comm_id = msg[:content]['comm_id']
151
257
  target_name = msg[:content]['target_name']
152
258
  Comm.comm[comm_id] = Comm.target[target_name].new(target_name, comm_id)
153
259
  end
154
260
 
261
+ # @private
155
262
  def comm_msg(msg)
156
263
  Comm.comm[msg[:content]['comm_id']].handle_msg(msg[:content]['data'])
157
264
  end
158
265
 
266
+ # @private
159
267
  def comm_close(msg)
160
268
  comm_id = msg[:content]['comm_id']
161
269
  Comm.comm[comm_id].handle_close(msg[:content]['data'])
162
270
  Comm.comm.delete(comm_id)
163
271
  end
272
+
273
+ private
274
+
275
+ def init_parent_process_poller
276
+ pid = ENV.fetch('JPY_PARENT_PID', 0).to_i
277
+ return unless pid > 1
278
+
279
+ case RUBY_PLATFORM
280
+ when /mswin/, /mingw/
281
+ # TODO
282
+ else
283
+ @parent_poller = start_parent_process_pollar_unix
284
+ end
285
+ end
286
+
287
+ def start_parent_process_pollar_unix
288
+ Thread.start do
289
+ IRuby.logger.warn("parent process poller thread started.")
290
+ loop do
291
+ begin
292
+ current_ppid = Process.ppid
293
+ if current_ppid == 1
294
+ IRuby.logger.warn("parent process appears to exited, shutting down.")
295
+ exit!(1)
296
+ end
297
+ sleep 1
298
+ rescue Errno::EINTR
299
+ # ignored
300
+ end
301
+ end
302
+ end
303
+ end
164
304
  end
165
305
  end