iruby 0.3 → 0.4.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 (41) hide show
  1. checksums.yaml +5 -5
  2. data/.travis.yml +35 -10
  3. data/Gemfile +4 -0
  4. data/README.md +115 -84
  5. data/Rakefile +26 -0
  6. data/ci/Dockerfile.base.erb +41 -0
  7. data/ci/Dockerfile.main.erb +9 -0
  8. data/ci/requirements.txt +1 -0
  9. data/docker/setup.sh +15 -0
  10. data/docker/test.sh +7 -0
  11. data/iruby.gemspec +4 -2
  12. data/lib/iruby.rb +13 -7
  13. data/lib/iruby/command.rb +67 -11
  14. data/lib/iruby/input/README.md +299 -0
  15. data/lib/iruby/jupyter.rb +76 -0
  16. data/lib/iruby/kernel.rb +49 -9
  17. data/lib/iruby/ostream.rb +4 -0
  18. data/lib/iruby/session.rb +116 -0
  19. data/lib/iruby/session/cztop.rb +4 -0
  20. data/lib/iruby/session/rbczmq.rb +5 -1
  21. data/lib/iruby/session_adapter.rb +68 -0
  22. data/lib/iruby/session_adapter/cztop_adapter.rb +45 -0
  23. data/lib/iruby/session_adapter/ffirzmq_adapter.rb +55 -0
  24. data/lib/iruby/session_adapter/pyzmq_adapter.rb +76 -0
  25. data/lib/iruby/session_adapter/rbczmq_adapter.rb +33 -0
  26. data/lib/iruby/utils.rb +1 -2
  27. data/lib/iruby/version.rb +1 -1
  28. data/run-test.sh +12 -0
  29. data/tasks/ci.rake +65 -0
  30. data/test/integration_test.rb +22 -10
  31. data/test/iruby/command_test.rb +208 -0
  32. data/test/iruby/jupyter_test.rb +28 -0
  33. data/test/iruby/multi_logger_test.rb +1 -1
  34. data/test/iruby/session_adapter/cztop_adapter_test.rb +20 -0
  35. data/test/iruby/session_adapter/ffirzmq_adapter_test.rb +20 -0
  36. data/test/iruby/session_adapter/rbczmq_adapter_test.rb +37 -0
  37. data/test/iruby/session_adapter/session_adapter_test_base.rb +29 -0
  38. data/test/iruby/session_adapter_test.rb +116 -0
  39. data/test/iruby/session_test.rb +53 -0
  40. data/test/test_helper.rb +44 -1
  41. metadata +72 -12
@@ -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,76 @@
1
+ module IRuby
2
+ module SessionAdapter
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
+
11
+ class << self
12
+ attr_reader :zmq
13
+ end
14
+
15
+ def make_router_socket(protocol, host, port)
16
+ make_socket(:ROUTER, protocol, host, port)
17
+ end
18
+
19
+ def make_pub_socket(protocol, host, port)
20
+ make_socket(:PUB, protocol, host, port)
21
+ end
22
+
23
+ def make_pub_socket(protocol, host, port)
24
+ make_socket(:REP, protocol, host, port)
25
+ end
26
+
27
+ def heartbeat_loop(sock)
28
+ PyCall.sys.path.append(File.expand_path('../pyzmq', __FILE__))
29
+ heartbeat = PyCall.import_module('iruby.heartbeat')
30
+ @heartbeat_thread = heartbeat.Heartbeat.new(sock)
31
+ @heartbeat_thread.start
32
+ end
33
+
34
+ private
35
+
36
+ def socket_type(type_symbol)
37
+ case type_symbol
38
+ when :ROUTER, :PUB, :REP
39
+ zmq[type_symbol]
40
+ else
41
+ raise ArgumentError, "Unknown ZMQ socket type: #{type_symbol}"
42
+ end
43
+ end
44
+
45
+ def make_socket(type_symbol, protocol, host, port)
46
+ type = socket_type(type_symbol)
47
+ sock = zmq_context.socket(type)
48
+ bind_socket(sock, protocol, host, port)
49
+ sock
50
+ end
51
+
52
+ def bind_socket(sock, protocol, host, port)
53
+ iface = "#{protocol}://#{host}"
54
+ case protocol
55
+ when 'tcp'
56
+ if port <= 0
57
+ port = sock.bind_to_random_port(iface)
58
+ else
59
+ sock.bind("#{iface}:#{port}")
60
+ end
61
+ else
62
+ raise ArgumentError, "Unsupported protocol: #{protocol}"
63
+ end
64
+ [sock, port]
65
+ end
66
+
67
+ def zmq_context
68
+ zmq.Context.instance
69
+ end
70
+
71
+ def zmq
72
+ self.class.zmq
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,33 @@
1
+ module IRuby
2
+ module SessionAdapter
3
+ class RbczmqAdapter < BaseAdapter
4
+ def self.load_requirements
5
+ require 'rbczmq'
6
+ end
7
+
8
+ def send(sock, data)
9
+ sock.send_message(ZMQ::Message(*data))
10
+ end
11
+
12
+ def recv(sock)
13
+ sock.recv_message
14
+ end
15
+
16
+ def heartbeat_loop(sock)
17
+ ZMQ.proxy(sock, sock)
18
+ end
19
+
20
+ private
21
+
22
+ def make_socket(type, protocol, host, port)
23
+ zmq_context.socket(type).tap do |sock|
24
+ sock.bind("#{protocol}://#{host}:#{port}")
25
+ end
26
+ end
27
+
28
+ def zmq_context
29
+ @zmq_context ||= ZMQ::Context.new
30
+ end
31
+ end
32
+ end
33
+ end
@@ -7,8 +7,7 @@ module IRuby
7
7
  def display(obj, options = {})
