iruby 0.4.0 → 0.7.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 (66) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ubuntu.yml +69 -0
  3. data/CHANGES.md +219 -0
  4. data/Gemfile +0 -2
  5. data/LICENSE +1 -1
  6. data/README.md +81 -62
  7. data/Rakefile +10 -10
  8. data/ci/Dockerfile.main.erb +1 -3
  9. data/iruby.gemspec +13 -19
  10. data/lib/iruby.rb +6 -6
  11. data/lib/iruby/backend.rb +38 -10
  12. data/lib/iruby/command.rb +2 -6
  13. data/lib/iruby/display.rb +216 -81
  14. data/lib/iruby/event_manager.rb +40 -0
  15. data/lib/iruby/formatter.rb +3 -3
  16. data/lib/iruby/input.rb +6 -6
  17. data/lib/iruby/input/autoload.rb +1 -1
  18. data/lib/iruby/input/builder.rb +4 -4
  19. data/lib/iruby/input/button.rb +2 -2
  20. data/lib/iruby/input/cancel.rb +1 -1
  21. data/lib/iruby/input/checkbox.rb +3 -3
  22. data/lib/iruby/input/date.rb +3 -3
  23. data/lib/iruby/input/field.rb +2 -2
  24. data/lib/iruby/input/file.rb +3 -3
  25. data/lib/iruby/input/form.rb +6 -6
  26. data/lib/iruby/input/label.rb +4 -4
  27. data/lib/iruby/input/multiple.rb +10 -10
  28. data/lib/iruby/input/popup.rb +2 -2
  29. data/lib/iruby/input/radio.rb +6 -6
  30. data/lib/iruby/input/select.rb +8 -8
  31. data/lib/iruby/input/textarea.rb +1 -1
  32. data/lib/iruby/input/widget.rb +2 -2
  33. data/lib/iruby/jupyter.rb +1 -0
  34. data/lib/iruby/kernel.rb +157 -29
  35. data/lib/iruby/ostream.rb +27 -10
  36. data/lib/iruby/session.rb +1 -0
  37. data/lib/iruby/session_adapter.rb +7 -3
  38. data/lib/iruby/session_adapter/pyzmq_adapter.rb +11 -10
  39. data/lib/iruby/session_adapter/test_adapter.rb +49 -0
  40. data/lib/iruby/utils.rb +15 -0
  41. data/lib/iruby/version.rb +1 -1
  42. data/run-test.sh +1 -1
  43. data/test/helper.rb +136 -0
  44. data/test/integration_test.rb +1 -2
  45. data/test/iruby/backend_test.rb +37 -0
  46. data/test/iruby/command_test.rb +0 -1
  47. data/test/iruby/display_test.rb +188 -0
  48. data/test/iruby/event_manager_test.rb +92 -0
  49. data/test/iruby/jupyter_test.rb +0 -1
  50. data/test/iruby/kernel_test.rb +185 -0
  51. data/test/iruby/mime_test.rb +50 -0
  52. data/test/iruby/multi_logger_test.rb +0 -4
  53. data/test/iruby/session_adapter/cztop_adapter_test.rb +1 -1
  54. data/test/iruby/session_adapter/ffirzmq_adapter_test.rb +1 -1
  55. data/test/iruby/session_adapter/session_adapter_test_base.rb +1 -3
  56. data/test/iruby/session_adapter_test.rb +42 -67
  57. data/test/iruby/session_test.rb +10 -15
  58. data/test/run-test.rb +19 -0
  59. metadata +74 -62
  60. data/.travis.yml +0 -41
  61. data/CHANGES +0 -143
  62. data/CONTRIBUTORS +0 -19
  63. data/lib/iruby/session/rbczmq.rb +0 -72
  64. data/lib/iruby/session_adapter/rbczmq_adapter.rb +0 -33
  65. data/test/iruby/session_adapter/rbczmq_adapter_test.rb +0 -37
  66. data/test/test_helper.rb +0 -48
@@ -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")
data/lib/iruby/jupyter.rb CHANGED
@@ -31,6 +31,7 @@ module IRuby
31
31
 
32
32
  # returns %APPDATA%
33
33
  def windows_user_appdata
34
+ require 'fiddle/import'
34
35
  check_windows
35
36
  path = Fiddle::Pointer.malloc(2 * 300) # uint16_t[300]
36
37
  csidl_appdata = 0x001a
data/lib/iruby/kernel.rb CHANGED
@@ -1,37 +1,126 @@
1
1
  module IRuby
