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
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
@@ -0,0 +1,116 @@
1
+ require 'iruby/session_adapter'
2
+ require 'iruby/session/mixin'
3
+
4
+ require 'securerandom'
5
+
6
+ module IRuby
7
+ class Session
8
+ include SessionSerialize
9
+
10
+ def initialize(config, adapter_name=nil)
11
+ @config = config
12
+ @adapter = create_session_adapter(config, adapter_name)
13
+
14
+ setup
15
+ setup_sockets
16
+ setup_heartbeat
17
+ setup_security
18
+ end
19
+
20
+ attr_reader :adapter, :config
21
+
22
+ def description
23
+ "#{@adapter.name} session adapter"
24
+ end
25
+
26
+ def setup
27
+ end
28
+
29
+ def setup_sockets
30
+ protocol, host = config.values_at('transport', 'ip')
31
+ shell_port = config['shell_port']
32
+ iopub_port = config['iopub_port']
33
+ stdin_port = config['stdin_port']
34
+
35
+ @shell_socket, @shell_port = @adapter.make_router_socket(protocol, host, shell_port)
36
+ @iopub_socket, @iopub_port = @adapter.make_pub_socket(protocol, host, iopub_port)
37
+ @stdin_socket, @stdin_port = @adapter.make_router_socket(protocol, host, stdin_port)
38
+
39
+ @sockets = {
40
+ publish: @iopub_socket,
41
+ reply: @shell_socket,
42
+ stdin: @stdin_socket
43
+ }
44
+ end
45
+
46
+ def setup_heartbeat
47
+ protocol, host = config.values_at('transport', 'ip')
48
+ hb_port = config['hb_port']
49
+ @hb_socket, @hb_port = @adapter.make_rep_socket(protocol, host, hb_port)
50
+ @heartbeat_thread = Thread.start do
51
+ begin
52
+ # NOTE: this loop is copied from CZTop's old session code
53
+ @adapter.heartbeat_loop(@hb_socket)
54
+ rescue Exception => e
55
+ IRuby.logger.fatal "Kernel heartbeat died: #{e.message}\n#{e.backtrace.join("\n")}"
56
+ end
57
+ end
58
+ end
59
+
60
+ def setup_security
61
+ @session_id = SecureRandom.uuid
62
+ unless config['key'].empty? || config['signature_scheme'].empty?
63
+ unless config['signature_scheme'] =~ /\Ahmac-/
64
+ raise "Unknown signature_scheme: #{config['signature_scheme']}"
65
+ end
66
+ digest_algorithm = config['signature_scheme'][/\Ahmac-(.*)\Z/, 1]
67
+ @hmac = OpenSSL::HMAC.new(config['key'], OpenSSL::Digest.new(digest_algorithm))
68
+ end
69
+ end
70
+
71
+ def send(socket_type, message_type, content)
72
+ sock = check_socket_type(socket_type)
73
+ idents = if socket_type == :reply && @last_recvd_msg
74
+ @last_recvd_msg[:idents]
75
+ else
76
+ message_type == :stream ? "stream.#{content[:name]}" : message_type
77
+ end
78
+ header = {
79
+ msg_type: message_type,
80
+ msg_id: SecureRandom.uuid,
81
+ username: 'kernel',
82
+ session: @session_id,
83
+ version: '5.0'
84
+ }
85
+ @adapter.send(sock, serialize(idents, header, content))
86
+ end
87
+
88
+ def recv(socket_type)
89
+ sock = check_socket_type(socket_type)
90
+ data = @adapter.recv(sock)
91
+ @last_recvd_msg = unserialize(data)
92
+ end
93
+
94
+ def recv_input
95
+ sock = check_socket_type(:stdin)
96
+ data = @adapter.recv(sock)
97
+ unserialize(data)[:content]["value"]
98
+ end
99
+
100
+ private
101
+
102
+ def check_socket_type(socket_type)
103
+ case socket_type
104
+ when :publish, :reply, :stdin
105
+ @sockets[socket_type]
106
+ else
107
+ raise ArgumentError, "Invalid socket type #{socket_type}"
108
+ end
109
+ end
110
+
111
+ def create_session_adapter(config, adapter_name)
112
+ adapter_class = SessionAdapter.select_adapter_class(adapter_name)
113
+ adapter_class.new(config)
114
+ end
115
+ end
116
+ end
@@ -1,34 +1,32 @@
1
- require 'rbczmq'
1
+ require 'cztop'
2
2
 