8
8
  Kernel.instance.session.send(:publish, :display_data,
9
9
  data: Display.display(obj, options),
10
- metadata: {},
11
- source: 'ruby') unless obj.nil?
10
+ metadata: {}) unless obj.nil?
12
11
  end
13
12
 
14
13
  def table(s, **options)
@@ -1,3 +1,3 @@
1
1
  module IRuby
2
- VERSION = '0.3'
2
+ VERSION = '0.4.0'
3
3
  end
@@ -0,0 +1,12 @@
1
+ #! /bin/bash
2
+
3
+ set -ex
4
+
5
+ export PYTHON=python3
6
+
7
+ ADAPTERS="cztop rbczmq ffi-rzmq pyzmq"
8
+
9
+ for adapter in $ADAPTERS; do
10
+ export IRUBY_TEST_SESSION_ADAPTER_NAME=$adapter
11
+ bundle exec rake test TESTOPTS=-v
12
+ done
@@ -0,0 +1,65 @@
1
+ namespace :ci do
2
+ namespace :docker do
3
+ def ruby_version
4
+ @ruby_version ||= ENV['ruby_version']
5
+ end
6
+
7
+ def ruby_image_name
8
+ @ruby_image_name ||= "rubylang/ruby:#{ruby_version}-bionic"
9
+ end
10
+
11
+ def iruby_test_base_image_name
12
+ @iruby_test_base_image_name ||= "iruby-test-base:ruby-#{ruby_version}"
13
+ end
14
+
15
+ def iruby_test_image_name
16
+ @docker_image_name ||= begin
17
+ "sciruby/iruby-test:ruby-#{ruby_version}"
18
+ end
19
+ end
20
+
21
+ def docker_image_found?(image_name)
22
+ image_id = `docker images -q #{image_name}`.chomp
23
+ image_id.length > 0
24
+ end
25
+
26
+ directory 'tmp'
27
+
28
+ desc "Build iruby-test-base docker image"
29
+ task :build_test_base_image => 'tmp' do
30
+ unless docker_image_found?(iruby_test_base_image_name)
31
+ require 'erb'
32
+ dockerfile_content = ERB.new(File.read('ci/Dockerfile.base.erb')).result(binding)
33
+ File.write('tmp/Dockerfile', dockerfile_content)
34
+ sh 'docker', 'build', '-t', iruby_test_base_image_name, '-f', 'tmp/Dockerfile', '.'
35
+ end
36
+ end
37
+
38
+ desc "Pull docker image of ruby"
39
+ task :pull_ruby_image do
40
+ sh 'docker', 'pull', ruby_image_name
41
+ end
42
+
43
+ desc "Build iruby-test docker image"
44
+ task :build_test_image => 'tmp' do
45
+ require 'erb'
46
+ dockerfile_content = ERB.new(File.read('ci/Dockerfile.main.erb')).result(binding)
47
+ File.write('tmp/Dockerfile', dockerfile_content)
48
+ sh 'docker', 'build', '-t', iruby_test_image_name, '-f', 'tmp/Dockerfile', '.'
49
+ end
50
+
51
+ desc 'before_install script for CI with Docker'
52
+ task :before_install => :pull_ruby_image
53
+ task :before_install => :build_test_base_image
54
+
55
+ desc 'install script for CI with Docker'
56
+ task :install => :build_test_image
57
+
58
+ desc 'main script for CI with Docker'
59
+ task :script do
60
+ volumes = ['-v', "#{Dir.pwd}:/iruby"] if ENV['attach_pwd']
61
+ sh 'docker', 'run', '--rm', '-it', *volumes,
62
+ iruby_test_image_name, 'bash', 'run-test.sh'
63
+ end
64
+ end
65
+ end
@@ -1,40 +1,52 @@
1
1
  require 'test_helper'