2
+ ExecutionInfo = Struct.new(:raw_cell, :store_history, :silent)
3
+
2
4
  class Kernel
3
5
  RED = "\e[31m"
4
6
  RESET = "\e[0m"
5
7
 
6
- 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
7
27
  attr_accessor :instance
8
28
  end
9
29
 
30
+ # Returns a session object
10
31
  attr_reader :session
11
32
 
12
- 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)
13
41
  @config = MultiJson.load(File.read(config_file))
14
42
  IRuby.logger.debug("IRuby kernel start with config #{@config}")
15
43
  Kernel.instance = self
16
44
 
17
- @session = Session.new(@config)
45
+ @session = Session.new(@config, session_adapter_name)
18
46
  $stdout = OStream.new(@session, :stdout)
19
47
  $stderr = OStream.new(@session, :stderr)
20
48
 
21
49
  init_parent_process_poller
22
50
 
51
+ @events = EventManager.new(EVENTS)
23
52
  @execution_count = 0
24
- @backend = create_backend
53
+ @backend = PlainBackend.new
25
54
  @running = true
55
+
56
+ self.class.events.trigger(:initialized, self)
26
57
  end
27
58
 
28
- def create_backend
29
- PryBackend.new
30
- rescue Exception => e
31
- IRuby.logger.warn "Could not load PryBackend: #{e.message}\n#{e.backtrace.join("\n")}" unless LoadError === e
32
- PlainBackend.new
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
+ # Switch the backend (interactive shell) system
88
+ #
89
+ # @param backend [:irb,:plain,:pry] Specify the backend name switched to
90
+ #
91
+ # @return [true,false] true if the switching succeeds, otherwise false
92
+ def switch_backend!(backend)
93
+ name = case backend
94
+ when String, Symbol
95
+ name = backend.downcase
96
+ else
97
+ name = backend
98
+ end
99
+
100
+ backend_class = case name
101
+ when :irb, :plain
102
+ PlainBackend
103
+ when :pry
104
+ PryBackend
105
+ else
106
+ raise ArgumentError,
107
+ "Unknown backend name: %p" % backend
108
+ end
109
+
110
+ begin
111
+ new_backend = backend_class.new
112
+ @backend = new_backend
113
+ true
114
+ rescue Exception => e
115
+ unless LoadError === e
116
+ IRuby.logger.warn "Could not load #{backend_class}: " +
117
+ "#{e.message}\n#{e.backtrace.join("\n")}"
118
+ end
119
+ return false
120
+ end
33
121
  end
34
122
 
123
+ # @private
35
124
  def run
36
125
  send_status :starting
37
126
  while @running
@@ -39,6 +128,7 @@ module IRuby
39
128
  end
40
129
  end
41
130
 
131
+ # @private
42
132
  def dispatch
43
133
  msg = @session.recv(:reply)
44
134
  IRuby.logger.debug "Kernel#dispatch: msg = #{msg}"
@@ -55,29 +145,51 @@ module IRuby
55
145
  @session.send(:publish, :error, error_content(e))
56
146
  end
57
147
 
148
+ # @private
58
149
  def kernel_info_request(msg)
59
150
  @session.send(:reply, :kernel_info_reply,
60
151
  protocol_version: '5.0',
61
152
  implementation: 'iruby',
62
- banner: "IRuby #{IRuby::VERSION} (with #{@session.description})",
63
153
  implementation_version: IRuby::VERSION,
64
154
  language_info: {
65
155
  name: 'ruby',
66
156
  version: RUBY_VERSION,
67
157
  mimetype: 'application/x-ruby',
68
158
  file_extension: '.rb'
69
- })
159
+ },
160
+ banner: "IRuby #{IRuby::VERSION} (with #{@session.description})",
161
+ help_links: [
162
+ {
163
+ text: "Ruby Documentation",
164
+ url: "https://ruby-doc.org/"
165
+ }
166
+ ],
167
+ status: :ok)
70
168
  end
71
169
 
170
+ # @private
72
171
  def send_status(status)
73
172
  IRuby.logger.debug "Send status: #{status}"
74
173
  @session.send(:publish, :status, execution_state: status)
75
174
  end
76
175
 
176
+ # @private
77
177
  def execute_request(msg)
78
178
  code = msg[:content]['code']
