iruby 0.4.0 → 0.7.1

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