3
3
  module IRuby
4
4
  class Session
5
5
  include SessionSerialize
6
6
 
7
7
  def initialize(config)
8
- c = ZMQ::Context.new
9
-
10
8
  connection = "#{config['transport']}://#{config['ip']}:%d"
11
- reply_socket = c.socket(:ROUTER)
12
- reply_socket.bind(connection % config['shell_port'])
13
-
14
- pub_socket = c.socket(:PUB)
15
- pub_socket.bind(connection % config['iopub_port'])
16
9
 
17
- stdin_socket = c.socket(:ROUTER)
18
- stdin_socket.bind(connection % config['stdin_port'])
10
+ reply_socket = CZTop::Socket::ROUTER.new(connection % config['shell_port'])
11
+ pub_socket = CZTop::Socket::PUB.new(connection % config['iopub_port'])
12
+ stdin_socket = CZTop::Socket::ROUTER.new(connection % config['stdin_port'])
19
13
 
20
14
  Thread.new do
21
15
  begin
22
- hb_socket = c.socket(:REP)
23
- hb_socket.bind(connection % config['hb_port'])
24
- ZMQ.proxy(hb_socket, hb_socket)
16
+ hb_socket = CZTop::Socket::REP.new(connection % config['hb_port'])
17
+ loop do
18
+ message = hb_socket.receive
19
+ hb_socket << message
20
+ end
25
21
  rescue Exception => e
26
22
  IRuby.logger.fatal "Kernel heartbeat died: #{e.message}\n#{e.backtrace.join("\n")}"
27
23
  end
28
24
  end
29
25
 
30
- @sockets = {
31
- publish: pub_socket, reply: reply_socket, stdin: stdin_socket
26
+ @sockets = {
27
+ publish: pub_socket,
28
+ reply: reply_socket,
29
+ stdin: stdin_socket,
32
30
  }
33
31
 
34
32
  @session = SecureRandom.uuid
@@ -38,6 +36,10 @@ module IRuby
38
36
  end
39
37
  end
40
38
 
39
+ def description
40
+ 'old-stle session using cztop'
41
+ end
42
+
41
43
  # Build and send a message
42
44
  def send(socket, type, content)
43
45
  idents =
@@ -53,16 +55,16 @@ module IRuby
53
55
  session: @session,
54
56
  version: '5.0'
55
57
  }
56
- @sockets[socket].send_message(ZMQ::Message(*serialize(idents, header, content)))
58
+ @sockets[socket] << serialize(idents, header, content)
57
59
  end
58
60
 
59
61
  # Receive a message and decode it
60
62
  def recv(socket)
61
- @last_recvd_msg = unserialize(@sockets[socket].recv_message)
63
+ @last_recvd_msg = unserialize(@sockets[socket].receive)
62
64
  end
63
65
 
64
- def recv_input
65
- unserialize(@sockets[:stdin].recv_message)[:content]["value"]
66
+ def recv_input
67
+ unserialize(@sockets[:stdin].receive)[:content]["value"]
66
68
  end
67
69
  end
68
70
  end
@@ -67,7 +67,7 @@ module IRuby
67
67
  while msg.empty? || socket.more_parts?
68
68
  begin
69
69
  frame = ''
70
- rc = socket.recv_string(frame, 1)
70
+ rc = socket.recv_string(frame)
71
71
  ZMQ::Util.error_check('zmq_msg_send', rc)