79
- @execution_count += 1 if msg[:content]['store_history']
80
- @session.send(:publish, :execute_input, code: code, execution_count: @execution_count)
179
+ store_history = msg[:content]['store_history']
180
+ silent = msg[:content]['silent']
181
+
182
+ @execution_count += 1 if store_history
183
+
184
+ unless silent
185
+ @session.send(:publish, :execute_input, code: code, execution_count: @execution_count)
186
+ end
187
+
188
+ events.trigger(:pre_execute)
189
+ unless silent
190
+ exec_info = ExecutionInfo.new(code, store_history, silent)
191
+ events.trigger(:pre_run_cell, exec_info)
192
+ end
81
193
 
82
194
  content = {
83
195
  status: :ok,
@@ -85,35 +197,49 @@ module IRuby
85
197
  user_expressions: {},
86
198
  execution_count: @execution_count
87
199
  }
200
+
88
201
  result = nil
89
202
  begin
90
- result = @backend.eval(code, msg[:content]['store_history'])
203
+ result = @backend.eval(code, store_history)
91
204
  rescue SystemExit
92
205
  content[:payload] << { source: :ask_exit }
93
206
  rescue Exception => e
94
207
  content = error_content(e)
95
208
  @session.send(:publish, :error, content)
96
209
  content[:status] = :error
210
+ content[:execution_count] = @execution_count
97
211
  end
212
+
213
+ unless result.nil? || silent
214
+ @session.send(:publish, :execute_result,
215
+ data: Display.display(result),
216
+ metadata: {},
217
+ execution_count: @execution_count)
218
+ end
219
+
220
+ events.trigger(:post_execute)
221
+ events.trigger(:post_run_cell, result) unless silent
222
+
98
223
  @session.send(:reply, :execute_reply, content)
99
- @session.send(:publish, :execute_result,
100
- data: Display.display(result),
101
- metadata: {},
102
- execution_count: @execution_count) unless result.nil? || msg[:content]['silent']
103
224
  end
104
225
 
226
+ # @private
105
227
  def error_content(e)
228
+ rindex = e.backtrace.rindex{|line| line.start_with?(@backend.eval_path)} || -1
229
+ backtrace = SyntaxError === e && rindex == -1 ? [] : e.backtrace[0..rindex]
106
230
  { ename: e.class.to_s,
107
231
  evalue: e.message,
108
- traceback: ["#{RED}#{e.class}#{RESET}: #{e.message}", *e.backtrace] }
232
+ traceback: ["#{RED}#{e.class}#{RESET}: #{e.message}", *backtrace] }
109
233
  end
110
234
 
235
+ # @private
111
236
  def is_complete_request(msg)
112
237
  # FIXME: the code completeness should be judged by using ripper or other Ruby parser
113
238
  @session.send(:reply, :is_complete_reply,
114
239
  status: :unknown)
115
240
  end
116
241
 
242
+ # @private
117
243
  def complete_request(msg)
118
244
  # HACK for #26, only complete last line
119
245
  code = msg[:content]['code']
@@ -123,47 +249,49 @@ module IRuby
123
249
  end
124
250
  @session.send(:reply, :complete_reply,
125
251
  matches: @backend.complete(code),
126
- status: :ok,
127
252
  cursor_start: start.to_i,
128
- cursor_end: msg[:content]['cursor_pos'])
253
+ cursor_end: msg[:content]['cursor_pos'],
254
+ metadata: {},
255
+ status: :ok)
129
256
  end
130
257
 
258
+ # @private
131
259
  def connect_request(msg)
132
260
  @session.send(:reply, :connect_reply, Hash[%w(shell_port iopub_port stdin_port hb_port).map {|k| [k, @config[k]] }])
133
261
  end
134
262
 
263
+ # @private
135
264
  def shutdown_request(msg)
136
265
  @session.send(:reply, :shutdown_reply, msg[:content])
137
266
  @running = false
138
267
  end
139
268
 
269
+ # @private
140
270
  def history_request(msg)
141
271
  # we will just send back empty history for now, pending clarification
142
272
  # as requested in ipython/ipython#3806
143
273
  @session.send(:reply, :history_reply, history: [])
144
274
  end
145
275
 
276
+ # @private
146
277
  def inspect_request(msg)
147
- result = @backend.eval(msg[:content]['code'])
148
- @session.send(:reply, :inspect_reply,
149
- status: :ok,
150
- data: Display.display(result),
151
- metadata: {})
152
- rescue Exception => e
153
- IRuby.logger.warn "Inspection error: #{e.message}\n#{e.backtrace.join("\n")}"
154
- @session.send(:reply, :inspect_reply, status: :error)
278
+ # not yet implemented. See (#119).
279
+ @session.send(:reply, :inspect_reply, status: :ok, found: false, data: {}, metadata: {})
155
280
  end
