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.
Files changed (72) hide show
  1. checksums.yaml +5 -5
  2. data/.github/workflows/ubuntu.yml +62 -0
  3. data/CHANGES +76 -0
  4. data/Gemfile +3 -1
  5. data/LICENSE +1 -1
  6. data/README.md +130 -82
  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 -17
  14. data/lib/iruby.rb +14 -6
  15. data/lib/iruby/backend.rb +41 -7
  16. data/lib/iruby/command.rb +68 -12
  17. data/lib/iruby/display.rb +80 -39
  18. data/lib/iruby/event_manager.rb +40 -0
  19. data/lib/iruby/formatter.rb +5 -4
  20. data/lib/iruby/input.rb +41 -0
  21. data/lib/iruby/input/README.ipynb +502 -0
  22. data/lib/iruby/input/README.md +299 -0
  23. data/lib/iruby/input/autoload.rb +25 -0
  24. data/lib/iruby/input/builder.rb +67 -0
  25. data/lib/iruby/input/button.rb +47 -0
  26. data/lib/iruby/input/cancel.rb +32 -0
  27. data/lib/iruby/input/checkbox.rb +74 -0
  28. data/lib/iruby/input/date.rb +37 -0
  29. data/lib/iruby/input/field.rb +31 -0
  30. data/lib/iruby/input/file.rb +57 -0
  31. data/lib/iruby/input/form.rb +77 -0
  32. data/lib/iruby/input/label.rb +27 -0
  33. data/lib/iruby/input/multiple.rb +76 -0
  34. data/lib/iruby/input/popup.rb +41 -0
  35. data/lib/iruby/input/radio.rb +59 -0
  36. data/lib/iruby/input/select.rb +59 -0
  37. data/lib/iruby/input/textarea.rb +23 -0
  38. data/lib/iruby/input/widget.rb +34 -0
  39. data/lib/iruby/jupyter.rb +77 -0
  40. data/lib/iruby/kernel.rb +106 -27
  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} +25 -13
  44. data/lib/iruby/session/ffi_rzmq.rb +15 -2
  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 +5 -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 +132 -43
  70. data/.travis.yml +0 -16
  71. data/CONTRIBUTORS +0 -19
  72. 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
- class<< self
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
- def initialize(config_file)
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, error_message(e))
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
- @execution_count += 1 if msg[:content]['store_history']
77
- @session.send(:publish, :execute_input, code: code, execution_count: @execution_count)
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, msg[:content]['store_history'])
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 = error_message(e)
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 error_message(e)
102
- { status: :error,
103
- ename: e.class.to_s,
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}", *e.backtrace.map { |l| "#{WHITE}#{l}#{RESET}" }],
106
- execution_count: @execution_count }
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("\n")
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
- 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)
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(s)
29
- raise 'I/O operation on closed file' unless @session
30
- @session.send(:publish, :stream, name: @name, text: s.to_s)
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 puts(*lines)
37
- lines = [''] if lines.empty?
38
- lines.each { |s| write("#{s}\n")}
39
- nil
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