72
72
  msg << frame
73
73
  rescue
@@ -0,0 +1,72 @@
1
+ module IRuby
2
+ class SessionAdapterNotFound < RuntimeError; end
3
+
4
+ module SessionAdapter
5
+ class BaseAdapter
6
+ def self.available?
7
+ load_requirements
8
+ true
9
+ rescue LoadError
10
+ false
11
+ end
12
+
13
+ def self.load_requirements
14
+ # Do nothing
15
+ end
16
+
17
+ def initialize(config)
18
+ @config = config
19
+ end
20
+
21
+ def name
22
+ self.class.name[/::(\w+)Adapter\Z/, 1].downcase
23
+ end
24
+
25
+ def make_router_socket(protocol, host, port)
26
+ socket, port = make_socket(:ROUTER, protocol, host, port)
27
+ [socket, port]
28
+ end
29
+
30
+ def make_pub_socket(protocol, host, port)
31
+ socket, port = make_socket(:PUB, protocol, host, port)
32
+ [socket, port]
33
+ end
34
+
35
+ def make_rep_socket(protocol, host, port)
36
+ socket, port = make_socket(:REP, protocol, host, port)
37
+ [socket, port]
38
+ end
39
+ end
40
+
41
+ require_relative 'session_adapter/ffirzmq_adapter'
42
+ require_relative 'session_adapter/cztop_adapter'
43
+ require_relative 'session_adapter/pyzmq_adapter'
44
+ require_relative 'session_adapter/test_adapter'
45
+
46
+ def self.select_adapter_class(name=nil)
47
+ classes = {
48
+ 'ffi-rzmq' => SessionAdapter::FfirzmqAdapter,
49
+ 'cztop' => SessionAdapter::CztopAdapter,
50
+ # 'pyzmq' => SessionAdapter::PyzmqAdapter
51
+ 'test' => SessionAdapter::TestAdapter,
52
+ }
53
+ if (name ||= ENV.fetch('IRUBY_SESSION_ADAPTER', nil))
54
+ cls = classes[name]
55
+ unless cls.available?
56
+ if ENV['IRUBY_SESSION_ADAPTER']
57
+ raise SessionAdapterNotFound,
58
+ "Session adapter `#{name}` from IRUBY_SESSION_ADAPTER is unavailable"
59
+ else
60
+ raise SessionAdapterNotFound,
61
+ "Session adapter `#{name}` is unavailable"
62
+ end
63
+ end
64
+ return cls
65
+ end
66
+ classes.each_value do |cls|
67
+ return cls if cls.available?
68
+ end
69
+ raise SessionAdapterNotFound, "No session adapter is available"
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,45 @@
1
+ module IRuby
2
+ module SessionAdapter
3
+ class CztopAdapter < BaseAdapter
4
+ def self.load_requirements
5
+ require 'cztop'
6
+ end
7
+
8
+ def send(sock, data)
9
+ sock << data
10
+ end
11
+
12
+ def recv(sock)
13
+ sock.receive
14
+ end
15
+
16
+ def heartbeat_loop(sock)
17
+ loop do
18
+ message = sock.receive
19
+ sock << message
20
+ end
21
+ end
22
+
23
+ private
24
+
25
+ def socket_type_class(type_symbol)
26
+ case type_symbol
27
+ when :ROUTER, :PUB, :REP
28
+ CZTop::Socket.const_get(type_symbol)
29
+ else
30
+ if CZTop::Socket.const_defined?(type_symbol)
31
+ raise ArgumentError, "Unsupported ZMQ socket type: #{type_symbol}"
32
+ else
33
+ raise ArgumentError, "Invalid ZMQ socket type: #{type_symbol}"
34
+ end
35
+ end
36
+ end
37
+
38
+ def make_socket(type_symbol, protocol, host, port)
39
+ uri = "#{protocol}://#{host}:#{port}"
40
+ socket_class = socket_type_class(type_symbol)
41
+ socket_class.new(uri)
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,55 @@
1
+ module IRuby
2
+ module SessionAdapter
3
+ class FfirzmqAdapter < BaseAdapter
4
+ def self.load_requirements
5
+ require 'ffi-rzmq'
6
+ end
7
+
8
+ def send(sock, data)
9
+ data.each_with_index do |part, i|
10
+ sock.send_string(part, i == data.size - 1 ? 0 : ZMQ::SNDMORE)
11
+ end
12
+ end
13
+
14
+ def recv(sock)
15
+ msg = []
16
+ while msg.empty? || sock.more_parts?
17
+ begin
18
+ frame = ''
19
+ rc = sock.recv_string(frame)
20
+ ZMQ::Util.error_check('zmq_msg_recv', rc)
21
+ msg << frame
22
+ rescue
23
+ end
24
+ end
25
+ msg
26
+ end
27
+
28
+ def heartbeat_loop(sock)
29
+ @heartbeat_device = ZMQ::Device.new(sock, sock)
30
+ end
31
+
32
+ private
33
+
34
+ def make_socket(type, protocol, host, port)
35
+ case type
36
+ when :ROUTER, :PUB, :REP
37
+ type = ZMQ.const_get(type)
38
+ else
39
+ if ZMQ.const_defined?(type)
40
+ raise ArgumentError, "Unsupported ZMQ socket type: #{type_symbol}"
41
+ else
42
+ raise ArgumentError, "Invalid ZMQ socket type: #{type_symbol}"
43
+ end
44
+ end
45
+ zmq_context.socket(type).tap do |sock|
46
+ sock.bind("#{protocol}://#{host}:#{port}")
47
+ end
48
+ end
49
+
50
+ def zmq_context
51
+ @zmq_context ||= ZMQ::Context.new
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,77 @@
1
+ module IRuby
2
+ module SessionAdapter
3
+ class PyzmqAdapter < BaseAdapter
4
+
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
+
17
+ attr_reader :zmq
18
+ end
19
+
20
+ def make_router_socket(protocol, host, port)
21
+ make_socket(:ROUTER, protocol, host, port)
22
+ end
23
+
24
+ def make_pub_socket(protocol, host, port)
25
+ make_socket(:PUB, protocol, host, port)
26
+ end
27
+
28
+ def heartbeat_loop(sock)
29
+ PyCall.sys.path.append(File.expand_path('../pyzmq', __FILE__))
30
+ heartbeat = PyCall.import_module('iruby.heartbeat')
31
+ @heartbeat_thread = heartbeat.Heartbeat.new(sock)
32
+ @heartbeat_thread.start
33
+ end
34
+
35
+ private
36
+
37
+ def socket_type(type_symbol)
38
+ case type_symbol
39
+ when :ROUTER, :PUB, :REP
40
+ zmq[type_symbol]
41
+ else
42
+ raise ArgumentError, "Unknown ZMQ socket type: #{type_symbol}"
43
+ end
44
+ end
45
+
46
+ def make_socket(type_symbol, protocol, host, port)
47
+ type = socket_type(type_symbol)
48
+ sock = zmq_context.socket(type)
49
+ bind_socket(sock, protocol, host, port)
50
+ sock
51
+ end
52
+
53
+ def bind_socket(sock, protocol, host, port)
54
+ iface = "#{protocol}://#{host}"
55
+ case protocol
56
+ when 'tcp'
57
+ if port <= 0
58
+ port = sock.bind_to_random_port(iface)
59
+ else
60
+ sock.bind("#{iface}:#{port}")
61
+ end
62
+ else
63
+ raise ArgumentError, "Unsupported protocol: #{protocol}"
64
+ end
65
+ [sock, port]
66
+ end
67
+
68
+ def zmq_context
69
+ zmq.Context.instance
70
+ end
71
+
72
+ def zmq
73
+ self.class.zmq
74
+ end
75
+ end
76
+ end
77
+ end