156
281
 
282
+ # @private
157
283
  def comm_open(msg)
158
284
  comm_id = msg[:content]['comm_id']
159
285
  target_name = msg[:content]['target_name']
160
286
  Comm.comm[comm_id] = Comm.target[target_name].new(target_name, comm_id)
161
287
  end
162
288
 
289
+ # @private
163
290
  def comm_msg(msg)
164
291
  Comm.comm[msg[:content]['comm_id']].handle_msg(msg[:content]['data'])
165
292
  end
166
293
 
294
+ # @private
167
295
  def comm_close(msg)
168
296
  comm_id = msg[:content]['comm_id']
169
297
  Comm.comm[comm_id].handle_close(msg[:content]['data'])
data/lib/iruby/ostream.rb CHANGED
@@ -25,26 +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 printf(*fmt)
37
- write sprintf(*fmt)
35
+ def printf(format, *obj)
36
+ str = build_string { |sio| sio.printf(format, *obj) }
37
+ session_send(str)
38
38
  end
39
39
 
40
- def puts(*lines)
41
- lines = [''] if lines.empty?
42
- lines.each { |s| write("#{s}\n")}
43
- nil
40
+ def puts(*obj)
41
+ str = build_string { |sio| sio.puts(*obj) }
42
+ session_send(str)
44
43
  end
45
44
 
46
45
  def writelines(lines)
47
46
  lines.each { |s| write(s) }
48
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
49
66
  end
50
67
  end
data/lib/iruby/session.rb CHANGED
@@ -10,6 +10,7 @@ module IRuby
10
10
  def initialize(config, adapter_name=nil)
11
11
  @config = config
12
12
  @adapter = create_session_adapter(config, adapter_name)
13
+ @last_recvd_msg = nil
13
14
 
14
15
  setup
15
16
  setup_sockets
@@ -10,6 +10,10 @@ module IRuby
10
10
  false
11
11
  end
12
12
 
13
+ def self.load_requirements
14
+ # Do nothing
15
+ end
16
+
13
17
  def initialize(config)
14
18
  @config = config
15
19
  end
@@ -36,15 +40,15 @@ module IRuby
36
40
 
37
41
  require_relative 'session_adapter/ffirzmq_adapter'
38
42
  require_relative 'session_adapter/cztop_adapter'
39
- require_relative 'session_adapter/rbczmq_adapter'
40
43
  require_relative 'session_adapter/pyzmq_adapter'
44
+ require_relative 'session_adapter/test_adapter'
41
45
 
42
46
  def self.select_adapter_class(name=nil)
43
47
  classes = {
44
48
  'ffi-rzmq' => SessionAdapter::FfirzmqAdapter,
45
49
  'cztop' => SessionAdapter::CztopAdapter,
46
- 'rbczmq' => SessionAdapter::RbczmqAdapter,
47
- 'pyzmq' => SessionAdapter::PyzmqAdapter
50
+ # 'pyzmq' => SessionAdapter::PyzmqAdapter
51
+ 'test' => SessionAdapter::TestAdapter,
48
52
  }
49
53
  if (name ||= ENV.fetch('IRUBY_SESSION_ADAPTER', nil))
50
54
  cls = classes[name]
@@ -1,14 +1,19 @@
1
1
  module IRuby
2
2
  module SessionAdapter
3
3
  class PyzmqAdapter < BaseAdapter
4
- def self.load_requirements
5
- require 'pycall'
6
- @zmq = PyCall.import_module('zmq')
7
- rescue PyCall::PyError => error
8
- raise LoadError, error.message
9
- end
10
4
 
11
5
  class << self
6
+ def load_requirements
7
+ require 'pycall'
8
+ import_pyzmq
9
+ end
10
+
11
+ def import_pyzmq
12
+ @zmq = PyCall.import_module('zmq')
13
+ rescue PyCall::PyError => error
14
+ raise LoadError, error.message
15
+ end
16
+
12
17
  attr_reader :zmq
13
18
  end
14
19
 
@@ -20,10 +25,6 @@ module IRuby
20
25
  make_socket(:PUB, protocol, host, port)
21
26
  end
22
27
 
23
- def make_pub_socket(protocol, host, port)
24
- make_socket(:REP, protocol, host, port)
25
- end
26
-
27
28
  def heartbeat_loop(sock)
28
29
  PyCall.sys.path.append(File.expand_path('../pyzmq', __FILE__))
29
30
  heartbeat = PyCall.import_module('iruby.heartbeat')