2
- require 'open3'
2
+ require 'pty'
3
3
  require 'expect'
4
4
 
5
- class IntegrationTest < IRubyTest
5
+ class IRubyTest::IntegrationTest < IRubyTest::TestBase
6
6
  def setup
7
- @stdin, @stdout, @stderr, @process = Open3.popen3('bin/iruby')
7
+ $expect_verbose = false # make true if you want to dump the output of iruby console
8
+
9
+ @in, @out, pid = PTY.spawn('bin/iruby --simple-prompt')
10
+ @waiter = Thread.start { Process.waitpid(pid) }
8
11
  expect 'In [', 30
9
12
  expect '1'
10
13
  expect ']:'
11
14
  end
12
15
 
13
16
  def teardown
14
- @stdin.close
15
- @stdout.close
16
- @stderr.close
17
- @process.kill
17
+ @in.close
18
+ @out.close
19
+ @waiter.join
18
20
  end
19
21
 
20
22
  def write(input)
21
- @stdin.puts input
23
+ @out.puts input
22
24
  end
23
25
 
24
- def expect(pattern, timeout = 1)
25
- assert @stdout.expect(pattern, timeout), "#{pattern} expected"
26
+ def expect(pattern, timeout = 10)
27
+ assert @in.expect(pattern, timeout), "#{pattern} expected, but timeout"
28
+ end
29
+
30
+ def wait_prompt
31
+ expect 'In ['
32
+ expect ']:'
26
33
  end
27
34
 
28
35
  def test_interaction
36
+ skip "This test too much unstable"
37
+
29
38
  write '"Hello, world!"'
30
39
  expect '"Hello, world!"'
31
40
 
41
+ wait_prompt
32
42
  write 'puts "Hello!"'
33
43
  expect 'Hello!'
34
44
 
45
+ wait_prompt
35
46
  write '12 + 12'
36
47
  expect '24'
37
48
 
49
+ wait_prompt
38
50
  write 'ls'
39
51
  expect 'self.methods'
40
52
  end
