iruby 0.2.8 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
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