@@ -0,0 +1,208 @@
1
+ require 'test_helper'
2
+ require 'iruby/command'
3
+
4
+ module IRubyTest
5
+ class CommandTest < TestBase
6
+ def test_with_empty_argv
7
+ with_env('JUPYTER_DATA_DIR' => nil,
8
+ 'IPYTHONDIR' => nil) do
9
+ assert_output(nil, /\A\z/) do
10
+ @command = IRuby::Command.new([])
11
+ kernel_dir = File.join(IRuby::Jupyter.kernelspec_dir, 'ruby')
12
+ assert_equal(kernel_dir, @command.kernel_dir)
13
+ assert_equal(File.join(kernel_dir, 'kernel.json'), @command.kernel_file)
14
+ end
15
+ end
16
+ end
17
+
18
+ def test_with_JUPYTER_DATA_DIR
19
+ Dir.mktmpdir do |tmpdir|
20
+ with_env('JUPYTER_DATA_DIR' => tmpdir,
21
+ 'IPYTHONDIR' => nil) do
22
+ assert_output(nil, /\A\z/) do
23
+ @command = IRuby::Command.new([])
24
+ kernel_dir = File.join(tmpdir, 'kernels', 'ruby')
25
+ assert_equal(kernel_dir, @command.kernel_dir)
26
+ assert_equal(File.join(kernel_dir, 'kernel.json'), @command.kernel_file)
27
+ end
28
+ end
29
+ end
30
+ end
31
+
32
+ def test_with_IPYTHONDIR
33
+ Dir.mktmpdir do |tmpdir|
34
+ with_env('JUPYTER_DATA_DIR' => nil,
35
+ 'IPYTHONDIR' => tmpdir) do
36
+ assert_output(nil, /IPYTHONDIR is deprecated\. Use JUPYTER_DATA_DIR instead\./) do
37
+ @command = IRuby::Command.new([])
38
+ kernel_dir = File.join(tmpdir, 'kernels', 'ruby')
39
+ assert_equal(kernel_dir, @command.kernel_dir)
40
+ assert_equal(File.join(kernel_dir, 'kernel.json'), @command.kernel_file)
41
+ end
42
+ end
43
+ end
44
+ end
45
+
46
+ def test_with_JUPYTER_DATA_DIR_and_IPYTHONDIR
47
+ Dir.mktmpdir do |tmpdir|
48
+ Dir.mktmpdir do |tmpdir2|
49
+ with_env('JUPYTER_DATA_DIR' => tmpdir,
50
+ 'IPYTHONDIR' => tmpdir2) do
51
+ assert_output(nil, /both JUPYTER_DATA_DIR and IPYTHONDIR are supplied; IPYTHONDIR is ignored\./) do
52
+ @command = IRuby::Command.new([])
53
+ kernel_dir = File.join(tmpdir, 'kernels', 'ruby')
54
+ assert_equal(kernel_dir, @command.kernel_dir)
55
+ assert_equal(File.join(kernel_dir, 'kernel.json'), @command.kernel_file)
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
61
+
62
+ def test_with_ipython_dir_option
63
+ Dir.mktmpdir do |tmpdir|
64
+ with_env('JUPYTER_DATA_DIR' => nil,
65
+ 'IPYTHONDIR' => nil) do
66
+ assert_output(nil, /--ipython-dir is deprecated\. Use JUPYTER_DATA_DIR environment variable instead\./) do
67
+ @command = IRuby::Command.new(%W[--ipython-dir=#{tmpdir}])
68
+ kernel_dir = File.join(tmpdir, 'kernels', 'ruby')
69
+ assert_equal(kernel_dir, @command.kernel_dir)
70
+ assert_equal(File.join(kernel_dir, 'kernel.json'), @command.kernel_file)
71
+ end
72
+ end
73
+ end
74
+ end
75
+
76
+ def test_with_JUPYTER_DATA_DIR_and_ipython_dir_option
77
+ Dir.mktmpdir do |tmpdir|
78
+ Dir.mktmpdir do |tmpdir2|
79
+ with_env('JUPYTER_DATA_DIR' => tmpdir,
80
+ 'IPYTHONDIR' => nil) do
81
+ assert_output(nil, /both JUPYTER_DATA_DIR and --ipython-dir are supplied; --ipython-dir is ignored\./) do
82
+ @command = IRuby::Command.new(%W[--ipython-dir=#{tmpdir2}])
83
+ kernel_dir = File.join(tmpdir, 'kernels', 'ruby')
84
+ assert_equal(kernel_dir, @command.kernel_dir)
85
+ assert_equal(File.join(kernel_dir, 'kernel.json'), @command.kernel_file)
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
91
+
92
+ def test_register_when_there_is_kernel_in_ipython_dir
93
+ Dir.mktmpdir do |tmpdir|
94
+ Dir.mktmpdir do |tmpdir2|
95
+ with_env('JUPYTER_DATA_DIR' => nil,
96
+ 'IPYTHONDIR' => nil,
97
+ 'HOME' => tmpdir2) do
98
+ ignore_warning do
99
+ @command = IRuby::Command.new(["register", "--ipython-dir=~/.ipython"])
100
+ assert_equal("#{tmpdir2}/.ipython/kernels/ruby/kernel.json", @command.kernel_file)
101
+ @command.run
102
+ assert(File.file?("#{tmpdir2}/.ipython/kernels/ruby/kernel.json"))
103
+ end
104
+ end
105
+
106
+ with_env('JUPYTER_DATA_DIR' => nil,
107
+ 'IPYTHONDIR' => nil,
108
+ 'HOME' => tmpdir2) do
109
+ @command = IRuby::Command.new(["register"])
110
+ assert_output(nil, /IRuby kernel file already exists in the deprecated IPython's data directory\.\nUsing --force, you can replace the old kernel file with the new one in Jupyter's data directory\./) do
111
+ @command.run
112
+ end
113
+ assert(File.file?(File.join(@command.ipython_kernel_dir, 'kernel.json')))
114
+ refute(File.file?(@command.kernel_file))
115
+
116
+ @command = IRuby::Command.new(["register", "--force"])
117
+ assert_output(nil, nil) do
118
+ @command.run
119
+ end
120
+ refute(File.exist?(@command.ipython_kernel_dir))
121
+ assert(File.file?(@command.kernel_file))
122
+ end
123
+ end
124
+ end
125
+ end
126
+
127
+ def test_register_and_unregister_with_JUPYTER_DATA_DIR
128
+ Dir.mktmpdir do |tmpdir|
129
+ Dir.mktmpdir do |tmpdir2|
130
+ with_env('JUPYTER_DATA_DIR' => tmpdir,
131
+ 'IPYTHONDIR' => nil,
132
+ 'HOME' => tmpdir2) do
133
+ assert_output(nil, nil) do
134
+ @command = IRuby::Command.new(['register'])
135
+ kernel_dir = File.join(tmpdir, 'kernels', 'ruby')
136
+ kernel_file = File.join(kernel_dir, 'kernel.json')
137
+ assert(!File.file?(kernel_file))
138
+
139
+ @command.run
140
+ assert(File.file?(kernel_file))
141
+
142
+ @command = IRuby::Command.new(['unregister'])
143
+ @command.run
144
+ assert(!File.file?(kernel_file))
145
+ end
146
+ end
147
+ end
148
+ end
149
+ end
150
+
151
+ def test_register_and_unregister_with_JUPYTER_DATA_DIR_when_there_is_kernel_in_ipython_dir
152
+ Dir.mktmpdir do |tmpdir|
153
+ Dir.mktmpdir do |tmpdir2|
154
+ with_env('HOME' => tmpdir2) do
155
+ ignore_warning do
156
+ @command = IRuby::Command.new(["register", "--ipython-dir=~/.ipython"])
157
+ assert_equal("#{tmpdir2}/.ipython/kernels/ruby/kernel.json", @command.kernel_file)
158
+ @command.run
159
+ assert(File.file?("#{tmpdir2}/.ipython/kernels/ruby/kernel.json"))
160
+ end
161
+ end
162
+
163
+ with_env('JUPYTER_DATA_DIR' => tmpdir,
164
+ 'IPYTHONDIR' => nil,
165
+ 'HOME' => tmpdir2) do
166
+ @command = IRuby::Command.new(["register"])
167
+ assert_output(nil, /IRuby kernel file already exists in the deprecated IPython's data directory\.\nUsing --force, you can replace the old kernel file with the new one in Jupyter's data directory\./) do
168
+ @command.run
169
+ end
170
+ assert(File.file?(File.join(@command.ipython_kernel_dir, 'kernel.json')))
171
+ refute(File.file?(@command.kernel_file))
172
+
173
+ @command = IRuby::Command.new(["register", "--force"])
174
+ assert_output(nil, nil) do
175
+ @command.run
176
+ end
177
+ refute(File.file?(File.join(@command.ipython_kernel_dir, 'kernel.json')))
178
+ assert(File.file?(@command.kernel_file))
179
+ end
180
+ end
181
+ end
182
+ end
183
+
184
+ def test_register_and_unregister_with_IPYTHONDIR
185
+ Dir.mktmpdir do |tmpdir|
186
+ Dir.mktmpdir do |tmpdir2|
187
+ with_env('JUPYTER_DATA_DIR' => nil,
188
+ 'IPYTHONDIR' => tmpdir,
189
+ 'HOME' => tmpdir2) do
190
+ ignore_warning do
191
+ @command = IRuby::Command.new(['register'])
192
+ kernel_dir = File.join(tmpdir, 'kernels', 'ruby')
193
+ kernel_file = File.join(kernel_dir, 'kernel.json')
194
+ assert(!File.file?(kernel_file))
195
+
196
+ @command.run
197
+ assert(File.file?(kernel_file))
198
+
199
+ @command = IRuby::Command.new(['unregister'])
200
+ @command.run
201
+ assert(!File.file?(kernel_file))
202
+ end
203
+ end
204
+ end
205
+ end
206
+ end
207
+ end
208
+ end