├── ci
├── requirements.txt
├── Dockerfile.main.erb
└── Dockerfile.base.erb
├── lib
├── iruby
│ ├── assets
│ │ ├── kernel.css
│ │ ├── logo-32x32.png
│ │ ├── logo-64x64.png
│ │ └── kernel.js
│ ├── version.rb
│ ├── error.rb
│ ├── input
│ │ ├── textarea.rb
│ │ ├── label.rb
│ │ ├── cancel.rb
│ │ ├── autoload.rb
│ │ ├── field.rb
│ │ ├── widget.rb
│ │ ├── date.rb
│ │ ├── button.rb
│ │ ├── popup.rb
│ │ ├── select.rb
│ │ ├── file.rb
│ │ ├── builder.rb
│ │ ├── radio.rb
│ │ ├── multiple.rb
│ │ ├── checkbox.rb
│ │ ├── form.rb
│ │ ├── README.md
│ │ └── README.ipynb
│ ├── logger.rb
│ ├── input.rb
│ ├── event_manager.rb
│ ├── session_adapter
│ │ ├── cztop_adapter.rb
│ │ ├── test_adapter.rb
│ │ └── ffirzmq_adapter.rb
│ ├── comm.rb
│ ├── utils.rb
│ ├── session
│ │ ├── mixin.rb
│ │ ├── cztop.rb
│ │ └── ffi_rzmq.rb
│ ├── ostream.rb
│ ├── session_adapter.rb
│ ├── jupyter.rb
│ ├── kernel_app.rb
│ ├── session.rb
│ ├── formatter.rb
│ ├── backend.rb
│ ├── kernel.rb
│ ├── application.rb
│ └── display.rb
└── iruby.rb
├── logo
├── logo-32x32.png
└── logo-64x64.png
├── docker
├── test.sh
└── setup.sh
├── exe
└── iruby
├── .github
├── dependabot.yml
└── workflows
│ └── ci.yml
├── .gitignore
├── run-test.sh
├── test
├── run-test.rb
├── iruby
│ ├── session_adapter
│ │ ├── cztop_adapter_test.rb
│ │ ├── ffirzmq_adapter_test.rb
│ │ └── session_adapter_test_base.rb
│ ├── utils_test.rb
│ ├── multi_logger_test.rb
│ ├── application
│ │ ├── application_test.rb
│ │ ├── helper.rb
│ │ ├── unregister_test.rb
│ │ ├── kernel_test.rb
│ │ ├── console_test.rb
│ │ └── register_test.rb
│ ├── jupyter_test.rb
│ ├── backend_test.rb
│ ├── mime_test.rb
│ ├── session_test.rb
│ ├── session_adapter_test.rb
│ ├── event_manager_test.rb
│ ├── kernel_test.rb
│ └── display_test.rb
├── integration_test.rb
└── helper.rb
├── Gemfile
├── Rakefile
├── LICENSE
├── iruby.gemspec
├── tasks
└── ci.rake
├── README.md
└── CHANGES.md
/ci/requirements.txt:
--------------------------------------------------------------------------------
1 | jupyter-console>=6.0.0
2 |
--------------------------------------------------------------------------------
/lib/iruby/assets/kernel.css:
--------------------------------------------------------------------------------
1 | /* Placeholder for Ruby kernel.css */
2 |
--------------------------------------------------------------------------------
/lib/iruby/version.rb:
--------------------------------------------------------------------------------
1 | module IRuby
2 | VERSION = '0.8.2'
3 | end
4 |
--------------------------------------------------------------------------------
/logo/logo-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SciRuby/iruby/HEAD/logo/logo-32x32.png
--------------------------------------------------------------------------------
/logo/logo-64x64.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SciRuby/iruby/HEAD/logo/logo-64x64.png
--------------------------------------------------------------------------------
/lib/iruby/assets/logo-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SciRuby/iruby/HEAD/lib/iruby/assets/logo-32x32.png
--------------------------------------------------------------------------------
/lib/iruby/assets/logo-64x64.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SciRuby/iruby/HEAD/lib/iruby/assets/logo-64x64.png
--------------------------------------------------------------------------------
/docker/test.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | set -ex
4 |
5 | cd /tmp/iruby
6 | bundle install --with test --without plot
7 | bundle exec rake test
8 |
--------------------------------------------------------------------------------
/exe/iruby:
--------------------------------------------------------------------------------
1 | #! /usr/bin/env ruby
2 | require "iruby"
3 | require "iruby/application"
4 |
5 | app = IRuby::Application.instance
6 | app.setup
7 | app.run
8 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: "github-actions"
4 | directory: "/"
5 | schedule:
6 | interval: "weekly"
7 |
--------------------------------------------------------------------------------
/ci/Dockerfile.main.erb:
--------------------------------------------------------------------------------
1 | FROM iruby-test-base:ruby-<%= ruby_version %>
2 |
3 | RUN gem install cztop
4 | RUN mkdir -p /iruby
5 | ADD . /iruby
6 | WORKDIR /iruby
7 | RUN bundle install
8 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .ipynb_checkpoints/
2 | *.gem
3 | *.rbc
4 | *.log
5 | .bundle
6 | .config
7 | .yardoc
8 | Gemfile.lock
9 | coverage
10 | doc/
11 | pkg
12 | rdoc
13 | test/tmp
14 | test/version_tmp
15 | tmp
16 | venv
17 |
--------------------------------------------------------------------------------
/run-test.sh:
--------------------------------------------------------------------------------
1 | #! /bin/bash
2 |
3 | set -ex
4 |
5 | export PYTHON=python3
6 |
7 | ADAPTERS="cztop ffi-rzmq"
8 |
9 | for adapter in $ADAPTERS; do
10 | export IRUBY_TEST_SESSION_ADAPTER_NAME=$adapter
11 | bundle exec rake test TESTOPTS=-v
12 | done
13 |
--------------------------------------------------------------------------------
/lib/iruby/error.rb:
--------------------------------------------------------------------------------
1 | module IRuby
2 | class Error < StandardError
3 | end
4 |
5 | class InvalidSubcommandError < Error
6 | def initialize(name, argv)
7 | @name = name
8 | @argv = argv
9 | super("Invalid subcommand name: #{@name}")
10 | end
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/docker/setup.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | set -ex
4 |
5 | apt-get update
6 | apt-get install -y --no-install-recommends \
7 | libczmq-dev \
8 | python3 \
9 | python3-pip \
10 | python3-setuptools \
11 | python3-wheel
12 |
13 | cd /tmp/iruby
14 | bundle install --with test --without plot
15 | pip3 install jupyter
16 |
--------------------------------------------------------------------------------
/test/run-test.rb:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 |
3 | $VERBOSE = true
4 |
5 | require "bundler/setup"
6 | require "pathname"
7 |
8 | base_dir = Pathname.new(__dir__).parent.expand_path
9 |
10 | lib_dir = base_dir + "lib"
11 | test_dir = base_dir + "test"
12 |
13 | $LOAD_PATH.unshift(lib_dir.to_s)
14 |
15 | require_relative "helper"
16 |
17 | ENV["TEST_UNIT_MAX_DIFF_TARGET_STRING_SIZE"] ||= "10000"
18 | ENV["IRUBY_TEST_SESSION_ADAPTER_NAME"] ||= "ffi-rzmq"
19 |
20 | exit Test::Unit::AutoRunner.run(true, test_dir)
21 |
--------------------------------------------------------------------------------
/lib/iruby/assets/kernel.js:
--------------------------------------------------------------------------------
1 | // Ruby kernel.js
2 |
3 | define(['base/js/namespace'], function(IPython) {
4 | "use strict";
5 | var onload = function() {
6 | IPython.CodeCell.options_default['cm_config']['indentUnit'] = 2;
7 | var cells = IPython.notebook.get_cells();
8 | for (var i in cells){
9 | var c = cells[i];
10 | if (c.cell_type === 'code')
11 | c.code_mirror.setOption('indentUnit', 2);
12 | }
13 | }
14 | return {onload:onload};
15 | });
16 |
--------------------------------------------------------------------------------
/test/iruby/session_adapter/cztop_adapter_test.rb:
--------------------------------------------------------------------------------
1 | require_relative 'session_adapter_test_base'
2 | require 'iruby'
3 |
4 | module IRubyTest
5 | if ENV['IRUBY_TEST_SESSION_ADAPTER_NAME'] == 'cztop'
6 | class CztopAdapterTest < SessionAdapterTestBase
7 | def adapter_class
8 | IRuby::SessionAdapter::CztopAdapter
9 | end
10 |
11 | def test_send
12 | assert(adapter_class.available?)
13 | end
14 |
15 | def test_recv
16 | omit
17 | end
18 | end
19 | end
20 | end
21 |
--------------------------------------------------------------------------------
/test/iruby/session_adapter/ffirzmq_adapter_test.rb:
--------------------------------------------------------------------------------
1 | require_relative 'session_adapter_test_base'
2 | require 'iruby'
3 |
4 | module IRubyTest
5 | if ENV['IRUBY_TEST_SESSION_ADAPTER_NAME'] == 'ffi-rzmq'
6 | class FfirzmqAdapterTest < SessionAdapterTestBase
7 | def adapter_class
8 | IRuby::SessionAdapter::FfirzmqAdapter
9 | end
10 |
11 | def test_send
12 | assert(adapter_class.available?)
13 | end
14 |
15 | def test_recv
16 | omit
17 | end
18 | end
19 | end
20 | end
21 |
--------------------------------------------------------------------------------
/lib/iruby/input/textarea.rb:
--------------------------------------------------------------------------------
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
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source 'https://rubygems.org'
2 |
3 | plugin 'rubygems-requirements-system'
4 |
5 | gemspec
6 |
7 | group :pry do
8 | gem 'pry'
9 | gem 'pry-doc'
10 | gem 'awesome_print'
11 | end
12 |
13 | group :plot do
14 | gem 'gnuplot'
15 | gem 'rubyvis'
16 | end
17 |
18 | group :test do
19 | gem 'cztop'
20 | end
21 |
22 | # Tests are failing on Ruby 3.3 because warnings related to OpenStruct are being written to the standard error output.
23 | # This is being captured by Open3.capture2e and then mistakenly parsed as JSON.
24 | # This gem can be removed when json gem is updated
25 | gem 'ostruct'
26 |
--------------------------------------------------------------------------------
/lib/iruby/input/label.rb:
--------------------------------------------------------------------------------
1 | module IRuby
2 | module Input
3 | class Label < Widget
4 | needs label: nil, icon: nil
5 |
6 | def widget_label
7 | div class: 'iruby-label input-group' do
8 | span class: 'input-group-addon' do
9 | text @label || to_label(@key)
10 | end
11 |
12 | yield
13 |
14 | if @icon
15 | span @icon, class: "input-group-addon"
16 | end
17 | end
18 | end
19 |
20 | private
21 |
22 | def to_label label
23 | label.to_s.tr('_',' ').capitalize
24 | end
25 | end
26 | end
27 | end
--------------------------------------------------------------------------------
/lib/iruby/logger.rb:
--------------------------------------------------------------------------------
1 | require 'logger'
2 |
3 | module IRuby
4 | class << self
5 | attr_accessor :logger
6 | end
7 |
8 | class MultiLogger < BasicObject
9 | def initialize(*loggers, level: ::Logger::DEBUG)
10 | @loggers = loggers
11 | @level = level
12 | end
13 |
14 | attr_reader :loggers
15 |
16 | attr_reader :level
17 |
18 | def level=(new_level)
19 | @loggers.each do |l|
20 | l.level = new_level
21 | end
22 | @level = new_level
23 | end
24 |
25 | def method_missing(name, *args, &b)
26 | @loggers.map {|x| x.respond_to?(name) && x.public_send(name, *args, &b) }.any?
27 | end
28 | end
29 | end
30 |
--------------------------------------------------------------------------------
/test/iruby/utils_test.rb:
--------------------------------------------------------------------------------
1 | module IRubyTest
2 | class UtilsTest < TestBase
3 | sub_test_case("IRuby.table") do
4 | def setup
5 | @data = {
6 | X: [ 1, 2, 3 ],
7 | Y: [ 4, 5, 6 ]
8 | }
9 | end
10 | sub_test_case("without header: option") do
11 | def test_table
12 | result = IRuby.table(@data)
13 | assert_include(result.object, "
X | ")
14 | end
15 | end
16 |
17 | sub_test_case("with header: false") do
18 | def test_table
19 | result = IRuby.table(@data, header: false)
20 | assert_not_include(result.object, "X | ")
21 | end
22 | end
23 | end
24 | end
25 | end
26 |
--------------------------------------------------------------------------------
/lib/iruby/input/cancel.rb:
--------------------------------------------------------------------------------
1 | module IRuby
2 | module Input
3 | class Cancel < Widget
4 | needs :label
5 |
6 | builder :cancel do |label='Cancel'|
7 | add_button Cancel.new(label: label)
8 | end
9 |
10 | def widget_css
11 | ".iruby-cancel { margin-left: 5px; }"
12 | end
13 |
14 | def widget_js
15 | <<-JS
16 | $('.iruby-cancel').click(function(){
17 | $('#iruby-form').remove();
18 | });
19 | JS
20 | end
21 |
22 | def widget_html
23 | button(
24 | @label,
25 | type: 'button',
26 | :'data-dismiss' => 'modal',
27 | class: "btn btn-danger pull-right iruby-cancel"
28 | )
29 | end
30 | end
31 | end
32 | end
--------------------------------------------------------------------------------
/lib/iruby/input/autoload.rb:
--------------------------------------------------------------------------------
1 | begin
2 | require 'erector'
3 | rescue LoadError
4 | raise LoadError, <<-ERROR.gsub(/\s+/,' ')
5 | IRuby::Input requires the erector gem.
6 | `gem install erector` or add `gem 'erector'`
7 | it to your Gemfile to continue.
8 | ERROR
9 | end
10 |
11 | require 'iruby/input/builder'
12 | require 'iruby/input/widget'
13 | require 'iruby/input/form'
14 | require 'iruby/input/label'
15 | require 'iruby/input/field'
16 | require 'iruby/input/popup'
17 | require 'iruby/input/button'
18 | require 'iruby/input/cancel'
19 | require 'iruby/input/file'
20 | require 'iruby/input/select'
21 | require 'iruby/input/checkbox'
22 | require 'iruby/input/radio'
23 | require 'iruby/input/textarea'
24 | require 'iruby/input/date'
25 | require 'iruby/input/multiple'
--------------------------------------------------------------------------------
/lib/iruby/input/field.rb:
--------------------------------------------------------------------------------
1 | module IRuby
2 | module Input
3 | class Field < Label
4 | needs default: nil, type: 'text', js_class: 'iruby-field'
5 |
6 | builder :input do |key='input', **params|
7 | params[:key] = unique_key key
8 | add_field Field.new(**params)
9 | end
10 |
11 | def widget_js
12 | <<-JS
13 | $('.iruby-field').keyup(function() {
14 | $(this).data('iruby-value', $(this).val());
15 | });
16 | JS
17 | end
18 |
19 | def widget_html
20 | widget_label do
21 | input(
22 | type: @type,
23 | :'data-iruby-key' => @key,
24 | class: "form-control #{@js_class}",
25 | value: @default
26 | )
27 | end
28 | end
29 | end
30 | end
31 | end
--------------------------------------------------------------------------------
/test/iruby/session_adapter/session_adapter_test_base.rb:
--------------------------------------------------------------------------------
1 | module IRubyTest
2 | class SessionAdapterTestBase < TestBase
3 | # https://jupyter-client.readthedocs.io/en/stable/kernels.html#connection-files
4 | def make_connection_config
5 | {
6 | "control_port" => 50160,
7 | "shell_port" => 57503,
8 | "transport" => "tcp",
9 | "signature_scheme" => "hmac-sha256",
10 | "stdin_port" => 52597,
11 | "hb_port" => 42540,
12 | "ip" => "127.0.0.1",
13 | "iopub_port" => 40885,
14 | "key" => "a0436f6c-1916-498b-8eb9-e81ab9368e84"
15 | }
16 | end
17 |
18 | def setup
19 | @config = make_connection_config
20 | @session_adapter = adapter_class.new(@config)
21 |
22 | unless adapter_class.available?
23 | omit("#{@session_adapter.name} is unavailable")
24 | end
25 | end
26 | end
27 | end
28 |
--------------------------------------------------------------------------------
/lib/iruby/input/widget.rb:
--------------------------------------------------------------------------------
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
--------------------------------------------------------------------------------
/test/iruby/multi_logger_test.rb:
--------------------------------------------------------------------------------
1 | class IRubyTest::MultiLoggerTest < IRubyTest::TestBase
2 | def test_multilogger
3 | out, err = StringIO.new, StringIO.new
4 | logger = IRuby::MultiLogger.new(Logger.new(out), Logger.new(err))
5 | logger.warn 'You did a bad thing'
6 | assert_match 'WARN', out.string
7 | assert_match 'WARN', err.string
8 | assert_match 'bad thing', out.string
9 | assert_match 'bad thing', err.string
10 | end
11 |
12 | def test_level
13 | out, err = StringIO.new, StringIO.new
14 | logger = IRuby::MultiLogger.new(Logger.new(out), Logger.new(err))
15 |
16 | logger.level = Logger::DEBUG
17 | assert_equal Logger::DEBUG, logger.level
18 | assert_all(logger.loggers) {|l| l.level == Logger::DEBUG }
19 |
20 | logger.level = Logger::INFO
21 | assert_equal Logger::INFO, logger.level
22 | assert_all(logger.loggers) {|l| l.level == Logger::INFO }
23 | end
24 | end
25 |
--------------------------------------------------------------------------------
/lib/iruby.rb:
--------------------------------------------------------------------------------
1 | require 'fileutils'
2 | require 'mime/types'
3 | require 'json'
4 | require 'securerandom'
5 | require 'openssl'
6 | require 'tempfile'
7 | require 'set'
8 | require 'stringio'
9 |
10 | require 'iruby/version'
11 | require 'iruby/jupyter'
12 | require 'iruby/event_manager'
13 | require 'iruby/logger'
14 | require 'iruby/kernel'
15 | require 'iruby/backend'
16 | require 'iruby/ostream'
17 | require 'iruby/input'
18 | require 'iruby/formatter'
19 | require 'iruby/utils'
20 | require 'iruby/display'
21 | require 'iruby/comm'
22 |
23 | if ENV.fetch('IRUBY_OLD_SESSION', false)
24 | require 'iruby/session/mixin'
25 | begin
26 | require 'iruby/session/ffi_rzmq'
27 | rescue LoadError
28 | begin
29 | require 'iruby/session/cztop'
30 | rescue LoadError
31 | STDERR.puts "Please install ffi-rzmq or cztop before running iruby. See README."
32 | end
33 | end
34 | else
35 | require 'iruby/session'
36 | end
37 |
--------------------------------------------------------------------------------
/lib/iruby/input/date.rb:
--------------------------------------------------------------------------------
1 | module IRuby
2 | module Input
3 | class Date < Field
4 | needs js_class: 'iruby-date', icon: '📅'
5 |
6 | builder :date do |key='date', **params|
7 | params[:default] ||= false
8 | params[:key] = unique_key key
9 |
10 | if params[:default].is_a? Time
11 | params[:default] = params[:default].strftime('%m/%d/%Y')
12 | end
13 |
14 | add_field Date.new(**params)
15 |
16 | process params[:key] do |result,key,value|
17 | result[key.to_sym] = Time.strptime(value,'%m/%d/%Y')
18 | end
19 | end
20 |
21 | def widget_css
22 | '#ui-datepicker-div { z-index: 2000 !important; }'
23 | end
24 |
25 | def widget_js
26 | <<-JS
27 | $('.iruby-date').datepicker({
28 | dateFormat: 'mm/dd/yy',
29 | onClose: function(date) {
30 | $(this).data('iruby-value', date);
31 | }
32 | });
33 | JS
34 | end
35 | end
36 | end
37 | end
38 |
--------------------------------------------------------------------------------
/lib/iruby/input.rb:
--------------------------------------------------------------------------------
1 | module IRuby
2 | module Input
3 | # autoload so that erector is not a required
4 | # runtime dependency of IRuby
5 | autoload :Builder, 'iruby/input/autoload'
6 |
7 | def input prompt='Input'
8 | result = form{input label: prompt}
9 | result[:input] unless result.nil?
10 | end
11 |
12 | def password prompt='Password'
13 | result = form{password label: prompt}
14 | result[:password] unless result.nil?
15 | end
16 |
17 | def form &block
18 | builder = Builder.new(&block)
19 | form = InputForm.new(
20 | fields: builder.fields,
21 | buttons: builder.buttons
22 | )
23 | form.widget_display
24 | builder.process_result form.submit
25 | end
26 |
27 | def popup title='Input', &block
28 | builder = Builder.new(&block)
29 | form = InputForm.new fields: builder.fields
30 | popup = Popup.new(
31 | title: title,
32 | form: form,
33 | buttons: builder.buttons
34 | )
35 | popup.widget_display
36 | builder.process_result form.submit
37 | end
38 | end
39 |
40 | extend Input
41 | end
--------------------------------------------------------------------------------
/lib/iruby/event_manager.rb:
--------------------------------------------------------------------------------
1 | module IRuby
2 | class EventManager
3 | def initialize(available_events)
4 | @available_events = available_events.dup.freeze
5 | @callbacks = available_events.map {|n| [n, []] }.to_h
6 | end
7 |
8 | attr_reader :available_events
9 |
10 | def register(event, &block)
11 | check_available_event(event)
12 | @callbacks[event] << block unless block.nil?
13 | block
14 | end
15 |
16 | def unregister(event, callback)
17 | check_available_event(event)
18 | val = @callbacks[event].delete(callback)
19 | unless val
20 | raise ArgumentError,
21 | "Given callable object #{callback} is not registered as a #{event} callback"
22 | end
23 | val
24 | end
25 |
26 | def trigger(event, *args, **kwargs)
27 | check_available_event(event)
28 | @callbacks[event].each do |fn|
29 | fn.call(*args, **kwargs)
30 | end
31 | end
32 |
33 | private
34 |
35 | def check_available_event(event)
36 | return if @callbacks.key?(event)
37 | raise ArgumentError, "Unknown event name: #{event}", caller
38 | end
39 | end
40 | end
41 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | require "bundler/gem_helper"
2 |
3 | base_dir = File.join(File.dirname(__FILE__))
4 |
5 | helper = Bundler::GemHelper.new(base_dir)
6 | helper.install
7 |
8 | FileList['tasks/**.rake'].each {|f| load f }
9 |
10 | desc "Run tests"
11 | task :test do
12 | test_opts = ENV.fetch("TESTOPTS", "").split
13 | cd(base_dir) do
14 | ruby("test/run-test.rb", *test_opts)
15 | end
16 | end
17 |
18 | task default: 'test'
19 |
20 | namespace :docker do
21 | def root_dir
22 | @root_dir ||= File.expand_path("..", __FILE__)
23 | end
24 |
25 | task :build do
26 | container_name = "iruby_build"
27 | image_name = "mrkn/iruby"
28 | sh "docker", "run",
29 | "--name", container_name,
30 | "-v", "#{root_dir}:/tmp/iruby",
31 | "rubylang/ruby", "/bin/bash", "/tmp/iruby/docker/setup.sh"
32 | sh "docker", "commit", container_name, image_name
33 | sh "docker", "rm", container_name
34 | end
35 |
36 | task :test do
37 | root_dir = File.expand_path("..", __FILE__)
38 | sh "docker", "run", "-it", "--rm",
39 | "-v", "#{root_dir}:/tmp/iruby",
40 | "mrkn/iruby", "/bin/bash", "/tmp/iruby/docker/test.sh"
41 | end
42 | end
43 |
--------------------------------------------------------------------------------
/test/iruby/application/application_test.rb:
--------------------------------------------------------------------------------
1 | require_relative "helper"
2 |
3 | module IRubyTest::ApplicationTests
4 | class ApplicationTest < ApplicationTestBase
5 | DEFAULT_KERNEL_NAME = IRuby::Application::DEFAULT_KERNEL_NAME
6 | DEFAULT_DISPLAY_NAME = IRuby::Application::DEFAULT_DISPLAY_NAME
7 |
8 | def test_help
9 | out, status = Open3.capture2e(*iruby_command("--help"))
10 | assert status.success?
11 | assert_match(/--help/, out)
12 | assert_match(/--version/, out)
13 | assert_match(/^register\b/, out)
14 | assert_match(/^unregister\b/, out)
15 | assert_match(/^kernel\b/, out)
16 | assert_match(/^console\b/, out)
17 | end
18 |
19 | def test_version
20 | out, status = Open3.capture2e(*iruby_command("--version"))
21 | assert status.success?
22 | assert_match(/\bIRuby\s+#{Regexp.escape(IRuby::VERSION)}\b/, out)
23 | end
24 |
25 | def test_unknown_subcommand
26 | out, status = Open3.capture2e(*iruby_command("matz"))
27 | refute status.success?
28 | assert_match(/^Invalid subcommand name: matz$/, out)
29 | assert_match(/^Subcommands$/, out)
30 | end
31 | end
32 | end
33 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2013 - present IRuby contributors and the Ruby Science Foundation
2 |
3 | MIT License
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining
6 | a copy of this software and associated documentation files (the
7 | "Software"), to deal in the Software without restriction, including
8 | without limitation the rights to use, copy, modify, merge, publish,
9 | distribute, sublicense, and/or sell copies of the Software, and to
10 | permit persons to whom the Software is furnished to do so, subject to
11 | the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be
14 | included in all copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23 |
--------------------------------------------------------------------------------
/lib/iruby/session_adapter/cztop_adapter.rb:
--------------------------------------------------------------------------------
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
46 |
--------------------------------------------------------------------------------
/lib/iruby/input/button.rb:
--------------------------------------------------------------------------------
1 | module IRuby
2 | module Input
3 | # extend the label class for the to_label helper
4 | class Button < Label
5 | needs color: :blue, js_class: 'iruby-button'
6 |
7 | COLORS = {
8 | blue: 'primary',
9 | gray: 'secondary',
10 | green: 'success',
11 | aqua: 'info',
12 | orange: 'warning',
13 | red: 'danger',
14 | none: 'link'
15 | }
16 |
17 | COLORS.default = 'primary'
18 |
19 | builder :button do |key='done', **params|
20 | params[:key] = unique_key(key)
21 | add_button Button.new(**params)
22 | end
23 |
24 | def widget_css
25 | ".#{@js_class} { margin-left: 5px; }"
26 | end
27 |
28 | def widget_js
29 | <<-JS
30 | $('.iruby-button').click(function(){
31 | $(this).data('iruby-value', true);
32 | $('#iruby-form').submit();
33 | });
34 | JS
35 | end
36 |
37 | def widget_html
38 | button(
39 | @label || to_label(@key),
40 | type: 'button',
41 | :'data-iruby-key' => @key,
42 | class: "btn btn-#{COLORS[@color]} pull-right #{@js_class}"
43 | )
44 | end
45 | end
46 | end
47 | end
--------------------------------------------------------------------------------
/test/iruby/jupyter_test.rb:
--------------------------------------------------------------------------------
1 | require 'iruby/jupyter'
2 |
3 | module IRubyTest
4 | class JupyterDefaultKernelSpecDirectoryTest < TestBase
5 | sub_test_case("with JUPYTER_DATA_DIR") do
6 | def test_default
7 | assert_equal(File.join(ENV["JUPYTER_DATA_DIR"], "kernels"),
8 | IRuby::Jupyter.kernelspec_dir)
9 | end
10 | end
11 |
12 | sub_test_case("without JUPYTER_DATA_DIR environment variable") do
13 | def setup
14 | with_env("JUPYTER_DATA_DIR" => nil) do
15 | @kernelspec_dir = IRuby::Jupyter.kernelspec_dir
16 | yield
17 | end
18 | end
19 |
20 | def test_default_windows
21 | windows_only
22 | appdata = IRuby::Jupyter.send :windows_user_appdata
23 | assert_equal(File.join(appdata, 'jupyter/kernels'), @kernelspec_dir)
24 | end
25 |
26 | def test_default_apple
27 | apple_only
28 | assert_equal(File.expand_path('~/Library/Jupyter/kernels'), @kernelspec_dir)
29 | end
30 |
31 | def test_default_unix
32 | unix_only
33 | with_env('XDG_DATA_HOME' => nil) do
34 | assert_equal(File.expand_path('~/.local/share/jupyter/kernels'), @kernelspec_dir)
35 | end
36 | end
37 | end
38 | end
39 | end
40 |
--------------------------------------------------------------------------------
/lib/iruby/input/popup.rb:
--------------------------------------------------------------------------------
1 | module IRuby
2 | module Input
3 | class Popup < Widget
4 | needs :title, :form, buttons: []
5 |
6 | def widget_css
7 | style = '.modal-body { overflow: auto; }'
8 | widget_join :widget_css, style, @form, *@buttons
9 | end
10 |
11 | def widget_js
12 | js = <<-JS
13 | require(['base/js/dialog'], function(dialog) {
14 | var popup = dialog.modal({
15 | title: '#{@title.gsub("'"){"\\'"}}',
16 | body: '#{@form.to_html}',
17 | destroy: true,
18 | sanitize: false,
19 | keyboard_manager: Jupyter.notebook.keyboard_manager,
20 | open: function() {
21 | #{widget_join :widget_js, @form, *@buttons}
22 |
23 | var popup = $(this);
24 |
25 | $('#iruby-form').submit(function() {
26 | popup.modal('hide');
27 | });
28 |
29 | Jupyter.notebook.keyboard_manager.disable();
30 | }
31 | });
32 |
33 | popup.find('.modal-footer').each(function(e) {
34 | $(this).append('#{@buttons.map(&:to_html).join}');
35 | });
36 | });
37 | JS
38 | end
39 | end
40 | end
41 | end
42 |
--------------------------------------------------------------------------------
/test/iruby/backend_test.rb:
--------------------------------------------------------------------------------
1 | module IRubyTest
2 | class PlainBackendTest < IRubyTest::TestBase
3 | def setup
4 | @plainbackend = IRuby::PlainBackend.new
5 | end
6 |
7 | def test_eval_one_plus_two
8 | assert_equal 3, @plainbackend.eval('1+2', false)
9 | end
10 |
11 | def test_include_module
12 | assert_nothing_raised do
13 | @plainbackend.eval("include Math, Comparable", false)
14 | end
15 | end
16 |
17 | def test_complete_req
18 | assert_includes @plainbackend.complete('req'), 'require'
19 | end
20 |
21 | def test_complete_2_dot
22 | assert_includes @plainbackend.complete('2.'), '2.even?'
23 | end
24 | end
25 |
26 | class PryBackendTest < IRubyTest::TestBase
27 | def setup
28 | @prybackend = IRuby::PryBackend.new
29 | end
30 |
31 | def test_eval_one_plus_two
32 | assert_equal 3, @prybackend.eval('1+2', false)
33 | end
34 |
35 | def test_include_module
36 | assert_nothing_raised do
37 | @prybackend.eval("include Math, Comparable", false)
38 | end
39 | end
40 |
41 | def test_complete_req
42 | assert_includes @prybackend.complete('req'), 'require'
43 | end
44 |
45 | def test_complete_2_dot
46 | assert_includes @prybackend.complete('2.'), '2.even?'
47 | end
48 | end
49 | end
50 |
--------------------------------------------------------------------------------
/lib/iruby/comm.rb:
--------------------------------------------------------------------------------
1 | module IRuby
2 | # Comm is a new messaging system for bidirectional communication.
3 | # Both kernel and front-end listens for messages.
4 | class Comm
5 | attr_writer :on_msg, :on_close
6 |
7 | class << self
8 | def target; @target ||= {} end
9 | def comm; @comm ||= {} end
10 | end
11 |
12 | def initialize(target_name, comm_id = SecureRandom.uuid)
13 | @target_name, @comm_id = target_name, comm_id
14 | end
15 |
16 | def open(metadata = nil, **data)
17 | Kernel.instance.session.send(:publish, :comm_open, metadata, comm_id: @comm_id, data: data, target_name: @target_name)
18 | Comm.comm[@comm_id] = self
19 | end
20 |
21 | def send(metadata = nil, **data)
22 | Kernel.instance.session.send(:publish, :comm_msg, metadata, comm_id: @comm_id, data: data)
23 | end
24 |
25 | def close(metadata = nil, **data)
26 | Kernel.instance.session.send(:publish, :comm_close, metadata, comm_id: @comm_id, data: data)
27 | Comm.comm.delete(@comm_id)
28 | end
29 |
30 | def on_msg(&b)
31 | @on_msg = b
32 | end
33 |
34 | def on_close(&b)
35 | @on_close = b
36 | end
37 |
38 | def handle_msg(data)
39 | @on_msg.call(data) if @on_msg
40 | end
41 |
42 | def handle_close(data)
43 | @on_close.call(data) if @on_close
44 | end
45 | end
46 | end
47 |
--------------------------------------------------------------------------------
/lib/iruby/session_adapter/test_adapter.rb:
--------------------------------------------------------------------------------
1 | require 'iruby/session/mixin'
2 |
3 | module IRuby
4 | module SessionAdapter
5 | class TestAdapter < BaseAdapter
6 | include IRuby::SessionSerialize
7 |
8 | DummySocket = Struct.new(:type, :protocol, :host, :port)
9 |
10 | def initialize(config)
11 | super
12 |
13 | unless config['key'].empty? || config['signature_scheme'].empty?
14 | unless config['signature_scheme'] =~ /\Ahmac-/
15 | raise "Unknown signature_scheme: #{config['signature_scheme']}"
16 | end
17 | digest_algorithm = config['signature_scheme'][/\Ahmac-(.*)\Z/, 1]
18 | @hmac = OpenSSL::HMAC.new(config['key'], OpenSSL::Digest.new(digest_algorithm))
19 | end
20 |
21 | @send_callback = nil
22 | @recv_callback = nil
23 | end
24 |
25 | attr_accessor :send_callback, :recv_callback
26 |
27 | def send(sock, data)
28 | unless @send_callback.nil?
29 | @send_callback.call(sock, unserialize(data))
30 | end
31 | end
32 |
33 | def recv(sock)
34 | unless @recv_callback.nil?
35 | serialize(@recv_callback.call(sock))
36 | end
37 | end
38 |
39 | def heartbeat_loop(sock)
40 | end
41 |
42 | private
43 |
44 | def make_socket(type, protocol, host, port)
45 | DummySocket.new(type, protocol, host, port)
46 | end
47 | end
48 | end
49 | end
50 |
--------------------------------------------------------------------------------
/ci/Dockerfile.base.erb:
--------------------------------------------------------------------------------
1 | FROM rubylang/ruby:<%= ruby_version %>-bionic
2 |
3 | ADD ci/requirements.txt /tmp
4 |
5 | RUN apt-get update \
6 | && apt-get install -y --no-install-recommends \
7 | libczmq-dev \
8 | python3 \
9 | python3-pip \
10 | python3-setuptools \
11 | libpython3.6 \
12 | && pip3 install wheel \
13 | && pip3 install -r /tmp/requirements.txt \
14 | && rm -f /tmp/requirements.txt
15 |
16 | # ZeroMQ version 4.1.6 and CZMQ version 3.0.2 for rbczmq
17 | RUN apt-get update \
18 | && apt-get install -y --no-install-recommends \
19 | build-essential \
20 | file \
21 | wget \
22 | && cd /tmp \
23 | && wget https://github.com/zeromq/zeromq4-1/releases/download/v4.1.6/zeromq-4.1.6.tar.gz \
24 | && wget https://archive.org/download/zeromq_czmq_3.0.2/czmq-3.0.2.tar.gz \
25 | && tar xf zeromq-4.1.6.tar.gz \
26 | && tar xf czmq-3.0.2.tar.gz \
27 | && \
28 | ( \
29 | cd zeromq-4.1.6 \
30 | && ./configure \
31 | && make install \
32 | ) \
33 | && \
34 | ( \
35 | cd czmq-3.0.2 \
36 | && wget -O 1.patch https://github.com/zeromq/czmq/commit/2594d406d8ec6f54e54d7570d7febba10a6906b2.diff \
37 | && wget -O 2.patch https://github.com/zeromq/czmq/commit/b651cb479235751b22b8f9a822a2fc6bc1be01ab.diff \
38 | && cat *.patch | patch -p1 \
39 | && ./configure \
40 | && make install \
41 | )
42 |
--------------------------------------------------------------------------------
/iruby.gemspec:
--------------------------------------------------------------------------------
1 | require_relative 'lib/iruby/version'
2 |
3 | Gem::Specification.new do |s|
4 | s.name = 'iruby'
5 | s.version = IRuby::VERSION
6 | s.authors = ['Daniel Mendler', 'The SciRuby developers']
7 | s.email = ['mail@daniel-mendler.de']
8 | s.summary = 'Ruby Kernel for Jupyter'
9 | s.description = 'A Ruby kernel for Jupyter environment. Try it at try.jupyter.org.'
10 | s.homepage = 'https://github.com/SciRuby/iruby'
11 | s.license = 'MIT'
12 |
13 | s.files = `git ls-files`.split($INPUT_RECORD_SEPARATOR)
14 | s.bindir = "exe"
15 | s.executables = %w[iruby]
16 | s.test_files = s.files.grep(%r{^test/})
17 | s.require_paths = %w[lib]
18 |
19 | s.required_ruby_version = '>= 2.3.0'
20 |
21 | s.add_dependency 'data_uri', '~> 0.1'
22 | s.add_dependency 'ffi-rzmq'
23 | s.add_dependency 'irb'
24 | s.add_dependency 'logger'
25 | s.add_dependency 'mime-types', '>= 3.3.1'
26 |
27 | s.add_development_dependency 'pycall', '>= 1.2.1'
28 | s.add_development_dependency 'rake'
29 | s.add_development_dependency 'test-unit'
30 | s.add_development_dependency 'test-unit-rr'
31 |
32 | [
33 | ['arch_linux', 'zeromq'],
34 | ['debian', 'libzmq3-dev'],
35 | ['freebsd', 'libzmq4'],
36 | ['homebrew', 'zmq'],
37 | ['macports', 'zmq'],
38 | ].each do |platform, package|
39 | s.requirements << "system: libzmq: #{platform}: #{package}"
40 | end
41 |
42 | s.metadata['msys2_mingw_dependencies'] = 'zeromq'
43 | end
44 |
--------------------------------------------------------------------------------
/test/integration_test.rb:
--------------------------------------------------------------------------------
1 | require 'bundler'
2 | require 'pty'
3 | require 'expect'
4 |
5 | class IRubyTest::IntegrationTest < IRubyTest::TestBase
6 | def setup
7 | system(*iruby_command("register", "--name=iruby-test"), out: File::NULL, err: File::NULL)
8 | kernel_json = File.join(ENV["JUPYTER_DATA_DIR"], "kernels", "iruby-test", "kernel.json")
9 | assert_path_exist kernel_json
10 |
11 | $expect_verbose = false # make true if you want to dump the output of iruby console
12 |
13 | command = iruby_command("console", "--kernel=iruby-test").map {|x| %Q["#{x}"] }
14 | @in, @out, pid = PTY.spawn(command.join(" "))
15 | @waiter = Thread.start { Process.waitpid(pid) }
16 | expect 'In [', 30
17 | expect '1'
18 | expect ']:'
19 | end
20 |
21 | def teardown
22 | @in.close
23 | @out.close
24 | @waiter.join
25 | end
26 |
27 | def write(input)
28 | @out.puts input
29 | end
30 |
31 | def expect(pattern, timeout = 10)
32 | assert @in.expect(pattern, timeout), "#{pattern} expected, but timeout"
33 | end
34 |
35 | def wait_prompt
36 | expect 'In ['
37 | expect ']:'
38 | end
39 |
40 | def test_interaction
41 | omit("This test is too unstable")
42 |
43 | write '"Hello, world!"'
44 | expect '"Hello, world!"'
45 |
46 | wait_prompt
47 | write 'puts "Hello!"'
48 | expect 'Hello!'
49 |
50 | wait_prompt
51 | write '12 + 12'
52 | expect '24'
53 |
54 | wait_prompt
55 | write 'ls'
56 | expect 'self.methods'
57 | end
58 | end
59 |
--------------------------------------------------------------------------------
/lib/iruby/utils.rb:
--------------------------------------------------------------------------------
1 | module IRuby
2 | module Utils
3 | def convert(object, options)
4 | Display.convert(object, options)
5 | end
6 |
7 | # Display the object
8 | def display(obj, options = {})
9 | Kernel.instance.session.send(:publish, :display_data,
10 | data: Display.display(obj, options),
11 | metadata: {}) unless obj.nil?
12 | # The next `nil` is necessary to prevent unintentional displaying
13 | # the result of Session#send
14 | nil
15 | end
16 |
17 | # Clear the output area
18 | def clear_output(wait=false)
19 | Display.clear_output(wait)
20 | end
21 |
22 | # Format the given object into HTML table
23 | def table(s, **options)
24 | html(HTML.table(s, **options))
25 | end
26 |
27 | # Treat the given string as LaTeX text
28 | def latex(s)
29 | convert(s, mime: 'text/latex')
30 | end
31 | alias tex latex
32 |
33 | # Format the given string of TeX equation into LaTeX text
34 | def math(s)
35 | convert("$$#{s}$$", mime: 'text/latex')
36 | end
37 |
38 | # Treat the given string as HTML
39 | def html(s)
40 | convert(s, mime: 'text/html')
41 | end
42 |
43 | # Treat the given string as JavaScript code
44 | def javascript(s)
45 | convert(s, mime: 'application/javascript')
46 | end
47 |
48 | # Treat the given string as SVG text
49 | def svg(s)
50 | convert(s, mime: 'image/svg+xml')
51 | end
52 | end
53 |
54 | extend Utils
55 | end
56 |
--------------------------------------------------------------------------------
/lib/iruby/session_adapter/ffirzmq_adapter.rb:
--------------------------------------------------------------------------------
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}"
41 | else
42 | raise ArgumentError, "Invalid ZMQ socket type: #{type}"
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
56 |
--------------------------------------------------------------------------------
/lib/iruby/input/select.rb:
--------------------------------------------------------------------------------
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
--------------------------------------------------------------------------------
/lib/iruby/input/file.rb:
--------------------------------------------------------------------------------
1 | require 'data_uri'
2 |
3 | module IRuby
4 | module Input
5 | class File < Label
6 | builder :file do |key='file', **params|
7 | key = unique_key key
8 | add_field File.new(key: key, **params)
9 |
10 | # tell the builder to process files differently
11 | process key do |result,key,value|
12 | uri = URI::Data.new value['data']
13 |
14 | # get rid of Chrome's silly path
15 | name = value['name'].sub('C:\\fakepath\\','')
16 |
17 | result[key.to_sym] = {
18 | name: name,
19 | data: uri.data,
20 | content_type: uri.content_type
21 | }
22 | end
23 | end
24 |
25 | def widget_js
26 | <<-JS
27 | $('.iruby-file').change(function() {
28 | var input = $(this);
29 |
30 | $.grep($(this).prop('files'), function(file) {
31 | var reader = new FileReader();
32 |
33 | reader.addEventListener("load", function(event) {
34 | input.data('iruby-value', {
35 | name: input.val(),
36 | data: event.target.result
37 | });
38 | });
39 |
40 | reader.readAsDataURL(file);
41 | });
42 | });
43 | JS
44 | end
45 |
46 | def widget_html
47 | widget_label do
48 | input(
49 | type: 'file',
50 | :'data-iruby-key' => @key,
51 | class: 'form-control iruby-file'
52 | )
53 | end
54 | end
55 | end
56 | end
57 | end
--------------------------------------------------------------------------------
/lib/iruby/session/mixin.rb:
--------------------------------------------------------------------------------
1 | module IRuby
2 | module SessionSerialize
3 | DELIM = ''
4 |
5 | private
6 |
7 | def serialize(idents, header, metadata = nil, content)
8 | msg = [JSON.generate(header),
9 | JSON.generate(@last_recvd_msg ? @last_recvd_msg[:header] : {}),
10 | JSON.generate(metadata || {}),
11 | JSON.generate(content || {})]
12 | frames = ([*idents].compact.map(&:to_s) << DELIM << sign(msg)) + msg
13 | IRuby.logger.debug "Sent #{frames.inspect}"
14 | frames
15 | end
16 |
17 | def unserialize(msg)
18 | raise 'no message received' unless msg
19 | frames = msg.to_a.map(&:to_s)
20 | IRuby.logger.debug "Received #{frames.inspect}"
21 |
22 | i = frames.index(DELIM)
23 | idents, msg_list = frames[0..i-1], frames[i+1..-1]
24 |
25 | minlen = 5
26 | raise "malformed message, must have at least #{minlen} elements" unless msg_list.length >= minlen
27 | s, header, parent_header, metadata, content, buffers = *msg_list
28 | raise 'Invalid signature' unless s == sign(msg_list[1..-1])
29 | {
30 | idents: idents,
31 | header: JSON.parse(header),
32 | parent_header: JSON.parse(parent_header),
33 | metadata: JSON.parse(metadata),
34 | content: JSON.parse(content),
35 | buffers: buffers
36 | }
37 | end
38 |
39 | # Sign using HMAC
40 | def sign(list)
41 | return '' unless @hmac
42 | @hmac.reset
43 | list.each {|m| @hmac.update(m) }
44 | @hmac.hexdigest
45 | end
46 | end
47 | end
48 |
--------------------------------------------------------------------------------
/lib/iruby/ostream.rb:
--------------------------------------------------------------------------------
1 | module IRuby
2 | # IO-like object that publishes to 0MQ socket.
3 | class OStream
4 | attr_accessor :sync
5 |
6 | def initialize(session, name)
7 | @session, @name = session, name
8 | end
9 |
10 | def close
11 | @session = nil
12 | end
13 |
14 | def closed?
15 | @session.nil?
16 | end
17 |
18 | def flush
19 | end
20 |
21 | def isatty
22 | false
23 | end
24 | alias_method :tty?, :isatty
25 |
26 | def read(*args)
27 | raise IOError, 'not opened for reading'
28 | end
29 | alias_method :next, :read
30 | alias_method :readline, :read
31 |
32 | def write(*obj)
33 | str = build_string { |sio| sio.write(*obj) }
34 | session_send(str)
35 | end
36 | alias_method :<<, :write
37 | alias_method :print, :write
38 |
39 | def printf(format, *obj)
40 | str = build_string { |sio| sio.printf(format, *obj) }
41 | session_send(str)
42 | end
43 |
44 | def puts(*obj)
45 | str = build_string { |sio| sio.puts(*obj) }
46 | session_send(str)
47 | end
48 |
49 | def writelines(lines)
50 | lines.each { |s| write(s) }
51 | end
52 |
53 | # Called by irb
54 | def set_encoding(extern, intern)
55 | extern
56 | end
57 |
58 | private
59 |
60 | def build_string
61 | StringIO.open { |sio| yield(sio); sio.string }
62 | end
63 |
64 | def session_send(str)
65 | raise 'I/O operation on closed file' unless @session
66 |
67 | @session.send(:publish, :stream, name: @name, text: str)
68 | nil
69 | end
70 | end
71 | end
72 |
--------------------------------------------------------------------------------
/lib/iruby/input/builder.rb:
--------------------------------------------------------------------------------
1 | module IRuby
2 | module Input
3 | class Builder
4 | attr_reader :fields, :buttons
5 |
6 | def initialize &block
7 | @processors = {}
8 | @fields, @buttons = [], []
9 | instance_eval &block
10 | end
11 |
12 | def add_field field
13 | @fields << field
14 | end
15 |
16 | def add_button button
17 | # see bit.ly/1Tsv6x4
18 | @buttons.unshift button
19 | end
20 |
21 | def html &block
22 | add_field Class.new(Widget) {
23 | define_method(:widget_html) { instance_eval &block }
24 | }.new
25 | end
26 |
27 | def text string
28 | html { label string }
29 | end
30 |
31 | def password key='password', **params
32 | input key, **params.merge(type: 'password')
33 | end
34 |
35 | def process_result result
36 | unless result.nil?
37 | result.each_with_object({}) do |(k,v),h|
38 | if @processors.has_key? k
39 | @processors[k].call h, k, v
40 | else
41 | h[k.to_sym] = v
42 | end
43 | end
44 | end
45 | end
46 |
47 | private
48 |
49 | def process key, &block
50 | @processors[key.to_s] = block
51 | end
52 |
53 | def unique_key key
54 | @keys ||= []
55 |
56 | if @keys.include? key
57 | (2..Float::INFINITY).each do |i|
58 | test = "#{key}#{i}"
59 | break key = test unless @keys.include? test
60 | end
61 | end
62 |
63 | @keys << key; key
64 | end
65 | end
66 | end
67 | end
--------------------------------------------------------------------------------
/test/iruby/application/helper.rb:
--------------------------------------------------------------------------------
1 | require "helper"
2 | require "open3"
3 | require "rbconfig"
4 |
5 | require "iruby/application"
6 |
7 | module IRubyTest
8 | module ApplicationTests
9 | class ApplicationTestBase < TestBase
10 | DEFAULT_KERNEL_NAME = IRuby::Application::DEFAULT_KERNEL_NAME
11 | DEFAULT_DISPLAY_NAME = IRuby::Application::DEFAULT_DISPLAY_NAME
12 |
13 | def ensure_iruby_kernel_is_installed(kernel_name=nil)
14 | if kernel_name
15 | system(*iruby_command("register", "--name=#{kernel_name}"), out: File::NULL, err: File::NULL)
16 | else
17 | system(*iruby_command("register"), out: File::NULL, err: File::NULL)
18 | kernel_name = DEFAULT_KERNEL_NAME
19 | end
20 | kernel_json = File.join(ENV["JUPYTER_DATA_DIR"], "kernels", kernel_name, "kernel.json")
21 | assert_path_exist kernel_json
22 |
23 | # Insert -I option to add the lib directory in the $LOAD_PATH of the kernel process
24 | modified_content = JSON.load(File.read(kernel_json))
25 | kernel_index = modified_content["argv"].index("kernel")
26 | modified_content["argv"].insert(kernel_index - 1, "-I#{LIB_DIR}")
27 | File.write(kernel_json, JSON.pretty_generate(modified_content))
28 | end
29 |
30 | def add_kernel_options(*additional_argv)
31 | kernel_json = File.join(ENV["JUPYTER_DATA_DIR"], "kernels", DEFAULT_KERNEL_NAME, "kernel.json")
32 | modified_content = JSON.load(File.read(kernel_json))
33 | modified_content["argv"].concat(additional_argv)
34 | File.write(kernel_json, JSON.pretty_generate(modified_content))
35 | end
36 | end
37 | end
38 | end
39 |
--------------------------------------------------------------------------------
/test/iruby/mime_test.rb:
--------------------------------------------------------------------------------
1 | class IRubyTest::MimeTest < IRubyTest::TestBase
2 | sub_test_case("IRuby::Display") do
3 | sub_test_case(".display") do
4 | sub_test_case("with mime type") do
5 | test("text/html") do
6 | html = "Bold Text"
7 |
8 | obj = Object.new
9 | obj.define_singleton_method(:to_s) { html }
10 |
11 | res = IRuby::Display.display(obj, mime: "text/html")
12 | assert_equal({ plain: obj.inspect, html: html },
13 | { plain: res["text/plain"], html: res["text/html"] })
14 | end
15 |
16 | test("application/javascript") do
17 | data = "alert('Hello World!')"
18 | res = IRuby::Display.display(data, mime: "application/javascript")
19 | assert_equal(data,
20 | res["application/javascript"])
21 | end
22 |
23 | test("image/svg+xml") do
24 | data = ''
25 | res = IRuby::Display.display(data, mime: "image/svg+xml")
26 | assert_equal(data,
27 | res["image/svg+xml"])
28 | end
29 | end
30 | end
31 | end
32 |
33 | sub_test_case("Rendering a file") do
34 | def setup
35 | @html = "Bold Text"
36 | Dir.mktmpdir do |tmpdir|
37 | @file = File.join(tmpdir, "test.html")
38 | File.write(@file, @html)
39 | yield
40 | end
41 | end
42 |
43 | def test_display
44 | File.open(@file, "rb") do |f|
45 | res = IRuby::Display.display(f)
46 | assert_equal(@html, res["text/html"])
47 | end
48 | end
49 | end
50 | end
51 |
--------------------------------------------------------------------------------
/test/iruby/session_test.rb:
--------------------------------------------------------------------------------
1 | module IRubyTest
2 | class SessionAdapterSelectionTest < TestBase
3 | def setup
4 | # https://jupyter-client.readthedocs.io/en/stable/kernels.html#connection-files
5 | @session_config = {
6 | "control_port" => 0,
7 | "shell_port" => 0,
8 | "transport" => "tcp",
9 | "signature_scheme" => "hmac-sha256",
10 | "stdin_port" => 0,
11 | "hb_port" => 0,
12 | "ip" => "127.0.0.1",
13 | "iopub_port" => 0,
14 | "key" => "a0436f6c-1916-498b-8eb9-e81ab9368e84"
15 | }
16 | end
17 |
18 | def test_new_with_session_adapter
19 | adapter_name = ENV['IRUBY_TEST_SESSION_ADAPTER_NAME']
20 | adapter_class = case adapter_name
21 | when 'cztop'
22 | IRuby::SessionAdapter::CztopAdapter
23 | when 'ffi-rzmq'
24 | IRuby::SessionAdapter::FfirzmqAdapter
25 | else
26 | flunk "Unknown session adapter: #{adapter_name.inspect}"
27 | end
28 |
29 | session = IRuby::Session.new(@session_config, adapter_name)
30 | assert_kind_of(adapter_class, session.adapter)
31 | end
32 |
33 | def test_without_any_session_adapter
34 | assert_rr do
35 | stub(IRuby::SessionAdapter::CztopAdapter).available? { false }
36 | stub(IRuby::SessionAdapter::FfirzmqAdapter).available? { false }
37 | stub(IRuby::SessionAdapter::TestAdapter).available? { false }
38 | assert_raises IRuby::SessionAdapterNotFound do
39 | IRuby::Session.new(@session_config)
40 | end
41 | end
42 | end
43 | end
44 | end
45 |
--------------------------------------------------------------------------------
/lib/iruby/input/radio.rb:
--------------------------------------------------------------------------------
1 | module IRuby
2 | module Input
3 | class Radio < Label
4 | needs :options, :default
5 |
6 | builder :radio do |*args, **params|
7 | key = :radio
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 | add_field Radio.new(**params)
14 | end
15 |
16 | def widget_css
17 | <<-CSS
18 | .iruby-radio.form-control { display: inline-table; }
19 | .iruby-radio .radio-inline { margin: 0 15px 0 0; }
20 | CSS
21 | end
22 |
23 | def widget_js
24 | <<-JS
25 | $('.iruby-radio input').change(function(){
26 | var parent = $(this).closest('.iruby-radio');
27 | $(parent).data('iruby-value',
28 | $(parent).find(':checked').val()
29 | );
30 | });
31 | $('.iruby-radio input').trigger('change');
32 | JS
33 | end
34 |
35 | def widget_html
36 | params = {
37 | :'data-iruby-key' => @key,
38 | :'data-iruby-value' => @options.first,
39 | class: 'iruby-radio form-control'
40 | }
41 | widget_label do
42 | div **params do
43 | @options.each do |option|
44 | label class: 'radio-inline' do
45 | input(
46 | name: @key,
47 | value: option,
48 | type: 'radio',
49 | checked: @default == option
50 | )
51 | text option
52 | end
53 | end
54 | end
55 | end
56 | end
57 | end
58 | end
59 | end
--------------------------------------------------------------------------------
/lib/iruby/input/multiple.rb:
--------------------------------------------------------------------------------
1 | module IRuby
2 | module Input
3 | class Multiple < Label
4 | needs :options, :default, size: nil
5 |
6 | builder :multiple do |*args, **params|
7 | key = :multiple
8 | key, *args = args if args.first.is_a? Symbol
9 |
10 | params[:key] = unique_key(key)
11 | params[:options] = args
12 |
13 | params[:default] = case params[:default]
14 | when false, nil
15 | []
16 | when true
17 | [*params[:options]]
18 | else
19 | [*params[:default]]
20 | end
21 |
22 | add_field Multiple.new(**params)
23 | end
24 |
25 | def widget_css
26 | <<-CSS
27 | .iruby-multiple {
28 | display: table;
29 | min-width: 25%;
30 | }
31 | .form-control.iruby-multiple-container {
32 | display: table;
33 | }
34 | CSS
35 | end
36 |
37 | def widget_js
38 | <<-JS
39 | $('.iruby-multiple').change(function(){
40 | var multiple = $(this);
41 | multiple.data('iruby-value', []);
42 |
43 | multiple.find(':selected').each(function(){
44 | multiple.data('iruby-value').push($(this).val());
45 | });
46 |
47 | if (multiple.data('iruby-value').length == 0) {
48 | multiple.data('iruby-value', null);
49 | }
50 | });
51 |
52 | $('.iruby-multiple').trigger('change');
53 | JS
54 | end
55 |
56 | def widget_html
57 | widget_label do
58 | div class: 'form-control iruby-multiple-container' do
59 | params = {
60 | size: @size,
61 | multiple: true,
62 | class: 'iruby-multiple',
63 | :'data-iruby-key' => @key
64 | }
65 |
66 | select **params do
67 | @options.each do |o|
68 | option o, selected: @default.include?(o)
69 | end
70 | end
71 | end
72 | end
73 | end
74 | end
75 | end
76 | end
--------------------------------------------------------------------------------
/lib/iruby/input/checkbox.rb:
--------------------------------------------------------------------------------
1 | module IRuby
2 | module Input
3 | class Checkbox < Label
4 | needs :options, :default
5 |
6 | builder :checkbox do |*args, **params|
7 | key = :checkbox
8 | key, *args = args if args.first.is_a? Symbol
9 |
10 | params[:key] = unique_key(key)
11 | params[:options] = args
12 |
13 | params[:default] = case params[:default]
14 | when false, nil
15 | []
16 | when true
17 | [*params[:options]]
18 | else
19 | [*params[:default]]
20 | end
21 |
22 | add_field Checkbox.new(**params)
23 | end
24 |
25 | def widget_css
26 | <<-CSS
27 | .iruby-checkbox.form-control { display: inline-table; }
28 | .iruby-checkbox .checkbox-inline { margin: 0 15px 0 0; }
29 | CSS
30 | end
31 |
32 | def widget_js
33 | <<-JS
34 | $('.iruby-checkbox input').change(function(){
35 | var parent = $(this).closest('.iruby-checkbox');
36 | $(parent).data('iruby-value', []);
37 |
38 | $(parent).find(':checked').each(function(){
39 | $(parent).data('iruby-value').push($(this).val());
40 | });
41 |
42 | if ($(parent).data('iruby-value').length == 0) {
43 | $(parent).data('iruby-value', null);
44 | }
45 | });
46 |
47 | $('.iruby-checkbox input').trigger('change');
48 | JS
49 | end
50 |
51 | def widget_html
52 | params = {
53 | :'data-iruby-key' => @key,
54 | class: 'iruby-checkbox form-control'
55 | }
56 | widget_label do
57 | div **params do
58 | @options.each do |option|
59 | label class: 'checkbox-inline' do
60 | input(
61 | name: @key,
62 | value: option,
63 | type: 'checkbox',
64 | checked: @default.include?(option)
65 | )
66 | text option
67 | end
68 | end
69 | end
70 | end
71 | end
72 | end
73 | end
74 | end
--------------------------------------------------------------------------------
/lib/iruby/session/cztop.rb:
--------------------------------------------------------------------------------
1 | require 'cztop'
2 |
3 | module IRuby
4 | class Session
5 | include SessionSerialize
6 |
7 | def initialize(config)
8 | connection = "#{config['transport']}://#{config['ip']}:%d"
9 |
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'])
13 |
14 | Thread.new do
15 | begin
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
21 | rescue Exception => e
22 | IRuby.logger.fatal "Kernel heartbeat died: #{e.message}\n#{e.backtrace.join("\n")}"
23 | end
24 | end
25 |
26 | @sockets = {
27 | publish: pub_socket,
28 | reply: reply_socket,
29 | stdin: stdin_socket,
30 | }
31 |
32 | @session = SecureRandom.uuid
33 | unless config['key'].to_s.empty? || config['signature_scheme'].to_s.empty?
34 | raise 'Unknown signature scheme' unless config['signature_scheme'] =~ /\Ahmac-(.*)\Z/
35 | @hmac = OpenSSL::HMAC.new(config['key'], OpenSSL::Digest.new($1))
36 | end
37 | end
38 |
39 | def description
40 | 'old-style session using cztop'
41 | end
42 |
43 | # Build and send a message
44 | def send(socket, type, content)
45 | idents =
46 | if socket == :reply && @last_recvd_msg
47 | @last_recvd_msg[:idents]
48 | else
49 | type == :stream ? "stream.#{content[:name]}" : type
50 | end
51 | header = {
52 | msg_type: type,
53 | msg_id: SecureRandom.uuid,
54 | username: 'kernel',
55 | session: @session,
56 | version: '5.0'
57 | }
58 | @sockets[socket] << serialize(idents, header, content)
59 | end
60 |
61 | # Receive a message and decode it
62 | def recv(socket)
63 | @last_recvd_msg = unserialize(@sockets[socket].receive)
64 | end
65 |
66 | def recv_input
67 | unserialize(@sockets[:stdin].receive)[:content]["value"]
68 | end
69 | end
70 | end
71 |
--------------------------------------------------------------------------------
/tasks/ci.rake:
--------------------------------------------------------------------------------
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
66 |
--------------------------------------------------------------------------------
/lib/iruby/input/form.rb:
--------------------------------------------------------------------------------
1 | require 'securerandom'
2 |
3 | module IRuby
4 | module Input
5 | class InputForm < Widget
6 | needs :fields, buttons: []
7 |
8 | def widget_js
9 | javascript = <<-JS
10 | var remove = function () {
11 | Jupyter.notebook.kernel.send_input_reply(
12 | JSON.stringify({
13 | '#{@id = SecureRandom.uuid}': null
14 | })
15 | );
16 | };
17 |
18 | $("#iruby-form").on("remove", remove);
19 |
20 | $('#iruby-form').submit(function() {
21 | var result = {};
22 | $(this).off('remove', remove);
23 |
24 | $('[data-iruby-key]').each(function() {
25 | if ($(this).data('iruby-key')) {
26 | var value = $(this).data('iruby-value');
27 | if (value) {
28 | result[$(this).data('iruby-key')] = value;
29 | }
30 | }
31 | });
32 |
33 | Jupyter.notebook.kernel.send_input_reply(
34 | JSON.stringify({'#{@id}': result})
35 | );
36 |
37 | $(this).remove();
38 | return false;
39 | });
40 |
41 | $('#iruby-form').keydown(function(event) {
42 | if (event.keyCode == 13 && !event.shiftKey) {
43 | $('#iruby-form').submit();
44 | } else if (event.keyCode == 27) {
45 | $('#iruby-form').remove();
46 | }
47 | });
48 | JS
49 |
50 | widget_join :widget_js, javascript, *@fields, *@buttons
51 | end
52 |
53 | def widget_css
54 | spacing = '#iruby-form > * { margin-bottom: 5px; }'
55 | widget_join :widget_css, spacing, *@fields, *@buttons
56 | end
57 |
58 | def widget_html
59 | form id: 'iruby-form', class: 'col-md-12' do
60 | @fields.each {|field| widget field}
61 | end
62 | @buttons.each {|button| widget button}
63 | end
64 |
65 | def submit
66 | result = JSON.parse(Kernel.instance.session.recv_input)
67 |
68 | unless result.has_key? @id
69 | submit
70 | else
71 | Display.clear_output
72 | result[@id]
73 | end
74 | end
75 | end
76 | end
77 | end
78 |
--------------------------------------------------------------------------------
/lib/iruby/session_adapter.rb:
--------------------------------------------------------------------------------
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/test_adapter'
44 |
45 | def self.select_adapter_class(name=nil)
46 | classes = {
47 | 'ffi-rzmq' => SessionAdapter::FfirzmqAdapter,
48 | 'cztop' => SessionAdapter::CztopAdapter,
49 | 'test' => SessionAdapter::TestAdapter,
50 | }
51 | if (name ||= ENV.fetch('IRUBY_SESSION_ADAPTER', nil))
52 | cls = classes[name]
53 | unless cls.available?
54 | if ENV['IRUBY_SESSION_ADAPTER']
55 | raise SessionAdapterNotFound,
56 | "Session adapter `#{name}` from IRUBY_SESSION_ADAPTER is unavailable"
57 | else
58 | raise SessionAdapterNotFound,
59 | "Session adapter `#{name}` is unavailable"
60 | end
61 | end
62 | if name == 'cztop'
63 | warn "WARNING: cztop is deprecated and will be removed; Use ffi-rzmq instead."
64 | end
65 | return cls
66 | end
67 | classes.each_value do |cls|
68 | return cls if cls.available?
69 | end
70 | raise SessionAdapterNotFound, "No session adapter is available"
71 | end
72 | end
73 | end
74 |
--------------------------------------------------------------------------------
/test/iruby/session_adapter_test.rb:
--------------------------------------------------------------------------------
1 | module IRubyTest
2 | class SessionAdapterTest < TestBase
3 | def test_available_p_return_false_when_load_error
4 | subclass = Class.new(IRuby::SessionAdapter::BaseAdapter)
5 | class << subclass
6 | def load_requirements
7 | raise LoadError
8 | end
9 | end
10 | refute subclass.available?
11 | end
12 |
13 | def test_select_adapter_class_with_cztop
14 | assert_rr do
15 | stub(IRuby::SessionAdapter::CztopAdapter).available? { true }
16 | stub(IRuby::SessionAdapter::FfirzmqAdapter).available? { false }
17 |
18 | cls = IRuby::SessionAdapter.select_adapter_class
19 | assert_equal IRuby::SessionAdapter::CztopAdapter, cls
20 | end
21 | end
22 |
23 | def test_select_adapter_class_with_ffirzmq
24 | assert_rr do
25 | stub(IRuby::SessionAdapter::FfirzmqAdapter).available? { true }
26 | stub(IRuby::SessionAdapter::CztopAdapter).available? { false }
27 |
28 | cls = IRuby::SessionAdapter.select_adapter_class
29 | assert_equal IRuby::SessionAdapter::FfirzmqAdapter, cls
30 | end
31 | end
32 |
33 | def test_select_adapter_class_with_env
34 | with_env('IRUBY_SESSION_ADAPTER' => 'cztop') do
35 | assert_rr do
36 | stub(IRuby::SessionAdapter::CztopAdapter).available? { true }
37 | assert_equal IRuby::SessionAdapter::CztopAdapter, IRuby::SessionAdapter.select_adapter_class
38 | end
39 |
40 | assert_rr do
41 | stub(IRuby::SessionAdapter::CztopAdapter).available? { false }
42 | assert_raises IRuby::SessionAdapterNotFound do
43 | IRuby::SessionAdapter.select_adapter_class
44 | end
45 | end
46 | end
47 |
48 | with_env('IRUBY_SESSION_ADAPTER' => 'ffi-rzmq') do
49 | assert_rr do
50 | stub(IRuby::SessionAdapter::FfirzmqAdapter).available? { true }
51 | assert_equal IRuby::SessionAdapter::FfirzmqAdapter, IRuby::SessionAdapter.select_adapter_class
52 | end
53 |
54 | assert_rr do
55 | stub(IRuby::SessionAdapter::FfirzmqAdapter).available? { false }
56 | assert_raises IRuby::SessionAdapterNotFound do
57 | IRuby::SessionAdapter.select_adapter_class
58 | end
59 | end
60 | end
61 | end
62 | end
63 | end
64 |
--------------------------------------------------------------------------------
/test/iruby/event_manager_test.rb:
--------------------------------------------------------------------------------
1 | module IRubyTest
2 | class EventManagerTest < TestBase
3 | def setup
4 | @man = IRuby::EventManager.new([:foo, :bar])
5 | end
6 |
7 | def test_available_events
8 | assert_equal([:foo, :bar],
9 | @man.available_events)
10 | end
11 |
12 | sub_test_case("#register") do
13 | sub_test_case("known event name") do
14 | def test_register
15 | fn = ->() {}
16 | assert_equal(fn,
17 | @man.register(:foo, &fn))
18 | end
19 | end
20 |
21 | sub_test_case("unknown event name") do
22 | def test_register
23 | assert_raise_message("Unknown event name: baz") do
24 | @man.register(:baz) {}
25 | end
26 | end
27 | end
28 | end
29 |
30 | sub_test_case("#unregister") do
31 | sub_test_case("no event is registered") do
32 | def test_unregister
33 | fn = ->() {}
34 | assert_raise_message("Given callable object #{fn} is not registered as a foo callback") do
35 | @man.unregister(:foo, fn)
36 | end
37 | end
38 | end
39 |
40 | sub_test_case("the registered callable is given") do
41 | def test_unregister
42 | results = { values: [] }
43 | fn = ->(a) { values << a }
44 |
45 | @man.register(:foo, &fn)
46 |
47 | results[:retval] = @man.unregister(:foo, fn)
48 |
49 | @man.trigger(:foo, 42)
50 |
51 | assert_equal({
52 | values: [],
53 | retval: fn
54 | },
55 | results)
56 | end
57 | end
58 | end
59 |
60 | sub_test_case("#trigger") do
61 | sub_test_case("no event is registered") do
62 | def test_trigger
63 | assert_nothing_raised do
64 | @man.trigger(:foo)
65 | end
66 | end
67 | end
68 |
69 | sub_test_case("some events are registered") do
70 | def test_trigger
71 | values = []
72 | @man.register(:foo) {|a| values << a }
73 | @man.register(:foo) {|a| values << 10*a }
74 | @man.register(:foo) {|a| values << 100+a }
75 |
76 | @man.trigger(:foo, 5)
77 |
78 | assert_equal([5, 50, 105],
79 | values)
80 | end
81 | end
82 |
83 | sub_test_case("unknown event name") do
84 | def test_trigger
85 | assert_raise_message("Unknown event name: baz") do
86 | @man.trigger(:baz, 100)
87 | end
88 | end
89 | end
90 | end
91 | end
92 | end
93 |
--------------------------------------------------------------------------------
/lib/iruby/jupyter.rb:
--------------------------------------------------------------------------------
1 | module IRuby
2 | module Jupyter
3 | class << self
4 | # User's default kernelspec directory is described here:
5 | # https://docs.jupyter.org/en/latest/use/jupyter-directories.html
6 | def default_data_dir
7 | data_dir = ENV["JUPYTER_DATA_DIR"]
8 | return data_dir if data_dir
9 |
10 | case
11 | when windows?
12 | appdata = windows_user_appdata
13 | if !appdata.empty?
14 | File.join(appdata, 'jupyter')
15 | else
16 | jupyter_config_dir = ENV.fetch('JUPYTER_CONFIG_DIR', File.expand_path('~/.jupyter'))
17 | File.join(jupyter_config_dir, 'data')
18 | end
19 | when apple?
20 | File.expand_path('~/Library/Jupyter')
21 | else
22 | xdg_data_home = ENV.fetch('XDG_DATA_HOME', '')
23 | data_home = xdg_data_home[0] ? xdg_data_home : File.expand_path('~/.local/share')
24 | File.join(data_home, 'jupyter')
25 | end
26 | end
27 |
28 | def kernelspec_dir(data_dir=nil)
29 | data_dir ||= default_data_dir
30 | File.join(data_dir, 'kernels')
31 | end
32 |
33 | private
34 |
35 | # returns %APPDATA%
36 | def windows_user_appdata
37 | require 'fiddle/import'
38 | check_windows
39 | path = Fiddle::Pointer.malloc(2 * 300) # uint16_t[300]
40 | csidl_appdata = 0x001a
41 | case call_SHGetFolderPathW(Fiddle::NULL, csidl_appdata, Fiddle::NULL, 0, path)
42 | when 0
43 | len = (1 ... (path.size/2)).find {|i| path[2*i, 2] == "\0\0" }
44 | path = path.to_str(2*len).encode(Encoding::UTF_8, Encoding::UTF_16LE)
45 | else
46 | ENV.fetch('APPDATA', '')
47 | end
48 | end
49 |
50 | def call_SHGetFolderPathW(hwnd, csidl, hToken, dwFlags, pszPath)
51 | require 'fiddle/import'
52 | shell32 = Fiddle::Handle.new('shell32')
53 | func = Fiddle::Function.new(
54 | shell32['SHGetFolderPathW'],
55 | [
56 | Fiddle::TYPE_VOIDP,
57 | Fiddle::TYPE_INT,
58 | Fiddle::TYPE_VOIDP,
59 | Fiddle::TYPE_INT,
60 | Fiddle::TYPE_VOIDP
61 | ],
62 | Fiddle::TYPE_INT,
63 | Fiddle::Importer.const_get(:CALL_TYPE_TO_ABI)[:stdcall])
64 | func.(hwnd, csidl, hToken, dwFlags, pszPath)
65 | end
66 |
67 | def check_windows
68 | raise 'the current platform is not Windows' unless windows?
69 | end
70 |
71 | def windows?
72 | /mingw|mswin/ =~ RUBY_PLATFORM
73 | end
74 |
75 | def apple?
76 | /darwin/ =~ RUBY_PLATFORM
77 | end
78 | end
79 | end
80 | end
81 |
--------------------------------------------------------------------------------
/test/iruby/application/unregister_test.rb:
--------------------------------------------------------------------------------
1 | require_relative "helper"
2 |
3 | module IRubyTest::ApplicationTests
4 | class UnregisterTest < ApplicationTestBase
5 | def setup
6 | Dir.mktmpdir do |tmpdir|
7 | @kernel_json = File.join(tmpdir, "kernels", DEFAULT_KERNEL_NAME, "kernel.json")
8 | with_env(
9 | "JUPYTER_DATA_DIR" => tmpdir,
10 | "IPYTHONDIR" => nil
11 | ) do
12 | yield
13 | end
14 | end
15 | end
16 |
17 | sub_test_case("when there is no IRuby kernel in JUPYTER_DATA_DIR") do
18 | test("the command succeeds") do
19 | assert system(*iruby_command("unregister", "-f", DEFAULT_KERNEL_NAME),
20 | out: File::NULL, err: File::NULL)
21 | end
22 | end
23 |
24 | sub_test_case("when the existing IRuby kernel in JUPYTER_DATA_DIR") do
25 | def setup
26 | super do
27 | ensure_iruby_kernel_is_installed
28 | yield
29 | end
30 | end
31 |
32 | test("uninstall the existing kernel") do
33 | assert system(*iruby_command("unregister", "-f", DEFAULT_KERNEL_NAME),
34 | out: File::NULL, err: File::NULL)
35 | assert_path_not_exist @kernel_json
36 | end
37 | end
38 |
39 | sub_test_case("when the existing IRuby kernel in IPython's kernels directory") do
40 | def setup
41 | super do
42 | Dir.mktmpdir do |tmpdir|
43 | ipython_dir = File.join(tmpdir, ".ipython")
44 |
45 | # prepare the existing IRuby kernel with the default name
46 | with_env("JUPYTER_DATA_DIR" => ipython_dir) do
47 | ensure_iruby_kernel_is_installed
48 | end
49 |
50 | fake_bin_dir = File.join(tmpdir, "bin")
51 | fake_jupyter = File.join(fake_bin_dir, "jupyter")
52 | FileUtils.mkdir_p(fake_bin_dir)
53 | IO.write(fake_jupyter, <<-FAKE_JUPYTER)
54 | #!/usr/bin/env ruby
55 | puts "Fake Jupyter"
56 | FAKE_JUPYTER
57 | File.chmod(0o755, fake_jupyter)
58 |
59 | new_path = [fake_bin_dir, ENV["PATH"]].join(File::PATH_SEPARATOR)
60 | with_env(
61 | "HOME" => tmpdir,
62 | "PATH" => new_path,
63 | "IPYTHONDIR" => nil
64 | ) do
65 | yield
66 | end
67 | end
68 | end
69 | end
70 |
71 | test("the kernel in IPython's kernels directory is not removed") do
72 | assert system(*iruby_command("unregister", "-f"), out: File::NULL, err: File::NULL)
73 | assert_path_exist File.join(File.expand_path("~/.ipython"), "kernels", DEFAULT_KERNEL_NAME, "kernel.json")
74 | end
75 | end
76 | end
77 | end
78 |
--------------------------------------------------------------------------------
/lib/iruby/session/ffi_rzmq.rb:
--------------------------------------------------------------------------------
1 | require 'ffi-rzmq'
2 |
3 | module IRuby
4 | class Session
5 | include SessionSerialize
6 |
7 | def initialize(config)
8 | c = ZMQ::Context.new
9 |
10 | connection = "#{config['transport']}://#{config['ip']}:%d"
11 | reply_socket = c.socket(ZMQ::XREP)
12 | reply_socket.bind(connection % config['shell_port'])
13 |
14 | pub_socket = c.socket(ZMQ::PUB)
15 | pub_socket.bind(connection % config['iopub_port'])
16 |
17 | stdin_socket = c.socket(ZMQ::XREP)
18 | stdin_socket.bind(connection % config['stdin_port'])
19 |
20 | Thread.new do
21 | begin
22 | hb_socket = c.socket(ZMQ::REP)
23 | hb_socket.bind(connection % config['hb_port'])
24 | ZMQ::Device.new(hb_socket, hb_socket)
25 | rescue Exception => e
26 | IRuby.logger.fatal "Kernel heartbeat died: #{e.message}\n#{e.backtrace.join("\n")}"
27 | end
28 | end
29 |
30 | @sockets = {
31 | publish: pub_socket, reply: reply_socket, stdin: stdin_socket
32 | }
33 |
34 | @session = SecureRandom.uuid
35 | unless config['key'].to_s.empty? || config['signature_scheme'].to_s.empty?
36 | raise 'Unknown signature scheme' unless config['signature_scheme'] =~ /\Ahmac-(.*)\Z/
37 | @hmac = OpenSSL::HMAC.new(config['key'], OpenSSL::Digest.new($1))
38 | end
39 | end
40 |
41 | # Build and send a message
42 | def send(socket, type, content)
43 | idents =
44 | if socket == :reply && @last_recvd_msg
45 | @last_recvd_msg[:idents]
46 | else
47 | type == :stream ? "stream.#{content[:name]}" : type
48 | end
49 | header = {
50 | msg_type: type,
51 | msg_id: SecureRandom.uuid,
52 | username: 'kernel',
53 | session: @session,
54 | version: '5.0'
55 | }
56 | socket = @sockets[socket]
57 | list = serialize(idents, header, content)
58 | list.each_with_index do |part, i|
59 | socket.send_string(part, i == list.size - 1 ? 0 : ZMQ::SNDMORE)
60 | end
61 | end
62 |
63 | # Receive a message and decode it
64 | def recv(socket)
65 | socket = @sockets[socket]
66 | msg = []
67 | while msg.empty? || socket.more_parts?
68 | begin
69 | frame = ''
70 | rc = socket.recv_string(frame)
71 | ZMQ::Util.error_check('zmq_msg_send', rc)
72 | msg << frame
73 | rescue
74 | end
75 | end
76 |
77 | @last_recvd_msg = unserialize(msg)
78 | end
79 |
80 | def recv_input
81 | last_recvd_msg = @last_recvd_msg
82 | input = recv(:stdin)[:content]["value"]
83 | @last_recvd_msg = last_recvd_msg
84 | input
85 | end
86 | end
87 | end
88 |
--------------------------------------------------------------------------------
/lib/iruby/kernel_app.rb:
--------------------------------------------------------------------------------
1 | module IRuby
2 | class KernelApplication
3 | def initialize(argv)
4 | parse_command_line(argv)
5 | end
6 |
7 | def run
8 | if @test_mode
9 | dump_connection_file
10 | return
11 | end
12 |
13 | run_kernel
14 | end
15 |
16 | DEFAULT_CONNECTION_FILE = "kernel-#{Process.pid}.json".freeze
17 |
18 | private def parse_command_line(argv)
19 | opts = OptionParser.new
20 | opts.banner = "Usage: #{$PROGRAM_NAME} [options] [subcommand] [options]"
21 |
22 | @connection_file = nil
23 | opts.on(
24 | "-f CONNECTION_FILE", String,
25 | "JSON file in which to store connection info (default: kernel-.json)"
26 | ) {|v| @connection_file = v }
27 |
28 | @test_mode = false
29 | opts.on(
30 | "--test",
31 | "Run as test mode; dump the connection file and exit."
32 | ) { @test_mode = true }
33 |
34 | @log_file = nil
35 | opts.on(
36 | "--log=FILE", String,
37 | "Specify the log file."
38 | ) {|v| @log_file = v }
39 |
40 | @log_level = Logger::INFO
41 | opts.on(
42 | "--debug",
43 | "Set log-level debug"
44 | ) { @log_level = Logger::DEBUG }
45 |
46 | opts.order!(argv)
47 |
48 | if @connection_file.nil?
49 | # Without -f option, the connection file is at the beginning of the rest arguments
50 | if argv.length <= 3
51 | @connection_file, @boot_file, @work_dir = argv
52 | else
53 | raise ArgumentError, "Too many commandline arguments"
54 | end
55 | else
56 | if argv.length <= 2
57 | @boot_file, @work_dir = argv
58 | else
59 | raise ArgumentError, "Too many commandline arguments"
60 | end
61 | end
62 |
63 | @connection_file ||= DEFAULT_CONNECTION_FILE
64 | end
65 |
66 | private def dump_connection_file
67 | puts File.read(@connection_file)
68 | end
69 |
70 | private def run_kernel
71 | IRuby.logger = MultiLogger.new(*Logger.new(STDOUT))
72 | STDOUT.sync = true # FIXME: This can make the integration test.
73 |
74 | IRuby.logger.loggers << Logger.new(@log_file) unless @log_file.nil?
75 | IRuby.logger.level = @log_level
76 |
77 | if @work_dir
78 | IRuby.logger.debug("iruby kernel") { "Change the working directory: #{@work_dir}" }
79 | Dir.chdir(@work_dir)
80 | end
81 |
82 | if @boot_file
83 | IRuby.logger.debug("iruby kernel") { "Load the boot file: #{@boot_file}" }
84 | require @boot_file
85 | end
86 |
87 | check_bundler {|e| IRuby.logger.warn "Could not load bundler: #{e.message}" }
88 |
89 | require "iruby"
90 | Kernel.new(@connection_file).run
91 | rescue Exception => e
92 | IRuby.logger.fatal "Kernel died: #{e.message}\n#{e.backtrace.join("\n")}"
93 | exit 1
94 | end
95 |
96 | private def check_bundler
97 | require "bundler"
98 | unless Bundler.definition.specs.any? {|s| s.name == "iruby" }
99 | raise %{IRuby is missing from Gemfile. This might not work. Add `gem "iruby"` in your Gemfile to fix it.}
100 | end
101 | Bundler.setup
102 | rescue LoadError
103 | # do nothing
104 | rescue Exception => e
105 | yield e
106 | end
107 | end
108 | end
109 |
--------------------------------------------------------------------------------
/test/iruby/application/kernel_test.rb:
--------------------------------------------------------------------------------
1 | require_relative "helper"
2 |
3 | module IRubyTest::ApplicationTests
4 | class KernelTest < ApplicationTestBase
5 | def setup
6 | Dir.mktmpdir do |tmpdir|
7 | @fake_bin_dir = File.join(tmpdir, "bin")
8 | FileUtils.mkdir_p(@fake_bin_dir)
9 |
10 | @fake_data_dir = File.join(tmpdir, "data")
11 | FileUtils.mkdir_p(@fake_data_dir)
12 |
13 | new_path = [@fake_bin_dir, ENV["PATH"]].join(File::PATH_SEPARATOR)
14 | with_env("PATH" => new_path,
15 | "JUPYTER_DATA_DIR" => @fake_data_dir) do
16 | ensure_iruby_kernel_is_installed
17 | yield
18 | end
19 | end
20 | end
21 |
22 | test("--test option dumps the given connection file") do
23 | connection_info = {
24 | "control_port" => 123456,
25 | "shell_port" => 123457,
26 | "transport" => "tcp",
27 | "signature_scheme" => "hmac-sha256",
28 | "stdin_port" => 123458,
29 | "hb_port" => 123459,
30 | "ip" => "127.0.0.1",
31 | "iopub_port" => 123460,
32 | "key" => "a0436f6c-1916-498b-8eb9-e81ab9368e84"
33 | }
34 | Dir.mktmpdir do |tmpdir|
35 | connection_file = File.join(tmpdir, "connection.json")
36 | File.write(connection_file, JSON.dump(connection_info))
37 | out, status = Open3.capture2e(*iruby_command("kernel", "-f", connection_file, "--test"))
38 | assert status.success?
39 | assert_equal connection_info, JSON.load(out)
40 | end
41 | end
42 |
43 | test("the default log level is INFO") do
44 | Dir.mktmpdir do |tmpdir|
45 | boot_file = File.join(tmpdir, "boot.rb")
46 | File.write(boot_file, <<~BOOT_SCRIPT)
47 | puts "!!! INFO: \#{Logger::INFO}"
48 | puts "!!! LOG LEVEL: \#{IRuby.logger.level}"
49 | puts "!!! LOG LEVEL IS INFO: \#{IRuby.logger.level == Logger::INFO}"
50 | BOOT_SCRIPT
51 |
52 | add_kernel_options(boot_file)
53 |
54 | out, status = Open3.capture2e(*iruby_command("console"), in: :close)
55 | assert status.success?
56 | assert_match(/^!!! LOG LEVEL IS INFO: true$/, out)
57 | end
58 | end
59 |
60 | test("--debug option makes the log level DEBUG") do
61 | Dir.mktmpdir do |tmpdir|
62 | boot_file = File.join(tmpdir, "boot.rb")
63 | File.write(boot_file, <<~BOOT_SCRIPT)
64 | puts "!!! LOG LEVEL IS DEBUG: \#{IRuby.logger.level == Logger::DEBUG}"
65 | BOOT_SCRIPT
66 |
67 | add_kernel_options("--debug", boot_file)
68 |
69 | out, status = Open3.capture2e(*iruby_command("console"), in: :close)
70 | assert status.success?
71 | assert_match(/^!!! LOG LEVEL IS DEBUG: true$/, out)
72 | end
73 | end
74 |
75 | test("--log option adds a log destination file") do
76 | Dir.mktmpdir do |tmpdir|
77 | boot_file = File.join(tmpdir, "boot.rb")
78 | File.write(boot_file, <<~BOOT_SCRIPT)
79 | IRuby.logger.info("bootfile") { "!!! LOG MESSAGE FROM BOOT FILE !!!" }
80 | BOOT_SCRIPT
81 |
82 | log_file = File.join(tmpdir, "log.txt")
83 |
84 | add_kernel_options("--log=#{log_file}", boot_file)
85 |
86 | _out, status = Open3.capture2e(*iruby_command("console"), in: :close)
87 | assert status.success?
88 | assert_path_exist log_file
89 | assert_match(/\bINFO -- bootfile: !!! LOG MESSAGE FROM BOOT FILE !!!$/, File.read(log_file))
90 | end
91 | end
92 | end
93 | end
94 |
--------------------------------------------------------------------------------
/test/iruby/application/console_test.rb:
--------------------------------------------------------------------------------
1 | require_relative "helper"
2 |
3 | module IRubyTest::ApplicationTests
4 | class ConsoleTest < ApplicationTestBase
5 | def setup
6 | Dir.mktmpdir do |tmpdir|
7 | @fake_bin_dir = File.join(tmpdir, "bin")
8 | FileUtils.mkdir_p(@fake_bin_dir)
9 |
10 | @fake_data_dir = File.join(tmpdir, "data")
11 | FileUtils.mkdir_p(@fake_data_dir)
12 |
13 | new_path = [@fake_bin_dir, ENV["PATH"]].join(File::PATH_SEPARATOR)
14 | with_env("PATH" => new_path,
15 | "JUPYTER_DATA_DIR" => @fake_data_dir) do
16 | yield
17 | end
18 | end
19 | end
20 |
21 | sub_test_case("there is the default IRuby kernel in JUPYTER_DATA_DIR") do
22 | def setup
23 | super do
24 | ensure_iruby_kernel_is_installed
25 | yield
26 | end
27 | end
28 |
29 | test("run `jupyter console` with the default IRuby kernel") do
30 | out, status = Open3.capture2e(*iruby_command("console"), in: :close)
31 | assert status.success?
32 | assert_match(/^Jupyter console [\d\.]+$/, out)
33 | assert_match(/^#{Regexp.escape("IRuby #{IRuby::VERSION}")}\b/, out)
34 | end
35 | end
36 |
37 | # NOTE: this case checks the priority of the default IRuby kernel when both kernels are available
38 | sub_test_case("there are both the default IRuby kernel and IRuby kernel named `ruby` in JUPYTER_DATA_DIR") do
39 | def setup
40 | super do
41 | ensure_iruby_kernel_is_installed
42 | ensure_iruby_kernel_is_installed("ruby")
43 | yield
44 | end
45 | end
46 |
47 | test("run `jupyter console` with the default IRuby kernel") do
48 | out, status = Open3.capture2e(*iruby_command("console"), in: :close)
49 | assert status.success?
50 | assert_match(/^Jupyter console [\d\.]+$/, out)
51 | assert_match(/^#{Regexp.escape("IRuby #{IRuby::VERSION}")}\b/, out)
52 | end
53 | end
54 |
55 | # NOTE: this case checks the availability of the old kernel name
56 | sub_test_case("there is the IRuby kernel, which is named `ruby`, in JUPYTER_DATA_DIR") do
57 | def setup
58 | super do
59 | ensure_iruby_kernel_is_installed("ruby")
60 | yield
61 | end
62 | end
63 |
64 | test("run `jupyter console` with the IRuby kernel `ruby`") do
65 | out, status = Open3.capture2e(*iruby_command("console"), in: :close)
66 | assert status.success?
67 | assert_match(/^Jupyter console [\d\.]+$/, out)
68 | assert_match(/^#{Regexp.escape("IRuby #{IRuby::VERSION}")}\b/, out)
69 | end
70 | end
71 |
72 | sub_test_case("with --kernel option") do
73 | test("run `jupyter console` command with the given kernel name") do
74 | kernel_name = "other-kernel-#{Process.pid}"
75 | out, status = Open3.capture2e(*iruby_command("console", "--kernel=#{kernel_name}"))
76 | refute status.success?
77 | assert_match(/\bNo such kernel named #{Regexp.escape(kernel_name)}\b/, out)
78 | end
79 | end
80 |
81 | sub_test_case("no subcommand") do
82 | def setup
83 | super do
84 | ensure_iruby_kernel_is_installed
85 | yield
86 | end
87 | end
88 |
89 | test("Run jupyter console command with the default IRuby kernel") do
90 | out, status = Open3.capture2e(*iruby_command, in: :close)
91 | assert status.success?
92 | assert_match(/^Jupyter console [\d\.]+$/, out)
93 | assert_match(/^#{Regexp.escape("IRuby #{IRuby::VERSION}")}\b/, out)
94 | end
95 | end
96 | end
97 | end
98 |
--------------------------------------------------------------------------------
/lib/iruby/session.rb:
--------------------------------------------------------------------------------
1 | require 'iruby/session_adapter'
2 | require 'iruby/session/mixin'
3 |
4 | require 'securerandom'
5 | require 'time'
6 |
7 | module IRuby
8 | class Session
9 | include SessionSerialize
10 |
11 | def initialize(config, adapter_name=nil)
12 | @config = config
13 | @adapter = create_session_adapter(config, adapter_name)
14 | @last_recvd_msg = nil
15 |
16 | setup
17 | setup_sockets
18 | setup_heartbeat
19 | setup_security
20 | end
21 |
22 | attr_reader :adapter, :config
23 |
24 | def description
25 | "#{@adapter.name} session adapter"
26 | end
27 |
28 | def setup
29 | end
30 |
31 | def setup_sockets
32 | protocol, host = config.values_at('transport', 'ip')
33 | shell_port = config['shell_port']
34 | iopub_port = config['iopub_port']
35 | stdin_port = config['stdin_port']
36 |
37 | @shell_socket, @shell_port = @adapter.make_router_socket(protocol, host, shell_port)
38 | @iopub_socket, @iopub_port = @adapter.make_pub_socket(protocol, host, iopub_port)
39 | @stdin_socket, @stdin_port = @adapter.make_router_socket(protocol, host, stdin_port)
40 |
41 | @sockets = {
42 | publish: @iopub_socket,
43 | reply: @shell_socket,
44 | stdin: @stdin_socket
45 | }
46 | end
47 |
48 | def setup_heartbeat
49 | protocol, host = config.values_at('transport', 'ip')
50 | hb_port = config['hb_port']
51 | @hb_socket, @hb_port = @adapter.make_rep_socket(protocol, host, hb_port)
52 | @heartbeat_thread = Thread.start do
53 | begin
54 | # NOTE: this loop is copied from CZTop's old session code
55 | @adapter.heartbeat_loop(@hb_socket)
56 | rescue Exception => e
57 | IRuby.logger.fatal "Kernel heartbeat died: #{e.message}\n#{e.backtrace.join("\n")}"
58 | end
59 | end
60 | end
61 |
62 | def setup_security
63 | @session_id = SecureRandom.uuid
64 | unless config['key'].empty? || config['signature_scheme'].empty?
65 | unless config['signature_scheme'] =~ /\Ahmac-/
66 | raise "Unknown signature_scheme: #{config['signature_scheme']}"
67 | end
68 | digest_algorithm = config['signature_scheme'][/\Ahmac-(.*)\Z/, 1]
69 | @hmac = OpenSSL::HMAC.new(config['key'], OpenSSL::Digest.new(digest_algorithm))
70 | end
71 | end
72 |
73 | def send(socket_type, message_type, metadata = nil, content)
74 | sock = check_socket_type(socket_type)
75 | idents = if socket_type == :reply && @last_recvd_msg
76 | @last_recvd_msg[:idents]
77 | else
78 | message_type == :stream ? "stream.#{content[:name]}" : message_type
79 | end
80 | header = {
81 | msg_type: message_type,
82 | msg_id: SecureRandom.uuid,
83 | date: Time.now.utc.iso8601,
84 | username: 'kernel',
85 | session: @session_id,
86 | version: '5.0'
87 | }
88 | @adapter.send(sock, serialize(idents, header, metadata, content))
89 | end
90 |
91 | def recv(socket_type)
92 | sock = check_socket_type(socket_type)
93 | data = @adapter.recv(sock)
94 | @last_recvd_msg = unserialize(data)
95 | end
96 |
97 | def recv_input
98 | sock = check_socket_type(:stdin)
99 | data = @adapter.recv(sock)
100 | unserialize(data)[:content]["value"]
101 | end
102 |
103 | private
104 |
105 | def check_socket_type(socket_type)
106 | case socket_type
107 | when :publish, :reply, :stdin
108 | @sockets[socket_type]
109 | else
110 | raise ArgumentError, "Invalid socket type #{socket_type}"
111 | end
112 | end
113 |
114 | def create_session_adapter(config, adapter_name)
115 | adapter_class = SessionAdapter.select_adapter_class(adapter_name)
116 | adapter_class.new(config)
117 | end
118 | end
119 | end
120 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | push:
5 | pull_request:
6 | workflow_dispatch:
7 |
8 | jobs:
9 | ubuntu:
10 | name: Ubuntu
11 | runs-on: ${{ matrix.os }}
12 |
13 | strategy:
14 | fail-fast: false
15 | matrix:
16 | os:
17 | - ubuntu-latest
18 | ruby:
19 | - "3.4"
20 | - "3.3"
21 | - "3.2"
22 | - "3.1"
23 | - "3.0"
24 | - "2.7"
25 | - "2.6"
26 | - "2.5"
27 | - debug
28 |
29 | steps:
30 | - uses: actions/checkout@v6
31 | with:
32 | fetch-depth: 1
33 |
34 | - uses: ruby/setup-ruby@v1
35 | with:
36 | ruby-version: ${{ matrix.ruby }}
37 |
38 | - run: rake build
39 |
40 | - name: Install ffi (if Ruby 2.7)
41 | if: |
42 | matrix.ruby == '2.7'
43 | run: |
44 | cat < Gemfile.irb
45 | source 'https://rubygems.org'
46 | gem 'ffi'
47 | GEMFILE
48 | BUNDLE_GEMFILE=Gemfile.irb bundle install --jobs 4 --retry 3
49 |
50 | - name: Install ffi 1.6.x (if Ruby 2.6)
51 | if: |
52 | matrix.ruby == '2.6'
53 | run: |
54 | cat < Gemfile.irb
55 | source 'https://rubygems.org'
56 | gem 'ffi', '~> 1.6.0'
57 | GEMFILE
58 | BUNDLE_GEMFILE=Gemfile.irb bundle install --jobs 4 --retry 3
59 |
60 | - name: Install ffi 1.6.x and irb < 1.4.3 (if Ruby 2.5)
61 | if: |
62 | matrix.ruby == '2.5'
63 | run: |
64 | cat < Gemfile.irb
65 | source 'https://rubygems.org'
66 | gem 'ffi', '~> 1.6.0'
67 | gem 'irb', '< 1.4.3'
68 | GEMFILE
69 | BUNDLE_GEMFILE=Gemfile.irb bundle install --jobs 4 --retry 3
70 |
71 | - name: Install IRuby gem
72 | run: |
73 | gem install rubygems-requirements-system
74 | gem install pkg/*.gem
75 |
76 | - run: ruby -r iruby -e "p IRuby::SessionAdapter.select_adapter_class"
77 | env:
78 | IRUBY_SESSION_ADAPTER: ffi-rzmq
79 |
80 | - name: Install requirements on ubuntu
81 | run: |
82 | sudo apt update
83 | sudo apt install -y --no-install-recommends \
84 | libczmq-dev \
85 | python3 \
86 | python3-pip \
87 | python3-setuptools
88 | sudo pip3 install wheel
89 | sudo pip3 install -r ci/requirements.txt
90 |
91 | - run: bundle install --jobs 4 --retry 3
92 |
93 | - name: Run tests
94 | env:
95 | PYTHON: python3
96 | ADAPTERS: cztop ffi-rzmq
97 | run: |
98 | for adapter in $ADAPTERS; do
99 | export IRUBY_TEST_SESSION_ADAPTER_NAME=$adapter
100 | bundle exec rake test TESTOPTS="-v"
101 | done
102 |
103 | windows:
104 | name: Windows
105 | runs-on: windows-latest
106 |
107 | steps:
108 | - uses: actions/checkout@v6
109 | with:
110 | fetch-depth: 1
111 |
112 | - uses: ruby/setup-ruby@v1
113 | with:
114 | ruby-version: "ruby"
115 |
116 | - run: rake build
117 |
118 | - run: gem install pkg/*.gem
119 |
120 | - run: ruby -r iruby -e "p IRuby::SessionAdapter.select_adapter_class"
121 | env:
122 | IRUBY_SESSION_ADAPTER: ffi-rzmq
123 |
124 | macos:
125 | name: macOS
126 | runs-on: macos-latest
127 |
128 | steps:
129 | - uses: actions/checkout@v6
130 | with:
131 | fetch-depth: 1
132 |
133 | - uses: ruby/setup-ruby@v1
134 | with:
135 | ruby-version: "ruby"
136 |
137 | - run: rake build
138 |
139 | - run: gem install rubygems-requirements-system pkg/*.gem
140 |
141 | - run: ruby -r iruby -e "p IRuby::SessionAdapter.select_adapter_class"
142 | env:
143 | IRUBY_SESSION_ADAPTER: ffi-rzmq
144 |
--------------------------------------------------------------------------------
/test/helper.rb:
--------------------------------------------------------------------------------
1 | require "iruby"
2 | require "json"
3 | require "pathname"
4 | require "rbconfig"
5 | require "test/unit"
6 | require "test/unit/rr"
7 | require "tmpdir"
8 |
9 | IRuby.logger = IRuby::MultiLogger.new(*Logger.new(STDERR, level: Logger::Severity::INFO))
10 |
11 | module IRubyTest
12 | class TestBase < Test::Unit::TestCase
13 | TEST_DIR = File.expand_path("..", __FILE__).freeze
14 | EXE_DIR = File.expand_path("../exe", TEST_DIR).freeze
15 | LIB_DIR = File.expand_path("../lib", TEST_DIR).freeze
16 |
17 | RUBY = RbConfig.ruby.freeze
18 | IRUBY_PATH = File.join(EXE_DIR, "iruby").freeze
19 |
20 | def iruby_command(*args)
21 | [RUBY, "-I#{LIB_DIR}", IRUBY_PATH, *args]
22 | end
23 |
24 | def self.startup
25 | @__work_dir = Dir.mktmpdir("iruby-test-data")
26 |
27 | @__jupyter_data_dir = File.join(@__work_dir, "jupyter")
28 | @__save_jupyter_data_dir = ENV["JUPYTER_DATA_DIR"]
29 | ENV["JUPYTER_DATA_DIR"] = @__jupyter_data_dir
30 |
31 | @__config_dir = File.join(@__work_dir, "config")
32 | @__config_path = Pathname.new(@__config_dir) + "config.json"
33 | @__config_path.dirname.mkpath
34 | File.write(@__config_path, {
35 | control_port: 50160,
36 | shell_port: 57503,
37 | transport: "tcp",
38 | signature_scheme: "hmac-sha256",
39 | stdin_port: 52597,
40 | hb_port: 42540,
41 | ip: "127.0.0.1",
42 | iopub_port: 40885,
43 | key: "a0436f6c-1916-498b-8eb9-e81ab9368e84"
44 | }.to_json)
45 |
46 | @__original_kernel_instance = IRuby::Kernel.instance
47 | end
48 |
49 | def self.shutdown
50 | FileUtils.remove_entry_secure(@__work_dir)
51 | ENV["JUPYTER_DATA_DIR"] = @__save_jupyter_data_dir
52 | end
53 |
54 | def self.test_config_filename
55 | @__config_path.to_s
56 | end
57 |
58 | def self.restore_kernel
59 | IRuby::Kernel.instance = @__original_kernel_instance
60 | end
61 |
62 | def teardown
63 | self.class.restore_kernel
64 | end
65 |
66 | def with_session_adapter(session_adapter_name)
67 | IRuby::Kernel.new(self.class.test_config_filename, session_adapter_name)
68 | $stdout = STDOUT
69 | $stderr = STDERR
70 | end
71 |
72 | def assert_output(stdout=nil, stderr=nil)
73 | flunk "assert_output requires a block to capture output." unless block_given?
74 |
75 | out, err = capture_io do
76 | yield
77 | end
78 |
79 | y = check_assert_output_result(stderr, err, "stderr")
80 | x = check_assert_output_result(stdout, out, "stdout")
81 |
82 | (!stdout || x) && (!stderr || y)
83 | end
84 |
85 | private
86 |
87 | def capture_io
88 | captured_stdout = StringIO.new
89 | captured_stderr = StringIO.new
90 |
91 | orig_stdout, $stdout = $stdout, captured_stdout
92 | orig_stderr, $stderr = $stderr, captured_stderr
93 |
94 | yield
95 |
96 | return captured_stdout.string, captured_stderr.string
97 | ensure
98 | $stdout = orig_stdout
99 | $stderr = orig_stderr
100 | end
101 |
102 | def check_assert_output_result(expected, actual, name)
103 | if expected
104 | message = "In #{name}"
105 | case expected
106 | when Regexp
107 | assert_match(expected, actual, message)
108 | else
109 | assert_equal(expected, actual, message)
110 | end
111 | end
112 | end
113 |
114 | def ignore_warning
115 | saved, $VERBOSE = $VERBOSE , nil
116 | yield
117 | ensure
118 | $VERBOSE = saved
119 | end
120 |
121 | def with_env(env)
122 | keys = env.keys
123 | saved_values = ENV.values_at(*keys)
124 | ENV.update(env)
125 | yield
126 | ensure
127 | if keys && saved_values
128 | keys.zip(saved_values) do |k, v|
129 | ENV[k] = v
130 | end
131 | end
132 | end
133 |
134 | def windows_only
135 | omit('windows only test') unless windows?
136 | end
137 |
138 | def apple_only
139 | omit('apple only test') unless windows?
140 | end
141 |
142 | def unix_only
143 | omit('unix only test') if windows? || apple?
144 | end
145 |
146 | def windows?
147 | /mingw|mswin/ =~ RUBY_PLATFORM
148 | end
149 |
150 | def apple?
151 | /darwin/ =~ RUBY_PLATFORM
152 | end
153 | end
154 | end
155 |
--------------------------------------------------------------------------------
/lib/iruby/formatter.rb:
--------------------------------------------------------------------------------
1 | module IRuby
2 | module LaTeX
3 | extend self
4 |
5 | def vector(v)
6 | x = 'c' * v.size
7 | y = v.map(&:to_s).join(' & ')
8 | "$$\\left(\\begin{array}{#{x}} #{y} \\end{array}\\right)$$"
9 | end
10 |
11 | def matrix(m, row_count, column_count)
12 | s = "$$\\left(\\begin{array}{#{'c' * column_count}}\n"
13 | (0...row_count).each do |i|
14 | s << ' ' << m[i,0].to_s
15 | (1...column_count).each do |j|
16 | s << '&' << m[i,j].to_s
17 | end
18 | s << "\\\\\n"
19 | end
20 | s << "\\end{array}\\right)$$"
21 | end
22 | end
23 |
24 | module HTML
25 | extend self
26 |
27 | def table(obj, maxrows: 15, maxcols: 15, **options)
28 | raise ArgumentError, 'Invalid :maxrows' if maxrows && maxrows < 3
29 | raise ArgumentError, 'Invalid :maxcols' if maxcols && maxcols < 3
30 |
31 | return obj unless obj.respond_to?(:each)
32 |
33 | rows = []
34 |
35 | if obj.respond_to?(:keys)
36 | # Hash of Arrays
37 | header = obj.keys
38 | keys = (0...obj.keys.size).to_a
39 | cols = obj.values.map {|x| [x].flatten(1) }
40 | num_rows = cols.map(&:size).max
41 | rows = []
42 | (0...num_rows).each do |i|
43 | rows << []
44 | (0...cols.size).each do |j|
45 | rows[i][j] = cols[j][i]
46 | end
47 | end
48 | else
49 | keys = nil
50 | array_size = 0
51 |
52 | obj.each do |row|
53 | if row.respond_to?(:keys)
54 | # Array of Hashes
55 | keys ||= Set.new
56 | keys.merge(row.keys)
57 | elsif row.respond_to?(:map)
58 | # Array of Arrays
59 | array_size = row.size if array_size < row.size
60 | end
61 | rows << row
62 | end
63 |
64 | if keys
65 | header = keys.to_a
66 | keys.merge(0...array_size)
67 | else
68 | keys = 0...array_size
69 | end
70 | keys = keys.to_a
71 | end
72 |
73 | header ||= keys if options[:header]
74 |
75 | rows1, rows2 = rows, nil
76 | keys1, keys2 = keys, nil
77 | header1, header2 = header, nil
78 |
79 | if maxcols && keys.size > maxcols
80 | keys1 = keys[0...maxcols / 2]
81 | keys2 = keys[-maxcols / 2 + 1..-1]
82 | if header
83 | header1 = header[0...maxcols / 2]
84 | header2 = header[-maxcols / 2 + 1..-1]
85 | end
86 | end
87 |
88 | if maxrows && rows.size > maxrows
89 | rows1 = rows[0...maxrows / 2]
90 | rows2 = rows[-maxrows / 2 + 1..-1]
91 | end
92 |
93 | table = +''
94 |
95 | if header1 && options[:header] != false
96 | table << '' << header1.map {|k| "| #{cell k} | " }.join
97 | table << "… | " << header2.map {|k| "#{cell k} | " }.join if keys2
98 | table << '
'
99 | end
100 |
101 | row_block(table, rows1, keys1, keys2)
102 |
103 | if rows2
104 | table << "| 1 ? " colspan='#{keys1.size}'" : ''}>⋮ | "
105 | table << "⋱ | 1 ? " colspan='#{keys2.size}'" : ''}>⋮ | " if keys2
106 | table << '
'
107 |
108 | row_block(table, rows2, keys1, keys2)
109 | end
110 |
111 | table << '
'
112 | end
113 |
114 | private
115 |
116 | def cell(obj)
117 | obj.respond_to?(:to_html) ? obj.to_html : obj
118 | end
119 |
120 | def elem(row, k)
121 | cell((row[k] rescue nil))
122 | end
123 |
124 | def row_block(table, rows, keys1, keys2)
125 | cols = keys1.size
126 | cols += keys2.size + 1 if keys2
127 | rows.each_with_index do |row, i|
128 | table << ''
129 | if row.respond_to?(:map)
130 | row_html = keys1.map {|k| "| #{elem row, k} | " }.join
131 | if keys2
132 | row_html << " 1 ? " rowspan='#{rows.size}'" : ''}>… | " if i == 0
133 | row_html << keys2.map {|k| "#{elem row, k} | " }.join
134 | end
135 | if row_html.empty?
136 | table << " 1 ? " colspan='#{cols}'" : ''}> | "
137 | else
138 | table << row_html
139 | end
140 | else
141 | table << " 1 ? " colspan='#{cols}'" : ''}>#{cell row} | "
142 | end
143 | table << '
'
144 | end
145 | end
146 | end
147 | end
148 |
--------------------------------------------------------------------------------
/lib/iruby/backend.rb:
--------------------------------------------------------------------------------
1 | module IRuby
2 | In, Out = [nil], [nil]
3 |
4 | class << self
5 | attr_accessor :silent_assignment
6 | end
7 | self.silent_assignment = false
8 |
9 | module History
10 | def eval(code, store_history)
11 | b = eval_binding
12 |
13 | b.local_variable_set(:_ih, In) unless b.local_variable_defined?(:_ih)
14 | b.local_variable_set(:_oh, Out) unless b.local_variable_defined?(:_oh)
15 |
16 | out = super
17 |
18 | # TODO Add IRuby.cache_size which controls the size of the Out array
19 | # and sets the oldest entries and _ variables to nil.
20 | if store_history
21 | b.local_variable_set("_#{Out.size}", out)
22 | b.local_variable_set("_i#{In.size}", code)
23 |
24 | Out << out
25 | In << code
26 |
27 | b.local_variable_set(:___, Out[-3])
28 | b.local_variable_set(:__, Out[-2])
29 | b.local_variable_set(:_, Out[-1])
30 | b.local_variable_set(:_iii, In[-3])
31 | b.local_variable_set(:_ii, In[-2])
32 | b.local_variable_set(:_i, In[-1])
33 | end
34 |
35 | out
36 | end
37 | end
38 |
39 | class PlainBackend
40 | attr_reader :eval_path
41 | prepend History
42 |
43 | def initialize
44 | require 'irb'
45 | require 'irb/completion'
46 | IRB.setup(nil)
47 | @main = TOPLEVEL_BINDING.eval("self").dup
48 | init_main_object(@main)
49 | @workspace = IRB::WorkSpace.new(@main)
50 | @irb = IRB::Irb.new(@workspace)
51 | @eval_path = @irb.context.irb_path
52 | IRB.conf[:MAIN_CONTEXT] = @irb.context
53 | @completor = IRB::RegexpCompletor.new if defined? IRB::RegexpCompletor # IRB::VERSION >= 1.8.2
54 | end
55 |
56 | def eval_binding
57 | @workspace.binding
58 | end
59 |
60 | def eval(code, store_history)
61 | @irb.context.evaluate(parse_code(code), 0)
62 | @irb.context.last_value unless IRuby.silent_assignment && assignment_expression?(code)
63 | end
64 |
65 | def parse_code(code)
66 | return code if Gem::Version.new(IRB::VERSION) < Gem::Version.new('1.13.0')
67 | return @irb.parse_input(code) if @irb.respond_to?(:parse_input)
68 | return @irb.build_statement(code) if @irb.respond_to?(:build_statement)
69 | end
70 |
71 | def complete(code)
72 | if @completor
73 | # preposing and postposing never used, so they are empty, pass only target as code
74 | @completor.completion_candidates('', code, '', bind: @workspace.binding)
75 | else
76 | IRB::InputCompletor::CompletionProc.call(code)
77 | end
78 | end
79 |
80 | private
81 |
82 | def init_main_object(main)
83 | wrapper_module = Module.new
84 | main.extend(wrapper_module)
85 | main.define_singleton_method(:include) do |*args|
86 | wrapper_module.include(*args)
87 | end
88 | end
89 |
90 | def assignment_expression?(code)
91 | @irb.respond_to?(:assignment_expression?) && @irb.assignment_expression?(code)
92 | end
93 | end
94 |
95 | class PryBackend
96 | attr_reader :eval_path
97 | prepend History
98 |
99 | def initialize
100 | require 'pry'
101 | Pry.memory_size = 3
102 | Pry.pager = false # Don't use the pager
103 | Pry.print = proc {|output, value|} # No result printing
104 | Pry.exception_handler = proc {|output, exception, _| }
105 | @eval_path = Pry.eval_path
106 | reset
107 | end
108 |
109 | def eval_binding
110 | TOPLEVEL_BINDING
111 | end
112 |
113 | def eval(code, store_history)
114 | Pry.current_line = 1
115 | @pry.last_result = nil
116 | unless @pry.eval(code)
117 | reset
118 | raise SystemExit
119 | end
120 |
121 | # Pry::Code.complete_expression? return false
122 | if !@pry.eval_string.empty?
123 | syntax_error = @pry.eval_string
124 | @pry.reset_eval_string
125 | @pry.evaluate_ruby(syntax_error)
126 |
127 | # Pry::Code.complete_expression? raise SyntaxError
128 | # evaluate again for current line number
129 | elsif @pry.last_result_is_exception? &&
130 | @pry.last_exception.is_a?(SyntaxError) &&
131 | @pry.last_exception.is_a?(Pry::UserError)
132 | @pry.evaluate_ruby(code)
133 | end
134 |
135 | raise @pry.last_exception if @pry.last_result_is_exception?
136 | @pry.push_initial_binding unless @pry.current_binding # ensure that we have a binding
137 | @pry.last_result
138 | end
139 |
140 | def complete(code)
141 | @pry.complete(code)
142 | end
143 |
144 | def reset
145 | @pry = Pry.new(output: $stdout, target: eval_binding)
146 | end
147 | end
148 | end
149 |
--------------------------------------------------------------------------------
/test/iruby/application/register_test.rb:
--------------------------------------------------------------------------------
1 | require_relative "helper"
2 |
3 | module IRubyTest::ApplicationTests
4 | class RegisterTest < ApplicationTestBase
5 | sub_test_case("when the existing IRuby kernel is in IPython's kernels directory") do
6 | def setup
7 | Dir.mktmpdir do |tmpdir|
8 | ipython_dir = File.join(tmpdir, ".ipython")
9 | # prepare the existing IRuby kernel with the default name
10 | with_env("JUPYTER_DATA_DIR" => ipython_dir) do
11 | ensure_iruby_kernel_is_installed
12 | end
13 |
14 | fake_bin_dir = File.join(tmpdir, "bin")
15 | fake_jupyter = File.join(fake_bin_dir, "jupyter")
16 | FileUtils.mkdir_p(fake_bin_dir)
17 | IO.write(fake_jupyter, <<-FAKE_JUPYTER)
18 | #!/usr/bin/env ruby
19 | puts "Fake Jupyter"
20 | FAKE_JUPYTER
21 | File.chmod(0o755, fake_jupyter)
22 |
23 | new_path = [fake_bin_dir, ENV["PATH"]].join(File::PATH_SEPARATOR)
24 | with_env(
25 | "HOME" => tmpdir,
26 | "PATH" => new_path,
27 | "JUPYTER_DATA_DIR" => nil,
28 | "IPYTHONDIR" => nil
29 | ) do
30 | yield
31 | end
32 | end
33 | end
34 |
35 | test("IRuby warns the existence of the kernel in IPython's kernels directory and executes `jupyter kernelspec install` command") do
36 | out, status = Open3.capture2e(*iruby_command("register"))
37 | assert status.success?
38 | assert_match(/^Fake Jupyter$/, out)
39 | assert_match(/^#{Regexp.escape("IRuby kernel `#{DEFAULT_KERNEL_NAME}` already exists in the deprecated IPython's data directory.")}$/,
40 | out)
41 | end
42 | end
43 |
44 | sub_test_case("when the existing IRuby kernel is in Jupyter's default kernels directory") do
45 | # TODO
46 | end
47 |
48 | sub_test_case("JUPYTER_DATA_DIR is supplied") do
49 | def setup
50 | Dir.mktmpdir do |tmpdir|
51 | @kernel_json = File.join(tmpdir, "kernels", DEFAULT_KERNEL_NAME, "kernel.json")
52 | with_env(
53 | "JUPYTER_DATA_DIR" => tmpdir,
54 | "IPYTHONDIR" => nil
55 | ) do
56 | yield
57 | end
58 | end
59 | end
60 |
61 | test("a new IRuby kernel `#{DEFAULT_KERNEL_NAME}` will be installed in JUPYTER_DATA_DIR") do
62 | assert_path_not_exist @kernel_json
63 |
64 | _out, status = Open3.capture2e(*iruby_command("register"))
65 | assert status.success?
66 | assert_path_exist @kernel_json
67 |
68 | kernel = JSON.load(File.read(@kernel_json))
69 | assert_equal DEFAULT_DISPLAY_NAME, kernel["display_name"]
70 | end
71 |
72 | sub_test_case("there is a IRuby kernel in JUPYTER_DATA_DIR") do
73 | def setup
74 | super do
75 | FileUtils.mkdir_p(File.dirname(@kernel_json))
76 | File.write(@kernel_json, '"dummy kernel"')
77 | assert_equal '"dummy kernel"', File.read(@kernel_json)
78 | yield
79 | end
80 | end
81 |
82 | test("warn the existence of the kernel") do
83 | out, status = Open3.capture2e(*iruby_command("register"))
84 | refute status.success?
85 | assert_match(/^#{Regexp.escape("IRuby kernel named `#{DEFAULT_KERNEL_NAME}` already exists!")}$/,
86 | out)
87 | assert_match(/^#{Regexp.escape("Use --force to force register the new kernel.")}$/,
88 | out)
89 | end
90 |
91 | test("the existing kernel is not overwritten") do
92 | _out, status = Open3.capture2e(*iruby_command("register"))
93 | refute status.success?
94 | assert_equal '"dummy kernel"', File.read(@kernel_json)
95 | end
96 |
97 | sub_test_case("`--force` option is specified") do
98 | test("the existing kernel is overwritten by the new kernel") do
99 | out, status = Open3.capture2e(*iruby_command("register", "--force"))
100 | assert status.success?
101 | assert_not_match(/^#{Regexp.escape("IRuby kernel named `#{DEFAULT_KERNEL_NAME}` already exists!")}$/,
102 | out)
103 | assert_not_match(/^#{Regexp.escape("Use --force to force register the new kernel.")}$/,
104 | out)
105 | assert_not_equal '"dummy kernel"', File.read(@kernel_json)
106 | end
107 | end
108 | end
109 | end
110 |
111 | sub_test_case("both JUPYTER_DATA_DIR and IPYTHONDIR are supplied") do
112 | def setup
113 | Dir.mktmpdir do |tmpdir|
114 | Dir.mktmpdir do |tmpdir2|
115 | with_env(
116 | "JUPYTER_DATA_DIR" => tmpdir,
117 | "IPYTHONDIR" => tmpdir2
118 | ) do
119 | yield
120 | end
121 | end
122 | end
123 | end
124 |
125 | test("warn for IPYTHONDIR") do
126 | out, status = Open3.capture2e(*iruby_command("register"))
127 | assert status.success?
128 | assert_match(/^#{Regexp.escape("both JUPYTER_DATA_DIR and IPYTHONDIR are supplied; IPYTHONDIR is ignored.")}$/,
129 | out)
130 | end
131 |
132 | test("a new kernel is installed in JUPYTER_DATA_DIR") do
133 | _out, status = Open3.capture2e(*iruby_command("register"))
134 | assert status.success?
135 | assert_path_exist File.join(ENV["JUPYTER_DATA_DIR"], "kernels", DEFAULT_KERNEL_NAME, "kernel.json")
136 | end
137 | end
138 | end
139 | end
140 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # IRuby
2 |
3 | [](https://badge.fury.io/rb/iruby)
4 | [](https://github.com/SciRuby/iruby/actions)
5 | [](https://mybinder.org/v2/gh/RubyData/binder/master?filepath=ruby-data.ipynb)
6 |
7 | IRuby is a Ruby kernel for [Jupyter project](https://jupyter.org/).
8 |
9 | ## Try IRuby
10 |
11 | You can try IRuby with a sample notebook on Binder (the same link as the banner placed above):
12 |
13 |
14 |
15 | The following URL launches JupyterLab directly on Binder.
16 |
17 |
18 |
19 | ## Installation
20 |
21 | ### Requirements
22 |
23 | - [Jupyter](https://jupyter.org)
24 |
25 | The following requirements are automatically installed.
26 |
27 | - [ffi-rzmq](https://github.com/chuckremes/ffi-rzmq)
28 | - [libzmq](https://github.com/zeromq/libzmq)
29 |
30 | The following dependencies are optional.
31 |
32 | - [Pry][Pry], if you want to use [Pry][Pry] instead of IRB for the code execution backend
33 |
34 | ### Installing Jupyter Notebook and/or JupyterLab
35 |
36 | See the official document to know how to install Jupyter Notebook and/or JupyterLab.
37 |
38 | -
39 | -
40 |
41 | ### Ubuntu
42 |
43 | #### Ubuntu 22.04+
44 |
45 | ```shell
46 | sudo apt install libtool libffi-dev ruby ruby-dev make
47 |
48 | gem install --user-install rubygems-requirements-system
49 | gem install --user-install iruby
50 | iruby register --force
51 | ```
52 |
53 | ### Fedora
54 |
55 | #### Fedora 40+
56 |
57 | ```shell
58 | sudo dnf install ruby ruby-dev make zeromq-devel
59 |
60 | gem install --user-install rubygems-requirements-system
61 | gem install --user-install iruby
62 | iruby register --force
63 | ```
64 |
65 | ### Windows
66 |
67 | [DevKit](https://rubyinstaller.org/add-ons/devkit.html) is necessary for building RubyGems with native C-based extensions.
68 |
69 | ```shell
70 | gem install iruby
71 | iruby register --force
72 | ```
73 |
74 | ### macOS
75 |
76 | Install ruby with rbenv or rvm.
77 | Install Jupyter.
78 |
79 | #### Homebrew
80 |
81 | ```shell
82 | gem install rubygems-requirements-system
83 | gem install iruby
84 | iruby register --force
85 | ```
86 |
87 | #### MacPorts
88 |
89 | If you are using macports, run the following commands.
90 |
91 | ```shell
92 | port install libtool autoconf automake autogen
93 | gem install rubygems-requirements-system
94 | gem install iruby
95 | iruby register --force
96 | ```
97 |
98 | ### Docker
99 |
100 | Try [RubyData Docker Stacks](https://github.com/RubyData/docker-stacks).
101 | Running jupyter notebook:
102 |
103 | ```shell
104 | docker run --rm -it -p 8888:8888 rubydata/datascience-notebook
105 | ```
106 |
107 | ### Installation for JRuby
108 |
109 | You can use Java classes in your IRuby notebook.
110 |
111 | - JRuby version >= 9.0.4.0
112 | - iruby gem
113 |
114 | After installation, make sure that your `env` is set up to use jruby.
115 |
116 | ```shell
117 | env ruby -v
118 | ```
119 |
120 | If you use RVM, it is enough to switch the current version to jruby.
121 |
122 | If you have already used IRuby with a different version, you need to generate a new kernel:
123 |
124 | ```shell
125 | iruby register --force
126 | ```
127 |
128 | ### Install the development version of IRuby
129 |
130 | **Be careful to use the development version because it is usually unstable.**
131 |
132 | If you want to install the development version of IRuby from the source code, try [specific_install](https://github.com/rdp/specific_install).
133 |
134 | ```
135 | gem specific_install https://github.com/SciRuby/iruby
136 | ```
137 |
138 | ### Note for using with CZTop and CZMQ
139 |
140 | [CZTop](https://gitlab.com/paddor/cztop) adapter has been deprecated since IRuby version 0.7.4.
141 | It will be removed after several versions.
142 |
143 | If you want to use IRuby with CZTop, you need to install it and [CZMQ](https://github.com/zeromq/czmq).
144 |
145 | If both ffi-rzmq and cztop are installed, ffi-rzmq is used. If you prefer cztop, set the following environment variable.
146 |
147 | ```sh
148 | export IRUBY_SESSION_ADAPTER="cztop"
149 | ```
150 |
151 | ## Backends
152 |
153 | There are two backends: PlainBackend and PryBackend.
154 |
155 | - PlainBackend is the default backend. It uses [IRB](https://github.com/ruby/irb).
156 | - PryBackend uses [Pry][Pry].
157 |
158 | You can switch the backend to PryBackend by running the code below.
159 |
160 | ```ruby
161 | IRuby::Kernel.instance.switch_backend!(:pry)
162 | ```
163 |
164 | ## Notebooks
165 |
166 | Take a look at the [example notebook](https://nbviewer.jupyter.org/urls/raw.github.com/SciRuby/sciruby-notebooks/master/getting_started.ipynb)
167 | and the [collection of notebooks](https://github.com/SciRuby/sciruby-notebooks/) which includes a Dockerfile to create a containerized installation of iruby
168 | and other scientific gems.
169 |
170 | ## Contributing
171 |
172 | Contributions to IRuby are very welcome.
173 |
174 | To former contributors
175 |
176 | In February 2021, [IRuby became the canonical repository](https://github.com/SciRuby/iruby/issues/285) and is no longer a fork from [minrk/iruby](https://github.com/minrk/iruby). Please fork from this repository again before making pull requests.
177 |
178 | ## License
179 |
180 | Copyright (c) IRuby contributors and the Ruby Science Foundation.
181 |
182 | Licensed under the [MIT](LICENSE) license.
183 |
184 | [Pry]: https://github.com/pry/pry
185 |
--------------------------------------------------------------------------------
/test/iruby/kernel_test.rb:
--------------------------------------------------------------------------------
1 | module IRubyTest
2 | class KernelTest < TestBase
3 | def setup
4 | super
5 | with_session_adapter("test")
6 | @kernel = IRuby::Kernel.instance
7 | end
8 |
9 | sub_test_case("iruby_initialized event") do
10 | def setup
11 | super
12 | @initialized_kernel = nil
13 | @callback = IRuby::Kernel.events.register(:initialized) do |kernel|
14 | @initialized_kernel = kernel
15 | end
16 | end
17 |
18 | def teardown
19 | IRuby::Kernel.events.unregister(:initialized, @callback)
20 | end
21 |
22 | def test_iruby_initialized_event
23 | with_session_adapter("test")
24 | assert_same(IRuby::Kernel.instance, @initialized_kernel)
25 | end
26 | end
27 |
28 | def test_execute_request
29 | obj = Object.new
30 |
31 | class << obj
32 | def to_html
33 | "HTML"
34 | end
35 |
36 | def inspect
37 | "!!! inspect !!!"
38 | end
39 | end
40 |
41 | ::IRubyTest.define_singleton_method(:test_object) { obj }
42 |
43 | msg_types = []
44 | execute_reply = nil
45 | execute_result = nil
46 | @kernel.session.adapter.send_callback = ->(sock, msg) do
47 | header = msg[:header]
48 | content = msg[:content]
49 | msg_types << header["msg_type"]
50 | case header["msg_type"]
51 | when "execute_reply"
52 | execute_reply = content
53 | when "execute_result"
54 | execute_result = content
55 | end
56 | end
57 |
58 | msg = {
59 | content: {
60 | "code" => "IRubyTest.test_object",
61 | "silent" => false,
62 | "store_history" => false,
63 | "user_expressions" => {},
64 | "allow_stdin" => false,
65 | "stop_on_error" => true,
66 | }
67 | }
68 | @kernel.execute_request(msg)
69 |
70 | assert_equal({
71 | msg_types: [ "execute_input", "execute_result", "execute_reply" ],
72 | execute_reply: {
73 | status: "ok",
74 | user_expressions: {},
75 | },
76 | execute_result: {
77 | data: {
78 | "text/html" => "HTML",
79 | "text/plain" => "!!! inspect !!!"
80 | },
81 | metadata: {},
82 | }
83 | },
84 | {
85 | msg_types: msg_types,
86 | execute_reply: {
87 | status: execute_reply["status"],
88 | user_expressions: execute_reply["user_expressions"]
89 | },
90 | execute_result: {
91 | data: execute_result["data"],
92 | metadata: execute_result["metadata"]
93 | }
94 | })
95 | end
96 |
97 | def test_events_around_of_execute_request
98 | event_history = []
99 |
100 | @kernel.events.register(:pre_execute) do
101 | event_history << :pre_execute
102 | end
103 |
104 | @kernel.events.register(:pre_run_cell) do |exec_info|
105 | event_history << [:pre_run_cell, exec_info]
106 | end
107 |
108 | @kernel.events.register(:post_execute) do
109 | event_history << :post_execute
110 | end
111 |
112 | @kernel.events.register(:post_run_cell) do |result|
113 | event_history << [:post_run_cell, result]
114 | end
115 |
116 | msg = {
117 | content: {
118 | "code" => "true",
119 | "silent" => false,
120 | "store_history" => false,
121 | "user_expressions" => {},
122 | "allow_stdin" => false,
123 | "stop_on_error" => true,
124 | }
125 | }
126 | @kernel.execute_request(msg)
127 |
128 | msg = {
129 | content: {
130 | "code" => "true",
131 | "silent" => true,
132 | "store_history" => false,
133 | "user_expressions" => {},
134 | "allow_stdin" => false,
135 | "stop_on_error" => true,
136 | }
137 | }
138 | @kernel.execute_request(msg)
139 |
140 | assert_equal([
141 | :pre_execute,
142 | [:pre_run_cell, IRuby::ExecutionInfo.new("true", false, false)],
143 | :post_execute,
144 | [:post_run_cell, true],
145 | :pre_execute,
146 | :post_execute
147 | ],
148 | event_history)
149 | end
150 |
151 | sub_test_case("#switch_backend!") do
152 | sub_test_case("") do
153 | def test_switch_backend
154 | classes = []
155 |
156 | # First pick the default backend class
157 | classes << @kernel.instance_variable_get(:@backend).class
158 |
159 | @kernel.switch_backend!(:pry)
160 | classes << @kernel.instance_variable_get(:@backend).class
161 |
162 | @kernel.switch_backend!(:irb)
163 | classes << @kernel.instance_variable_get(:@backend).class
164 |
165 | @kernel.switch_backend!(:pry)
166 | classes << @kernel.instance_variable_get(:@backend).class
167 |
168 | @kernel.switch_backend!(:plain)
169 | classes << @kernel.instance_variable_get(:@backend).class
170 |
171 | assert_equal([
172 | IRuby::PlainBackend,
173 | IRuby::PryBackend,
174 | IRuby::PlainBackend,
175 | IRuby::PryBackend,
176 | IRuby::PlainBackend
177 | ],
178 | classes)
179 | end
180 | end
181 | end
182 | end
183 | end
184 |
--------------------------------------------------------------------------------
/test/iruby/display_test.rb:
--------------------------------------------------------------------------------
1 | module IRubyTest
2 | class DisplayTest < TestBase
3 | def setup
4 | @object = Object.new
5 | @object.instance_variable_set(:@to_html_called, false)
6 | @object.instance_variable_set(:@to_markdown_called, false)
7 | @object.instance_variable_set(:@to_iruby_called, false)
8 | @object.instance_variable_set(:@to_iruby_mimebundle_called, false)
9 |
10 | class << @object
11 | attr_reader :to_html_called
12 | attr_reader :to_markdown_called
13 | attr_reader :to_iruby_called
14 | attr_reader :to_iruby_mimebundle_called
15 |
16 | def html
17 | "html"
18 | end
19 |
20 | def markdown
21 | "*markdown*"
22 | end
23 |
24 | def inspect
25 | "!!! inspect !!!"
26 | end
27 | end
28 | end
29 |
30 | def define_to_html
31 | class << @object
32 | def to_html
33 | @to_html_called = true
34 | html
35 | end
36 | end
37 | end
38 |
39 | def define_to_markdown
40 | class << @object
41 | def to_markdown
42 | @to_markdown_called = true
43 | markdown
44 | end
45 | end
46 | end
47 |
48 | def define_to_iruby
49 | class << @object
50 | def to_iruby
51 | @to_iruby_called = true
52 | ["text/html", "to_iruby"]
53 | end
54 | end
55 | end
56 |
57 | def define_to_iruby_mimebundle
58 | class << @object
59 | def to_iruby_mimebundle(include: [])
60 | @to_iruby_mimebundle_called = true
61 | mimes = if include.empty?
62 | ["text/markdown", "application/json"]
63 | else
64 | include
65 | end
66 | formats = mimes.map { |mime|
67 | result = case mime
68 | when "text/markdown"
69 | "**markdown**"
70 | when "application/json"
71 | %Q[{"mimebundle": "json"}]
72 | end
73 | [mime, result]
74 | }.to_h
75 | metadata = {}
76 | return formats, metadata
77 | end
78 | end
79 | end
80 |
81 | def assert_iruby_display(expected)
82 | assert_equal(expected,
83 | {
84 | result: IRuby::Display.display(@object),
85 | to_html_called: @object.to_html_called,
86 | to_markdown_called: @object.to_markdown_called,
87 | to_iruby_called: @object.to_iruby_called,
88 | to_iruby_mimebundle_called: @object.to_iruby_mimebundle_called
89 | })
90 | end
91 |
92 | sub_test_case("the object cannot handle all the mime types") do
93 | def test_display
94 | assert_iruby_display({
95 | result: {"text/plain" => "!!! inspect !!!"},
96 | to_html_called: false,
97 | to_markdown_called: false,
98 | to_iruby_called: false,
99 | to_iruby_mimebundle_called: false
100 | })
101 | end
102 | end
103 |
104 | sub_test_case("the object can respond to to_iruby") do
105 | def setup
106 | super
107 | define_to_iruby
108 | end
109 |
110 | def test_display
111 | assert_iruby_display({
112 | result: {
113 | "text/html" => "to_iruby",
114 | "text/plain" => "!!! inspect !!!"
115 | },
116 | to_html_called: false,
117 | to_markdown_called: false,
118 | to_iruby_called: true,
119 | to_iruby_mimebundle_called: false
120 | })
121 | end
122 |
123 | sub_test_case("the object can respond to to_markdown") do
124 | def setup
125 | super
126 | define_to_markdown
127 | end
128 |
129 | def test_display
130 | assert_iruby_display({
131 | result: {
132 | "text/markdown" => "*markdown*",
133 | "text/plain" => "!!! inspect !!!"
134 | },
135 | to_html_called: false,
136 | to_markdown_called: true,
137 | to_iruby_called: false,
138 | to_iruby_mimebundle_called: false
139 | })
140 | end
141 | end
142 |
143 | sub_test_case("the object can respond to to_html") do
144 | def setup
145 | super
146 | define_to_html
147 | end
148 |
149 | def test_display
150 | assert_iruby_display({
151 | result: {
152 | "text/html" => "html",
153 | "text/plain" => "!!! inspect !!!"
154 | },
155 | to_html_called: true,
156 | to_markdown_called: false,
157 | to_iruby_called: false,
158 | to_iruby_mimebundle_called: false
159 | })
160 | end
161 |
162 | sub_test_case("the object can respond to to_iruby_mimebundle") do
163 | def setup
164 | super
165 | define_to_iruby_mimebundle
166 | end
167 |
168 | def test_display
169 | assert_iruby_display({
170 | result: {
171 | "text/markdown" => "**markdown**",
172 | "application/json" => %Q[{"mimebundle": "json"}],
173 | "text/plain" => "!!! inspect !!!"
174 | },
175 | to_html_called: false,
176 | to_markdown_called: false,
177 | to_iruby_called: false,
178 | to_iruby_mimebundle_called: true
179 | })
180 | end
181 | end
182 | end
183 | end
184 | end
185 | end
186 |
--------------------------------------------------------------------------------
/lib/iruby/input/README.md:
--------------------------------------------------------------------------------
1 |
2 | # IRuby Input
3 |
4 | This README is generated from README.ipynb. Please do not edit this file directly.
5 |
6 | The `IRuby::Input` class makes it easier for IRuby users to get input from users. For example:
7 |
8 |
9 | ```ruby
10 | name = IRuby.input 'Enter your name'
11 | ```
12 |
13 | The following input methods are supported on the `IRuby` module:
14 |
15 | | method | description |
16 | | -------- | -------- |
17 | | `input(prompt)` | Prompts the user for a line of input |
18 | | `password(prompt)` | Prompts the user for a password |
19 | | `form(&block)` | Presents a form to the user |
20 | | `popup(title,&block)` | Displays a form to the user as a popup |
21 |
22 | ## Forms
23 |
24 | Forms are groups of inputs to be collected from the user. For example:
25 |
26 |
27 | ```ruby
28 | result = IRuby.form do
29 | input :username
30 | password :password
31 | button
32 | end
33 | ```
34 |
35 | The following methods are available to build forms:
36 |
37 | | method | description |
38 | | -------- | -------- |
39 | | `input(key=:input)` | Prompts the user for a line of input |
40 | | `textarea(key=:textarea),` | Adds a textarea to the form |
41 | | `password(key=:password)` | Prompts the user for a password |
42 | | `button(key=:done, color: :blue)` | Adds a submit button to the form |
43 | | `cancel(prompt='Cancel')` | Adds a cancel button to the form |
44 | | `text(string)` | Adds text to the form |
45 | | `html(&block)` | Inserts HTML from the given [erector block](https://github.com/erector/erector) |
46 | | `file(key=:file)` | Adds a file input to the form |
47 | | `date(key=:date)` | Adds a date picker to the form |
48 | | `select(*options)` | Adds a dropdown select input to the form |
49 | | `radio(*options)` | Adds a radio select input to the form |
50 | | `checkbox(*options)` | Adds checkbox inputs to the form |
51 |
52 | ## Popups
53 |
54 | Popups are just forms in a bootstrap modal. They are useful when users **Run All** in a notebook with a lot of inputs. The popups always appear in the same spot, so users don't have to scroll down to find the next input.
55 |
56 | Popups accept a `title` argument, for example:
57 |
58 |
59 | ```ruby
60 | result = IRuby.popup 'Please enter your name' do
61 | input
62 | button
63 | end
64 | ```
65 |
66 | ## Submit and cancel
67 |
68 | The enter key will submit an input or form and the escape key will cancel it. Canceled inputs are returned as `nil`. Inputs are automatically canceled if destroyed. An input can be destroyed by clearing its cell's output. The `cancel` button will cancel a form and all other buttons will submit it.
69 |
70 | After a form destroyed, the cell's output is cleared. Be careful not to prompt for input in a block that has previous output you would like to keep. Output is cleared to prevent forms from interfering with one another and to ensure that inputs are not inadvertently saved to the notebook.
71 |
72 |
73 | ```ruby
74 | result = IRuby.popup 'Confirm' do
75 | text 'Are you sure you want to continue?'
76 | cancel 'No'
77 | button 'Yes'
78 | end
79 | ```
80 |
81 | ## Custom keys
82 |
83 | Every widget has an entry in the final results hash. A custom key can be passed as the first parameter to the hash. If no key is provided, the widget name is used as the key. The `cancel` widget has no key; it's first parameter is its label.
84 |
85 |
86 | ```ruby
87 | result = IRuby.form do
88 | input :username
89 | password :password
90 | end
91 | ```
92 |
93 | ## Custom labels
94 |
95 | Field labels appear to the left of the field. Button labels appear as the text on the button. `cancel` labels are passed as the first argument. All other widgets' labels are set using the `label` parameter.
96 |
97 |
98 | ```ruby
99 | result = IRuby.form do
100 | input :name, label: 'Please enter your name'
101 | cancel 'None of your business!'
102 | button :submit, label: 'All done'
103 | end
104 | ```
105 |
106 | ## Defaults
107 |
108 | Most inputs will accept a `default` parameter. If no default is given, the default is `nil`. Since checkboxes can have multiple values selected, you can pass an array of values. To check everything, pass `true` as the default.
109 |
110 |
111 | ```ruby
112 | result = IRuby.form do
113 | checkbox :one, 'Fish', 'Cat', 'Dog', default: 'Fish'
114 | checkbox :many, 'Fish', 'Cat', 'Dog', default: ['Cat', 'Dog']
115 | checkbox :all, 'Fish', 'Cat', 'Dog', default: true
116 | button :submit, label: 'All done'
117 | end
118 | ```
119 |
120 | ## Dates
121 |
122 | The `date` widget provides a calendar popup and returns a `Time` object. It's default should also be a `Time` object.
123 |
124 |
125 | ```ruby
126 | result = IRuby.form do
127 | date :birthday
128 | date :today, default: Time.now
129 | button
130 | end
131 | ```
132 |
133 | ## Buttons
134 |
135 | Buttons do not appear in the final hash unless they are clicked. If clicked, their value is `true`. Here are the various colors a button can be:
136 |
137 |
138 | ```ruby
139 | result = IRuby.form do
140 | IRuby::Input::Button::COLORS.each_key do |color|
141 | button color, color: color
142 | end
143 | end
144 | ```
145 |
146 | ## Textareas
147 |
148 | Textareas are multiline inputs that are convenient for larger inputs. If you need a line return when typing in a textarea, use shift+enter since enter will submit the form.
149 |
150 |
151 | ```ruby
152 | result = IRuby.form do
153 | text 'Enter email addresses, one per line (use shift+enter for newlines)'
154 | textarea :emails
155 | end
156 | ```
157 |
158 | ## Text and HTML
159 |
160 | You can insert lines of text or custom html using their respective helpers:
161 |
162 |
163 | ```ruby
164 | result = IRuby.form do
165 | html { h1 'Choose a Stooge' }
166 | text 'Choose your favorite stooge'
167 | select :stooge, 'Moe', 'Larry', 'Curly'
168 | button
169 | end
170 | ```
171 |
172 | ## Dropdowns
173 |
174 | A `select` is a dropdown of options. Use a `multiple` to allow multiple selections. `multiple` widgets accept an additional `size` parameters that determines the number of rows. The default is 4.
175 |
176 |
177 | ```ruby
178 | result = IRuby.form do
179 | select :stooge, 'Moe', 'Larry', 'Curly'
180 | select :stooge, 'Moe', 'Larry', 'Curly', default: 'Moe'
181 | multiple :stooges, 'Moe', 'Larry', 'Curly', default: true, size: 3
182 | multiple :stooges, 'Moe', 'Larry', 'Curly', default: ['Moe','Curly']
183 | button
184 | end
185 | ```
186 |
187 | ## Radio selects and checkboxes
188 |
189 | Like selects, radio selects and checkboxes take multiple arguments, each one an option. If the first argument is a symbol, it is used as the key.
190 |
191 | Note that the `checkbox` widget will always return `nil` or an array.
192 |
193 |
194 | ```ruby
195 | result = IRuby.form do
196 | radio :children, *(0..12), label: 'How many children do you have?'
197 | checkbox :gender, 'Male', 'Female', 'Intersex', label: 'Select the genders of your children'
198 | button
199 | end
200 | ```
201 |
202 | ## Files
203 |
204 | Since file widgets capture the enter key, you should include a button when creating forms that contain only a file input:
205 |
206 |
207 | ```ruby
208 | IRuby.form do
209 | file :avatar, label: 'Choose an Avatar'
210 | button :submit
211 | end
212 | ```
213 |
214 | File widgets return a hash with three keys:
215 |
216 | * `data`: The contents of the file as a string
217 | * `content_type`: The type of file, such as `text/plain` or `image/jpeg`
218 | * `name`: The name of the uploaded file
219 |
220 | ## Example
221 |
222 | Here is an example form that uses every built-in widget.
223 |
224 |
225 | ```ruby
226 | result = IRuby.form do
227 | html { h1 'The Everything Form' }
228 | text 'Marvel at the strange and varied inputs!'
229 | date
230 | file
231 | input :username
232 | password
233 | textarea
234 | radio *(1..10)
235 | checkbox 'Fish', 'Cat', 'Dog', label: 'Animals'
236 | select :color, *IRuby::Input::Button::COLORS.keys
237 | cancel
238 | button
239 | end
240 | ```
241 |
242 | ## Writing your own widget
243 |
244 | Most form methods are `IRuby::Input::Widget` instances. A `Widget` is an [`Erector::Widget`](https://github.com/erector/erector) with some additional helpers. Here is the `cancel` widget:
245 |
246 | ```ruby
247 | module IRuby
248 | module Input
249 | class Cancel < Widget
250 | needs :label
251 |
252 | builder :cancel do |label='Cancel'|
253 | add_button Cancel.new(label: label)
254 | end
255 |
256 | def widget_css
257 | ".iruby-cancel { margin-left: 5px; }"
258 | end
259 |
260 | def widget_js
261 | <<-JS
262 | $('.iruby-cancel').click(function(){
263 | $('#iruby-form').remove();
264 | });
265 | JS
266 | end
267 |
268 | def widget_html
269 | button(
270 | @label,
271 | type: 'button',
272 | :'data-dismiss' => 'modal',
273 | class: "btn btn-danger pull-right iruby-cancel"
274 | )
275 | end
276 | end
277 | end
278 | end
279 | ```
280 |
281 | The following methods are available for widgets to use or override:
282 |
283 | | method | description |
284 | | -------- | -------- |
285 | | `widget_js` | Returns the widget's Javascript |
286 | | `widget_css` | Returns the widget's CSS |
287 | | `widget_html` | Returns the widget's |
288 | | `builder(method,&block)` | Class method to add form building helpers. |
289 |
290 | The following methods are available in the `builder` block:
291 |
292 | | method | description |
293 | | -------- | -------- |
294 | | `add_field(field)` | Adds a widget to the form's field area |
295 | | `add_button(button)` | Adds a button to the form's button area |
296 | | `process(key,&block)` | Register a custom processing block for the given key in the results hash |
297 | | `unique_key(key)` | Returns a unique key for the given key. Use this to make sure that there are no key collisions in the final results hash. |
298 |
299 | A canceled form always returns `nil`. Otherwise, the form collects any element with a `data-iruby-key` and non-falsey `data-iruby-value` and passes those to the processor proc registered for the key. See the `File` widget for a more involved example of processing results.
300 |
--------------------------------------------------------------------------------
/lib/iruby/kernel.rb:
--------------------------------------------------------------------------------
1 | module IRuby
2 | ExecutionInfo = Struct.new(:raw_cell, :store_history, :silent)
3 |
4 | class Kernel
5 | RED = "\e[31m"
6 | RESET = "\e[0m"
7 |
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
27 | attr_accessor :instance
28 | end
29 |
30 | # Returns a session object
31 | attr_reader :session
32 |
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)
41 | @config = JSON.parse(File.read(config_file))
42 | IRuby.logger.debug("IRuby kernel start with config #{@config}")
43 | Kernel.instance = self
44 |
45 | @session = Session.new(@config, session_adapter_name)
46 | $stdout = OStream.new(@session, :stdout)
47 | $stderr = OStream.new(@session, :stderr)
48 |
49 | init_parent_process_poller
50 |
51 | @events = EventManager.new(EVENTS)
52 | @execution_count = 0
53 | @backend = PlainBackend.new
54 | @running = true
55 |
56 | self.class.events.trigger(:initialized, self)
57 | end
58 |
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
121 | end
122 |
123 | # @private
124 | def run
125 | send_status :starting
126 | while @running
127 | dispatch
128 | end
129 | end
130 |
131 | # @private
132 | def dispatch
133 | msg = @session.recv(:reply)
134 | IRuby.logger.debug "Kernel#dispatch: msg = #{msg}"
135 | type = msg[:header]['msg_type']
136 | raise "Unknown message type: #{msg.inspect}" unless type =~ /comm_|_request\Z/ && respond_to?(type)
137 | begin
138 | send_status :busy
139 | send(type, msg)
140 | ensure
141 | send_status :idle
142 | end
143 | rescue Exception => e
144 | IRuby.logger.debug "Kernel error: #{e.message}\n#{e.backtrace.join("\n")}"
145 | @session.send(:publish, :error, error_content(e))
146 | end
147 |
148 | # @private
149 | def kernel_info_request(msg)
150 | @session.send(:reply, :kernel_info_reply,
151 | protocol_version: '5.0',
152 | implementation: 'iruby',
153 | implementation_version: IRuby::VERSION,
154 | language_info: {
155 | name: 'ruby',
156 | version: RUBY_VERSION,
157 | mimetype: 'application/x-ruby',
158 | file_extension: '.rb'
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)
168 | end
169 |
170 | # @private
171 | def send_status(status)
172 | IRuby.logger.debug "Send status: #{status}"
173 | @session.send(:publish, :status, execution_state: status)
174 | end
175 |
176 | # @private
177 | def execute_request(msg)
178 | code = msg[:content]['code']
179 | silent = msg[:content]['silent']
180 | # https://jupyter-client.readthedocs.io/en/stable/messaging.html#execute
181 | store_history = silent ? false : msg[:content].fetch('store_history', true)
182 |
183 | @execution_count += 1 if store_history
184 |
185 | unless silent
186 | @session.send(:publish, :execute_input, code: code, execution_count: @execution_count)
187 | end
188 |
189 | events.trigger(:pre_execute)
190 | unless silent
191 | exec_info = ExecutionInfo.new(code, store_history, silent)
192 | events.trigger(:pre_run_cell, exec_info)
193 | end
194 |
195 | content = {
196 | status: :ok,
197 | payload: [],
198 | user_expressions: {},
199 | execution_count: @execution_count
200 | }
201 |
202 | result = nil
203 | begin
204 | result = @backend.eval(code, store_history)
205 | rescue SystemExit
206 | content[:payload] << { source: :ask_exit }
207 | rescue Exception => e
208 | content = error_content(e)
209 | @session.send(:publish, :error, content)
210 | content[:status] = :error
211 | content[:execution_count] = @execution_count
212 | end
213 |
214 | unless result.nil? || silent
215 | @session.send(:publish, :execute_result,
216 | data: Display.display(result),
217 | metadata: {},
218 | execution_count: @execution_count)
219 | end
220 |
221 | events.trigger(:post_execute)
222 | # **{} is for Ruby2.7. Gnuplot#to_hash returns an Array.
223 | events.trigger(:post_run_cell, result, **{}) unless silent
224 |
225 | @session.send(:reply, :execute_reply, content)
226 | end
227 |
228 | # @private
229 | def error_content(e)
230 | rindex = e.backtrace.rindex{|line| line.start_with?(@backend.eval_path)} || -1
231 | backtrace = SyntaxError === e && rindex == -1 ? [] : e.backtrace[0..rindex]
232 | { ename: e.class.to_s,
233 | evalue: e.message,
234 | traceback: ["#{RED}#{e.class}#{RESET}: #{e.message}", *backtrace] }
235 | end
236 |
237 | # @private
238 | def is_complete_request(msg)
239 | # FIXME: the code completeness should be judged by using ripper or other Ruby parser
240 | @session.send(:reply, :is_complete_reply,
241 | status: :unknown)
242 | end
243 |
244 | # @private
245 | def complete_request(msg)
246 | # HACK for #26, only complete last line
247 | code = msg[:content]['code']
248 | if start = code.rindex(/\s|\R/)
249 | code = code[start+1..-1]
250 | start += 1
251 | end
252 | @session.send(:reply, :complete_reply,
253 | matches: @backend.complete(code),
254 | cursor_start: start.to_i,
255 | cursor_end: msg[:content]['cursor_pos'],
256 | metadata: {},
257 | status: :ok)
258 | end
259 |
260 | # @private
261 | def connect_request(msg)
262 | @session.send(:reply, :connect_reply, Hash[%w(shell_port iopub_port stdin_port hb_port).map {|k| [k, @config[k]] }])
263 | end
264 |
265 | # @private
266 | def shutdown_request(msg)
267 | @session.send(:reply, :shutdown_reply, msg[:content])
268 | @running = false
269 | end
270 |
271 | # @private
272 | def history_request(msg)
273 | # we will just send back empty history for now, pending clarification
274 | # as requested in ipython/ipython#3806
275 | @session.send(:reply, :history_reply, history: [])
276 | end
277 |
278 | # @private
279 | def inspect_request(msg)
280 | # not yet implemented. See (#119).
281 | @session.send(:reply, :inspect_reply, status: :ok, found: false, data: {}, metadata: {})
282 | end
283 |
284 | # @private
285 | def comm_open(msg)
286 | comm_id = msg[:content]['comm_id']
287 | target_name = msg[:content]['target_name']
288 | Comm.comm[comm_id] = Comm.target[target_name].new(target_name, comm_id)
289 | end
290 |
291 | # @private
292 | def comm_msg(msg)
293 | Comm.comm[msg[:content]['comm_id']].handle_msg(msg[:content]['data'])
294 | end
295 |
296 | # @private
297 | def comm_close(msg)
298 | comm_id = msg[:content]['comm_id']
299 | Comm.comm[comm_id].handle_close(msg[:content]['data'])
300 | Comm.comm.delete(comm_id)
301 | end
302 |
303 | private
304 |
305 | def init_parent_process_poller
306 | pid = ENV.fetch('JPY_PARENT_PID', 0).to_i
307 | return unless pid > 1
308 |
309 | case RUBY_PLATFORM
310 | when /mswin/, /mingw/
311 | # TODO
312 | else
313 | @parent_poller = start_parent_process_pollar_unix
314 | end
315 | end
316 |
317 | def start_parent_process_pollar_unix
318 | Thread.start do
319 | IRuby.logger.warn("parent process poller thread started.")
320 | loop do
321 | begin
322 | current_ppid = Process.ppid
323 | if current_ppid == 1
324 | IRuby.logger.warn("parent process appears to exited, shutting down.")
325 | exit!(1)
326 | end
327 | sleep 1
328 | rescue Errno::EINTR
329 | # ignored
330 | end
331 | end
332 | end
333 | end
334 | end
335 | end
336 |
--------------------------------------------------------------------------------
/CHANGES.md:
--------------------------------------------------------------------------------
1 | # 0.8.2 (2025-04-10)
2 |
3 | * Update CI to refresh apt packages before installing IRuby gem by @kojix2 in https://github.com/SciRuby/iruby/pull/367
4 | * Fix missing `OStream#closed?` method by @RobinDaugherty in https://github.com/SciRuby/iruby/pull/368
5 | * Use rubygems-requirements-system to install system dependencies automatically by @kou in https://github.com/SciRuby/iruby/pull/369
6 | * Various typo corrections by @kojix2 in https://github.com/SciRuby/iruby/pull/370
7 | * Minor changes to README by @kojix2 in https://github.com/SciRuby/iruby/pull/371
8 |
9 | # 0.8.1 (2025-02-16)
10 |
11 | * Add support for jupyter widgets by @matt-do-it in https://github.com/SciRuby/iruby/pull/350
12 | * Suppress "literal string will be frozen in the future" warning by @tikkss in https://github.com/SciRuby/iruby/pull/353
13 | * Fix warnings in project by @simpl1g in https://github.com/SciRuby/iruby/pull/356
14 | * restore support for IRB <= v1.13.0 by @sealocal in https://github.com/SciRuby/iruby/pull/358
15 | * Restore ruby 2.6 and 2.5 in CI by @sealocal in https://github.com/SciRuby/iruby/pull/359
16 | * Add Ruby 3.4 to CI by @kojix2 in https://github.com/SciRuby/iruby/pull/360
17 | * Fix NoMethodError in backend by @edsinclair in https://github.com/SciRuby/iruby/pull/364
18 |
19 | # 0.8.0 (2024-07-28)
20 |
21 | * Hide output on assignment by @ankane in https://github.com/SciRuby/iruby/pull/312
22 | * Introduce the new Application classes by @mrkn in https://github.com/SciRuby/iruby/pull/317
23 | * Fix Gnuplot issues in Ruby 2.7 (#321) by @kojix2 in https://github.com/SciRuby/iruby/pull/322
24 | * Add Ruby3.1 to CI by @kojix2 in https://github.com/SciRuby/iruby/pull/323
25 | * Update README.md by @marek-witkowski in https://github.com/SciRuby/iruby/pull/324
26 | * ci: upgrade actions/checkout by @kojix2 in https://github.com/SciRuby/iruby/pull/325
27 | * Add Ruby 3.2 to CI for ubuntu by @petergoldstein in https://github.com/SciRuby/iruby/pull/327
28 | * Default to true for `store_history` if not in silent mode by @gartens in https://github.com/SciRuby/iruby/pull/330
29 | * Add Ruby 3.3 to CI for Ubuntu by @kojix2 in https://github.com/SciRuby/iruby/pull/331
30 | * Remove Ruby 2.3 and 2.4 from CI by @kojix2 in https://github.com/SciRuby/iruby/pull/332
31 | * Fix typos by @kojix2 in https://github.com/SciRuby/iruby/pull/335
32 | * Format README.md and ci.yml by @kojix2 in https://github.com/SciRuby/iruby/pull/337
33 | * Fix PlainBackend for irb v1.13.0 by @zalt50 in https://github.com/SciRuby/iruby/pull/339
34 | * Added `date` to header by @ebababi in https://github.com/SciRuby/iruby/pull/342
35 | * Update CI Configuration for IRuby by @kojix2 in https://github.com/SciRuby/iruby/pull/344
36 | * Add logger and Remove base64 to Fix CI Tests by @kojix2 in https://github.com/SciRuby/iruby/pull/345
37 | * Update CI trigger configuration by @kojix2 in https://github.com/SciRuby/iruby/pull/346
38 |
39 | # 0.7.4 (2021-08-18)
40 |
41 | ## Enhancements
42 |
43 | * Install zeromq library automatically https://github.com/SciRuby/iruby/pull/307, https://github.com/SciRuby/iruby/pull/308 (@mrkn, @kou)
44 | * Remove pyzmq session adapter (@mrkn)
45 | * Make cztop session adapter deprecated (@mrkn)
46 |
47 | # 0.7.3 (2021-07-08)
48 |
49 | ## Bug Fixes
50 |
51 | * Do not call default renderers when to_iruby_mimebundle is available (@mrkn)
52 |
53 | # 0.7.2 (2021-06-23)
54 |
55 | ## Bug Fixes
56 |
57 | * Fix IRuby.table for Ruby >= 2.7 https://github.com/SciRuby/iruby/pull/305 (@topofocus)
58 | * Fix PlainBackend to include modules https://github.com/SciRuby/iruby/issues/303 (@UliKuch, @mrkn)
59 |
60 | # 0.7.1 (2021-06-21)
61 |
62 | ## Enhancements
63 |
64 | * Add support of `to_iruby_mimebundle` format method https://github.com/SciRuby/iruby/pull/304 (@mrkn)
65 |
66 | ## Bug Fixes
67 |
68 | * Prevent unintentional display the result of Session#send (@mrkn)
69 | * Update display formatter for Gruff::Base to prevent warning (@mrkn)
70 |
71 | # 0.7.0 (2021-05-28)
72 |
73 | ## Enhancements
74 |
75 | * The default backend is changed to IRB (@mrkn)
76 | * Add IRuby::Kernel#switch_backend! method (@mrkn)
77 |
78 | ## Bug Fixes
79 |
80 | * Fix the handling of image/svg+xml https://github.com/SciRuby/iruby/pull/300, https://github.com/SciRuby/iruby/pull/301 (@kojix2)
81 |
82 | # 0.6.1 (2021-05-26)
83 |
84 | ## Bug Fixes
85 |
86 | * Follow the messages and hooks orders during execute_request processing (@mrkn)
87 |
88 | # 0.6.0 (2021-05-25)
89 |
90 | ## Bug Fixes
91 |
92 | * Fix the handling of application/javascript https://github.com/SciRuby/iruby/issues/292, https://github.com/SciRuby/iruby/pull/294 (@kylekyle, @mrkn)
93 |
94 | ## Enhancements
95 |
96 | * Add the `initialized` event in `IRuby::Kernel` class https://github.com/SciRuby/iruby/pull/168, https://github.com/SciRuby/iruby/pull/296 (@Yuki-Inoue, @mrkn)
97 | * Add the following four events https://github.com/SciRuby/iruby/pull/295 (@mrkn):
98 | * `pre-execute` -- occurs before every code execution
99 | * `pre-run-cell` -- occurs before every non-silent code execution
100 | * `post-execute` -- occurs after every code execution
101 | * `post-run-cell` -- occurs after every non-silent code execution
102 | * Replace Bond with IRB in PlainBackend https://github.com/SciRuby/iruby/pull/276, https://github.com/SciRuby/iruby/pull/297 (@cfis, @mrkn)
103 |
104 | # 0.5.0 (2021-03-25)
105 |
106 | ## Bug Fixes
107 |
108 | * Fix Jupyter console crashes issue https://github.com/SciRuby/iruby/pull/210 (@kojix2)
109 | * Fix syntax highlighting issue on Jpyter Lab https://github.com/SciRuby/iruby/issues/224 (@kojix2)
110 | * Fix interoperability issue with ruby-git https://github.com/SciRuby/iruby/pull/139 (@habemus-papadum)
111 | * Fix the issue of `$stderr.write` that cannot handle multiple arguments https://github.com/SciRuby/iruby/issues/206 (@kojix2)
112 | * Remove a buggy `inspect_request` implementation https://github.com/SciRuby/iruby/pull/119 (@LunarLanding)
113 | * Fix uninitialized constant `Fiddle` caused in initialization phase https://github.com/SciRuby/iruby/issues/264 (@MatthewSteen, @kjoix2)
114 | * Fix the issue on displaying a table https://github.com/SciRuby/iruby/pull/281 (@ankane)
115 |
116 | ## Enhancements
117 |
118 | * Add `IRuby.clear_output` method https://github.com/SciRuby/iruby/pull/220 (@kojix2)
119 | * Make backtrace on exception simplify and more appropriate for code in a cell https://github.com/SciRuby/iruby/pull/249 (@zheng-yongping)
120 | * Make syntax error message more appropriate https://github.com/SciRuby/iruby/pull/251 (@zheng-yongping)
121 | * Remove top-level `In` and `Out` constants https://github.com/SciRuby/iruby/pull/229 (@kojix2)
122 | * Use text/plain for the default format of `Numo::NArray` objects https://github.com/SciRuby/iruby/pull/255 (@kojix2)
123 | * Use ffi-rzmq as the default ZeroMQ adapter https://github.com/SciRuby/iruby/pull/256 (@kojix2)
124 | * Drop rbczmq support https://github.com/SciRuby/iruby/pull/260 (@rstammer)
125 | * Add ruby-vips image support https://github.com/SciRuby/iruby/pull/279 (@ankane)
126 | * Replace mimemagic with mime-types https://github.com/SciRuby/iruby/pull/291 (@mrkn)
127 |
128 | # 0.4.0 (2019-07-31)
129 |
130 | (TBD)
131 |
132 | # 0.3 (2017-03-26)
133 |
134 | ## Bug Fixes
135 |
136 | * Disable Jupyter keyboard manager for all popups made using IRuby.popup (@kylekyle).
137 | * Fix Iruby/Input date values bug that set date fields to whatever the last date value was (@kylekyle).
138 | * Fix a bug where time strings put into prompter would give an 'out of range' error (@kylekyle).
139 |
140 | ## Enhancements
141 |
142 | * Improvements to IRuby dependency detection using `Bundler::Dependencies#specs` (@kou).
143 | * Use less memory forcing pry to store only the last 3 commands in memory (@kylekyle).
144 | * Use bigger z-index that is used across all browsers (@kylekyle).
145 | * Ability to input date values as DateTime objects in IRuby/Input (@kylekyle).
146 | * Add option to have check boxes checked by default (@kylekyle).
147 | * Option for multi-select in drop down menus in the prompter (@kylekyle).
148 | * Add support for multiple widgets using `IRuby::Input::Multiple` (@kylekyle).
149 | * Calender icon for date selector icon (@kylekyle).
150 | * Add support for Numo/NArray (@zalt50).
151 | * Text now only completes after a space (@zalt50).
152 | * Remove the DONTWAIT flag when receiving a message (@cloud-oak).
153 | * Add support for CZTop (@kou).
154 |
155 | # 0.2.9 (2016-05-02)
156 |
157 | ## Bug Fixes
158 |
159 | * Fix an error where a NoMethodError was being raised where a table rendered using an Array of Hashes has more than `maxcols` columns. (@CGamesPlay)
160 | * Patch PryBackend to throw unterminated string and unexpected end-of-file syntax errors (@kylekyle)
161 |
162 | ## Enhnacements
163 |
164 | * Add an IRuby::Input class which provides widgets for getting inputs from users. (@kylekyle)
165 | * Add data_uri dependency (@kylekyle)
166 | * Added a clear_output display function (@mrkn)
167 | * Doc fixes for installation (@kozo2, @generall)
168 |
169 | # 0.2.8 (2015-12-06)
170 |
171 | * Add compatibility with ffi-rzmq
172 | * Windows support
173 |
174 | # 0.2.7 (2015-07-02)
175 |
176 | * Fix problem with autoloaded constants in Display, problem with sciruby gem
177 |
178 | # 0.2.6 (2015-06-21)
179 |
180 | * Check registered kernel and Gemfile to prevent gem loading problems
181 | * Support to_tex method for the rendering
182 |
183 | # 0.2.5 (2015-06-07)
184 |
185 | * Fix #29, empty signatures
186 | * Move iruby utils to IRuby::Utils module
187 | * Add IRuby.tex alias for IRuby.latex
188 | * Remove example notebooks from gem
189 |
190 | # 0.2.4 (2015-06-02)
191 |
192 | * Better exception handling
193 | * Fix ctrl-C issue #17
194 | * Fix timeout issue #19
195 |
196 | # 0.2.3 (2015-05-31)
197 |
198 | * Fix notebook indentation
199 | * Fix tab completion for multiple lines
200 |
201 | # 0.2.2 (2015-05-26)
202 |
203 | * Support history variables In, Out, _, _i etc
204 | * Internal refactoring and minor bugfixes
205 |
206 | # 0.2.1 (2015-05-26)
207 |
208 | * Copy Ruby logo to kernel specification
209 |
210 | # 0.2.0 (2015-05-25)
211 |
212 | * Dropped IPython2 support
213 | * Dropped Ruby < 2.0.0 support
214 | * Supports and requires now IPython3/Jupyter
215 | * Switch from ffi-rzmq to rbczmq
216 | * Added IRuby::Conn (experimental, to be used by widgets)
217 | * iruby register/unregister commands to register IRuby kernel in Jupyter
218 |
219 | # 0.1.13 (2014-08-19)
220 |
221 | * Improved IRuby.table, supports :maxrows and :maxcols
222 | * IRuby#javascript workaround (https://github.com/ipython/ipython/issues/6259)
223 |
224 | # 0.1.12 (2014-08-01)
225 |
226 | * IRuby#table add option maxrows
227 | * powerful display system with format and datatype registry, see #25
228 | * Add IRuby#javascript
229 | * Add IRuby#svg
230 |
231 | # 0.1.11 (2014-07-08)
232 |
233 | * Push binding if pry binding stack is empty
234 |
235 | # 0.1.10 (2014-07-08)
236 |
237 | * Fix #19 (pp)
238 | * Handle exception when symlink cannot be created
239 | * Fix dependencies and Pry backend
240 |
241 | # 0.1.9 (2014-02-28)
242 |
243 | * Check IPython version
244 |
245 | # 0.1.7/0.1.8
246 |
247 | * Bugfixes #11, #12, #13
248 |
249 | # 0.1.6 (2013-10-11)
250 |
251 | * Print Matrix and GSL::Matrix as LaTeX
252 | * Add check for Pry version
253 |
254 | # 0.1.5 (2013-10-03)
255 |
256 | * Implement a rich display system
257 | * Fix error output
258 |
259 | # 0.1.4 (2013-10-03)
260 |
261 | * Extract display handler from kernel
262 | * Always return a text/plain response
263 |
264 | # 0.1.3 (2013-10-03)
265 |
266 | * Implement missing request handlers
267 | * Detect if Bundler is running and set kernel_cmd appropriately
268 | * Improve Pry integration
269 | * Add support for the gems gnuplot, gruff, rmagick and mini_magick
270 |
271 | # 0.1.2 (2013-10-02)
272 |
273 | * Support for Pry added
274 | * Launch `iruby console` if plain `iruby` is started
275 |
276 | # 0.1.0 (2013-10-02)
277 |
278 | * Cleanup and rewrite of some parts
279 |
--------------------------------------------------------------------------------
/lib/iruby/application.rb:
--------------------------------------------------------------------------------
1 | require "fileutils"
2 | require "json"
3 | require "optparse"
4 | require "rbconfig"
5 | require "singleton"
6 |
7 | require_relative "error"
8 | require_relative "kernel_app"
9 |
10 | module IRuby
11 | class Application
12 | include Singleton
13 |
14 | # Set the application instance up.
15 | def setup(argv=nil)
16 | @iruby_executable = File.expand_path($PROGRAM_NAME)
17 | parse_command_line(argv)
18 | end
19 |
20 | # Parse the command line arguments
21 | #
22 | # @param argv [Array, nil] The array of arguments.
23 | private def parse_command_line(argv)
24 | argv = ARGV.dup if argv.nil?
25 | @argv = argv # save the original
26 |
27 | case argv[0]
28 | when "help"
29 | # turn `iruby help notebook` into `iruby notebook -h`
30 | argv = [*argv[1..-1], "-h"]
31 | when "version"
32 | # turn `iruby version` into `iruby -v`
33 | argv = ["-v", *argv[1..-1]]
34 | else
35 | argv = argv.dup # prevent to break @argv
36 | end
37 |
38 | opts = OptionParser.new
39 | opts.program_name = "IRuby"
40 | opts.version = ::IRuby::VERSION
41 | opts.banner = "Usage: #{$PROGRAM_NAME} [options] [subcommand] [options]"
42 |
43 | opts.on_tail("-h", "--help") do
44 | print_help(opts)
45 | exit
46 | end
47 |
48 | opts.on_tail("-v", "--version") do
49 | puts opts.ver
50 | exit
51 | end
52 |
53 | opts.order!(argv)
54 |
55 | if argv.length == 0 || argv[0].start_with?("-")
56 | # If no subcommand is given, we use the console
57 | argv = ["console", *argv]
58 | end
59 |
60 | begin
61 | parse_sub_command(argv) if argv.length > 0
62 | rescue InvalidSubcommandError => err
63 | $stderr.puts err.message
64 | print_help(opts, $stderr)
65 | abort
66 | end
67 | end
68 |
69 | SUB_COMMANDS = {
70 | "register" => "Register IRuby kernel.",
71 | "unregister" => "Unregister the existing IRuby kernel.",
72 | "kernel" => "Launch IRuby kernel",
73 | "console" => "Launch jupyter console with IRuby kernel"
74 | }.freeze.each_value(&:freeze)
75 |
76 | private_constant :SUB_COMMANDS
77 |
78 | private def parse_sub_command(argv)
79 | sub_cmd, *sub_argv = argv
80 | case sub_cmd
81 | when *SUB_COMMANDS.keys
82 | @sub_cmd = sub_cmd.to_sym
83 | @sub_argv = sub_argv
84 | else
85 | raise InvalidSubcommandError.new(sub_cmd, sub_argv)
86 | end
87 | end
88 |
89 | private def print_help(opts, out=$stdout)
90 | out.puts opts.help
91 | out.puts
92 | out.puts "Subcommands"
93 | out.puts "==========="
94 | SUB_COMMANDS.each do |name, description|
95 | out.puts "#{name}"
96 | out.puts " #{description}"
97 | end
98 | end
99 |
100 | def run
101 | case @sub_cmd
102 | when :register
103 | register_kernel(@sub_argv)
104 | when :unregister
105 | unregister_kernel(@sub_argv)
106 | when :console
107 | exec_jupyter(@sub_cmd.to_s, @sub_argv)
108 | when :kernel
109 | @sub_app = KernelApplication.new(@sub_argv)
110 | @sub_app.run
111 | else
112 | raise "[IRuby][BUG] Unknown subcommand: #{@sub_cmd}; this must be treated in parse_command_line."
113 | end
114 | end
115 |
116 | ruby_version_info = RUBY_VERSION.split('.')
117 | DEFAULT_KERNEL_NAME = "ruby#{ruby_version_info[0]}".freeze
118 | DEFAULT_DISPLAY_NAME = "Ruby #{ruby_version_info[0]} (iruby kernel)"
119 |
120 | RegisterParams = Struct.new(
121 | :name,
122 | :display_name,
123 | :profile,
124 | :env,
125 | :user,
126 | :prefix,
127 | :sys_prefix,
128 | :force,
129 | :ipython_dir
130 | ) do
131 | def initialize(*args)
132 | super
133 | self.name ||= DEFAULT_KERNEL_NAME
134 | self.force = false
135 | self.user = true
136 | end
137 | end
138 |
139 | def register_kernel(argv)
140 | params = parse_register_command_line(argv)
141 |
142 | if params.name != DEFAULT_KERNEL_NAME
143 | # `--name` is specified and `--display-name` is not
144 | # default `params.display_name` to `params.name`
145 | params.display_name ||= params.name
146 | end
147 |
148 | check_and_warn_kernel_in_default_ipython_directory(params)
149 |
150 | if installed_kernel_exist?(params.name, params.ipython_dir)
151 | unless params.force
152 | $stderr.puts "IRuby kernel named `#{params.name}` already exists!"
153 | $stderr.puts "Use --force to force register the new kernel."
154 | exit 1
155 | end
156 | end
157 |
158 | Dir.mktmpdir("iruby_kernel") do |tmpdir|
159 | path = File.join(tmpdir, DEFAULT_KERNEL_NAME)
160 | FileUtils.mkdir_p(path)
161 |
162 | # Stage assets
163 | assets_dir = File.expand_path("../assets", __FILE__)
164 | FileUtils.cp_r(Dir.glob(File.join(assets_dir, "*")), path)
165 |
166 | kernel_dict = {
167 | "argv" => make_iruby_cmd(),
168 | "display_name" => params.display_name || DEFAULT_DISPLAY_NAME,
169 | "language" => "ruby",
170 | "metadata" => {"debugger": false}
171 | }
172 |
173 | # TODO: Support params.profile
174 | # TODO: Support params.env
175 |
176 | kernel_content = JSON.pretty_generate(kernel_dict)
177 | File.write(File.join(path, "kernel.json"), kernel_content)
178 |
179 | args = ["--name=#{params.name}"]
180 | args << "--user" if params.user
181 | args << path
182 |
183 | # TODO: Support params.prefix
184 | # TODO: Support params.sys_prefix
185 |
186 | system("jupyter", "kernelspec", "install", *args)
187 | end
188 | end
189 |
190 | # Warn the existence of the IRuby kernel in the default IPython's kernels directory
191 | private def check_and_warn_kernel_in_default_ipython_directory(params)
192 | default_ipython_kernels_dir = File.expand_path("~/.ipython/kernels")
193 | [params.name, "ruby"].each do |name|
194 | if File.exist?(File.join(default_ipython_kernels_dir, name, "kernel.json"))
195 | warn "IRuby kernel `#{name}` already exists in the deprecated IPython's data directory."
196 | end
197 | end
198 | end
199 |
200 | alias __system__ system
201 |
202 | private def system(*cmdline, dry_run: false)
203 | $stderr.puts "EXECUTE: #{cmdline.map {|x| x.include?(' ') ? x.inspect : x}.join(' ')}"
204 | __system__(*cmdline) unless dry_run
205 | end
206 |
207 | private def installed_kernel_exist?(name, ipython_dir)
208 | kernels_dir = resolve_kernelspec_dir(ipython_dir)
209 | kernel_dir = File.join(kernels_dir, name)
210 | File.file?(File.join(kernel_dir, "kernel.json"))
211 | end
212 |
213 | private def resolve_kernelspec_dir(ipython_dir)
214 | if ENV.has_key?("JUPYTER_DATA_DIR")
215 | if ENV.has_key?("IPYTHONDIR")
216 | warn "both JUPYTER_DATA_DIR and IPYTHONDIR are supplied; IPYTHONDIR is ignored."
217 | end
218 | jupyter_data_dir = ENV["JUPYTER_DATA_DIR"]
219 | return File.join(jupyter_data_dir, "kernels")
220 | end
221 |
222 | if ipython_dir.nil? && ENV.key?("IPYTHONDIR")
223 | warn 'IPYTHONDIR is deprecated. Use JUPYTER_DATA_DIR instead.'
224 | ipython_dir = ENV["IPYTHONDIR"]
225 | end
226 |
227 | if ipython_dir
228 | File.join(ipython_dir, 'kernels')
229 | else
230 | Jupyter.kernelspec_dir
231 | end
232 | end
233 |
234 | private def make_iruby_cmd(executable: nil, extra_arguments: nil)
235 | executable ||= default_executable
236 | extra_arguments ||= []
237 | [*Array(executable), "kernel", "-f", "{connection_file}", *extra_arguments]
238 | end
239 |
240 | private def default_executable
241 | [RbConfig.ruby, @iruby_executable]
242 | end
243 |
244 | private def parse_register_command_line(argv)
245 | opts = OptionParser.new
246 | opts.banner = "Usage: #{$PROGRAM_NAME} register [options]"
247 |
248 | params = RegisterParams.new
249 |
250 | opts.on(
251 | "--force",
252 | "Force register a new kernel spec. The existing kernel spec will be removed."
253 | ) { params.force = true }
254 |
255 | opts.on(
256 | "--user",
257 | "Register for the current user instead of system-wide."
258 | ) { params.user = true }
259 |
260 | opts.on(
261 | "--name=VALUE", String,
262 | "Specify a name for the kernelspec. This is needed to have multiple IRuby kernels at the same time."
263 | ) {|v| params.name = v }
264 |
265 | opts.on(
266 | "--display-name=VALUE", String,
267 | "Specify the display name for the kernelspec. This is helpful when you have multiple IRuby kernels."
268 | ) {|v| params.display_name = v }
269 |
270 | # TODO: --profile
271 | # TODO: --prefix
272 | # TODO: --sys-prefix
273 | # TODO: --env
274 |
275 | define_ipython_dir_option(opts, params)
276 |
277 | opts.order!(argv)
278 |
279 | params
280 | end
281 |
282 | UnregisterParams = Struct.new(
283 | :names,
284 | #:profile,
285 | #:user,
286 | #:prefix,
287 | #:sys_prefix,
288 | :ipython_dir,
289 | :force,
290 | :yes
291 | ) do
292 | def initialize(*args, **kw)
293 | super
294 | self.names = []
295 | # self.user = true
296 | self.force = false
297 | self.yes = false
298 | end
299 | end
300 |
301 | def unregister_kernel(argv)
302 | params = parse_unregister_command_line(argv)
303 | opts = []
304 | opts << "-y" if params.yes
305 | opts << "-f" if params.force
306 | system("jupyter", "kernelspec", "uninstall", *opts, *params.names)
307 | end
308 |
309 | private def parse_unregister_command_line(argv)
310 | opts = OptionParser.new
311 | opts.banner = "Usage: #{$PROGRAM_NAME} unregister [options] NAME [NAME ...]"
312 |
313 | params = UnregisterParams.new
314 |
315 | opts.on(
316 | "-f", "--force",
317 | "Force removal, don't prompt for confirmation."
318 | ) { params.force = true}
319 |
320 | opts.on(
321 | "-y", "--yes",
322 | "Answer yes to any prompts."
323 | ) { params.yes = true }
324 |
325 | # TODO: --user
326 | # TODO: --profile
327 | # TODO: --prefix
328 | # TODO: --sys-prefix
329 |
330 | define_ipython_dir_option(opts, params)
331 |
332 | opts.order!(argv)
333 |
334 | params.names = argv.dup
335 |
336 | params
337 | end
338 |
339 | def exec_jupyter(sub_cmd, argv)
340 | opts = OptionParser.new
341 | opts.banner = "Usage: #{$PROGRAM_NAME} unregister [options]"
342 |
343 | kernel_name = resolve_installed_kernel_name(DEFAULT_KERNEL_NAME)
344 | opts.on(
345 | "--kernel=NAME", String,
346 | "The name of the default kernel to start."
347 | ) {|v| kernel_name = v }
348 |
349 | opts.order!(argv)
350 |
351 | opts = ["--kernel=#{kernel_name}"]
352 | exec("jupyter", "console", *opts)
353 | end
354 |
355 | private def resolve_installed_kernel_name(default_name)
356 | kernels = IO.popen(["jupyter", "kernelspec", "list", "--json"], "r", err: File::NULL) do |jupyter_out|
357 | JSON.load(jupyter_out.read)
358 | end
359 | unless kernels["kernelspecs"].key?(default_name)
360 | return "ruby" if kernels["kernelspecs"].key?("ruby")
361 | end
362 | default_name
363 | end
364 |
365 | private def define_ipython_dir_option(opts, params)
366 | opts.on(
367 | "--ipython-dir=DIR", String,
368 | "Specify the IPython's data directory (DEPRECATED)."
369 | ) do |v|
370 | if ENV.key?("JUPYTER_DATA_DIR")
371 | warn 'Both JUPYTER_DATA_DIR and --ipython-dir are supplied; --ipython-dir is ignored.'
372 | else
373 | warn '--ipython-dir is deprecated. Use JUPYTER_DATA_DIR environment variable instead.'
374 | end
375 |
376 | params.ipython_dir = v
377 | end
378 | end
379 | end
380 | end
381 |
--------------------------------------------------------------------------------
/lib/iruby/display.rb:
--------------------------------------------------------------------------------
1 | require "set"
2 |
3 | module IRuby
4 | module Display
5 | DEFAULT_MIME_TYPE_FORMAT_METHODS = {
6 | "text/html" => :to_html,
7 | "text/markdown" => :to_markdown,
8 | "image/svg+xml" => :to_svg,
9 | "image/png" => :to_png,
10 | "application/pdf" => :to_pdf,
11 | "image/jpeg" => :to_jpeg,
12 | "text/latex" => [:to_latex, :to_tex],
13 | # NOTE: Do not include the entry of "application/json" because
14 | # all objects can respond to `to_json` due to json library
15 | # "application/json" => :to_json,
16 | "application/javascript" => :to_javascript,
17 | nil => :to_iruby,
18 | "text/plain" => :inspect
19 | }.freeze
20 |
21 | class << self
22 | # @private
23 | def convert(obj, options)
24 | Representation.new(obj, options)
25 | end
26 |
27 | # @private
28 | def display(obj, options = {})
29 | obj = convert(obj, options)
30 | options = obj.options
31 | obj = obj.object
32 |
33 | fuzzy_mime = options[:format] # Treated like a fuzzy mime type
34 | unless !fuzzy_mime || String === fuzzy_mime
35 | raise 'Invalid argument :format'
36 | end
37 |
38 | if exact_mime = options[:mime]
39 | raise 'Invalid argument :mime' unless String === exact_mime
40 | raise 'Invalid mime type' unless exact_mime.include?('/')
41 | end
42 |
43 | data = if obj.respond_to?(:to_iruby_mimebundle)
44 | render_mimebundle(obj, exact_mime, fuzzy_mime)
45 | else
46 | {}
47 | end
48 |
49 | # Render by additional formatters
50 | render_by_registry(data, obj, exact_mime, fuzzy_mime)
51 |
52 | # Render by to_xxx methods
53 | default_renderers = if obj.respond_to?(:to_iruby_mimebundle)
54 | # Do not use Hash#slice for Ruby < 2.5
55 | {"text/plain" => DEFAULT_MIME_TYPE_FORMAT_METHODS["text/plain"]}
56 | else
57 | DEFAULT_MIME_TYPE_FORMAT_METHODS
58 | end
59 | default_renderers.each do |mime, methods|
60 | next if mime.nil? && !data.empty? # for to_iruby
61 |
62 | next if mime && data.key?(mime) # do not overwrite
63 |
64 | method = Array(methods).find {|m| obj.respond_to?(m) }
65 | next if method.nil?
66 |
67 | result = obj.send(method)
68 | case mime
69 | when nil # to_iruby
70 | case result
71 | when nil
72 | # do nothing
73 | next
74 | when Array
75 | mime, result = result
76 | else
77 | warn(("Ignore the result of to_iruby method of %p because " +
78 | "it does not return a pair of mime-type and formatted representation") % obj)
79 | next
80 | end
81 | end
82 | data[mime] = result
83 | end
84 |
85 | # As a last resort, interpret string representation of the object
86 | # as the given mime type.
87 | if exact_mime && !data.key?(exact_mime)
88 | data[exact_mime] = protect(exact_mime, obj)
89 | end
90 |
91 | data
92 | end
93 |
94 | # @private
95 | def clear_output(wait = false)
96 | IRuby::Kernel.instance.session.send(:publish, :clear_output, wait: wait)
97 | end
98 |
99 | private
100 |
101 | def protect(mime, data)
102 | ascii?(mime) ? data.to_s : [data.to_s].pack('m0')
103 | end
104 |
105 | # Each of the following mime types must be a text type,
106 | # but mime-types library tells us it is a non-text type.
107 | FORCE_TEXT_TYPES = Set[
108 | "application/javascript",
109 | "image/svg+xml"
110 | ].freeze
111 |
112 | def ascii?(mime)
113 | if FORCE_TEXT_TYPES.include?(mime)
114 | true
115 | else
116 | MIME::Type.new("content-type" => mime).ascii?
117 | end
118 | end
119 |
120 | private def render_mimebundle(obj, exact_mime, fuzzy_mime)
121 | data = {}
122 | include_mime = [exact_mime].compact
123 | formats, _metadata = obj.to_iruby_mimebundle(include: include_mime)
124 | formats.each do |mime, value|
125 | if fuzzy_mime.nil? || mime.include?(fuzzy_mime)
126 | data[mime] = value
127 | end
128 | end
129 | data
130 | end
131 |
132 | private def render_by_registry(data, obj, exact_mime, fuzzy_mime)
133 | # Filter matching renderer by object type
134 | renderer = Registry.renderer.select { |r| r.match?(obj) }
135 |
136 | matching_renderer = nil
137 |
138 | # Find exactly matching display by exact_mime
139 | if exact_mime
140 | matching_renderer = renderer.find { |r| exact_mime == r.mime }
141 | end
142 |
143 | # Find fuzzy matching display by fuzzy_mime
144 | if fuzzy_mime
145 | matching_renderer ||= renderer.find { |r| r.mime&.include?(fuzzy_mime) }
146 | end
147 |
148 | renderer.unshift matching_renderer if matching_renderer
149 |
150 | # Return first render result which has the right mime type
151 | renderer.each do |r|
152 | mime, result = r.render(obj)
153 | next if data.key?(mime)
154 |
155 | if mime && result && (!exact_mime || exact_mime == mime) && (!fuzzy_mime || mime.include?(fuzzy_mime))
156 | data[mime] = protect(mime, result)
157 | break
158 | end
159 | end
160 |
161 | nil
162 | end
163 | end
164 |
165 | private def render_by_to_iruby(data, obj)
166 | if obj.respond_to?(:to_iruby)
167 | result = obj.to_iruby
168 | mime, rep = case result
169 | when Array
170 | result
171 | else
172 | [nil, result]
173 | end
174 | data[mime] = rep
175 | end
176 | end
177 |
178 | class Representation
179 | attr_reader :object, :options
180 |
181 | def initialize(object, options)
182 | @object = object
183 | @options = options
184 | end
185 |
186 | class << self
187 | alias old_new new
188 |
189 | def new(obj, options)
190 | options = { format: options } if String === options
191 | if Representation === obj
192 | options = obj.options.merge(options)
193 | obj = obj.object
194 | end
195 | old_new(obj, options)
196 | end
197 | end
198 | end
199 |
200 | class FormatMatcher
201 | def initialize(&block)
202 | @block = block
203 | end
204 |
205 | def call(obj)
206 | @block.(obj)
207 | end
208 |
209 | def inspect
210 | "#{self.class.name}[%p]" % @block
211 | end
212 | end
213 |
214 | class RespondToFormatMatcher < FormatMatcher
215 | def initialize(name)
216 | super() {|obj| obj.respond_to?(name) }
217 | @name = name
218 | end
219 |
220 | attr_reader :name
221 |
222 | def inspect
223 | "#{self.class.name}[respond_to?(%p)]" % name
224 | end
225 | end
226 |
227 | class TypeFormatMatcher < FormatMatcher
228 | def initialize(class_block)
229 | super() do |obj|
230 | begin
231 | self.klass === obj
232 | # We have to rescue all exceptions since constant autoloading could fail with a different error
233 | rescue Exception
234 | false
235 | end
236 | end
237 | @class_block = class_block
238 | end
239 |
240 | def klass
241 | @class_block.()
242 | end
243 |
244 | def inspect
245 | klass = begin
246 | @class_block.()
247 | rescue Exception
248 | @class_block
249 | end
250 | "#{self.class.name}[%p]" % klass
251 | end
252 | end
253 |
254 | class Renderer
255 | attr_reader :match, :mime, :priority
256 |
257 | def initialize(match, mime, render, priority)
258 | @match = match
259 | @mime = mime
260 | @render = render
261 | @priority = priority
262 | end
263 |
264 | def match?(obj)
265 | @match.call(obj)
266 | end
267 |
268 | def render(obj)
269 | result = @render.call(obj)
270 | Array === result ? result : [@mime, result]
271 | end
272 | end
273 |
274 | module Registry
275 | extend self
276 |
277 | def renderer
278 | @renderer ||= []
279 | end
280 |
281 | def match(&block)
282 | @match = FormatMatcher.new(&block)
283 | priority 0
284 | nil
285 | end
286 |
287 | def respond_to(name)
288 | @match = RespondToFormatMatcher.new(name)
289 | priority 0
290 | nil
291 | end
292 |
293 | def type(&block)
294 | @match = TypeFormatMatcher.new(block)
295 | priority 0
296 | nil
297 | end
298 |
299 | def priority(p)
300 | @priority = p
301 | nil
302 | end
303 |
304 | def format(mime = nil, &block)
305 | renderer << Renderer.new(@match, mime, block, @priority)
306 | renderer.sort_by! { |r| -r.priority }
307 |
308 | # Decrease priority implicitly for all formats
309 | # which are added later for a type.
310 | # Overwrite with the `priority` method!
311 | @priority -= 1
312 | nil
313 | end
314 |
315 | type { NMatrix }
316 | format 'text/latex' do |obj|
317 | obj.dim == 2 ?
318 | LaTeX.matrix(obj, obj.shape[0], obj.shape[1]) :
319 | LaTeX.vector(obj.to_a)
320 | end
321 |
322 | type { Numo::NArray }
323 | format 'text/plain', &:inspect
324 | format 'text/latex' do |obj|
325 | obj.ndim == 2 ?
326 | LaTeX.matrix(obj, obj.shape[0], obj.shape[1]) :
327 | LaTeX.vector(obj.to_a)
328 | end
329 | format 'text/html' do |obj|
330 | HTML.table(obj.to_a)
331 | end
332 |
333 | type { NArray }
334 | format 'text/plain', &:inspect
335 | format 'text/latex' do |obj|
336 | obj.dim == 2 ?
337 | LaTeX.matrix(obj.transpose(1, 0), obj.shape[1], obj.shape[0]) :
338 | LaTeX.vector(obj.to_a)
339 | end
340 | format 'text/html' do |obj|
341 | HTML.table(obj.to_a)
342 | end
343 |
344 | type { Matrix }
345 | format 'text/latex' do |obj|
346 | LaTeX.matrix(obj, obj.row_size, obj.column_size)
347 | end
348 | format 'text/html' do |obj|
349 | HTML.table(obj.to_a)
350 | end
351 |
352 | type { GSL::Matrix }
353 | format 'text/latex' do |obj|
354 | LaTeX.matrix(obj, obj.size1, obj.size2)
355 | end
356 | format 'text/html' do |obj|
357 | HTML.table(obj.to_a)
358 | end
359 |
360 | type { GSL::Vector }
361 | format 'text/latex' do |obj|
362 | LaTeX.vector(obj.to_a)
363 | end
364 | format 'text/html' do |obj|
365 | HTML.table(obj.to_a)
366 | end
367 |
368 | type { GSL::Complex }
369 | format 'text/latex' do |obj|
370 | "$#{obj.re}+#{obj.im}\\imath$"
371 | end
372 |
373 | type { Complex }
374 | format 'text/latex' do |obj|
375 | "$#{obj.real}+#{obj.imag}\\imath$"
376 | end
377 |
378 | type { Gnuplot::Plot }
379 | format 'image/svg+xml' do |obj|
380 | Tempfile.open('plot') do |f|
381 | terminal = obj['terminal'].to_s.split(' ')
382 | terminal[0] = 'svg'
383 | terminal << 'enhanced' unless terminal.include?('noenhanced')
384 | obj.terminal terminal.join(' ')
385 | obj.output f.path
386 | Gnuplot.open do |io|
387 | io << obj.to_gplot
388 | io << obj.store_datasets
389 | end
390 | File.read(f.path)
391 | end
392 | end
393 |
394 | format_magick_image = ->(obj) do
395 | format = obj.format || 'PNG'
396 | [
397 | format == 'PNG' ? 'image/png' : 'image/jpeg',
398 | obj.to_blob {|i| i.format = format }
399 | ]
400 | end
401 |
402 | match do |obj|
403 | defined?(Magick::Image) && Magick::Image === obj ||
404 | defined?(MiniMagick::Image) && MiniMagick::Image === obj
405 | end
406 | format 'image', &format_magick_image
407 |
408 | type { Gruff::Base }
409 | format 'image' do |obj|
410 | format_magick_image.(obj.to_image)
411 | end
412 |
413 | match do |obj|
414 | defined?(Vips::Image) && Vips::Image === obj
415 | end
416 | format do |obj|
417 | # handles Vips::Error, vips_image_get: field "vips-loader" not found
418 | loader = obj.get('vips-loader') rescue nil
419 | if loader == 'jpegload'
420 | ['image/jpeg', obj.write_to_buffer('.jpg')]
421 | else
422 | # falls back to png for other/unknown types
423 | ['image/png', obj.write_to_buffer('.png')]
424 | end
425 | end
426 |
427 | type { Rubyvis::Mark }
428 | format 'image/svg+xml' do |obj|
429 | obj.render
430 | obj.to_svg
431 | end
432 |
433 | match { |obj| obj.respond_to?(:path) && obj.method(:path).arity == 0 && File.readable?(obj.path) }
434 | format do |obj|
435 | mime = MIME::Types.of(obj.path).first.to_s
436 | if mime && DEFAULT_MIME_TYPE_FORMAT_METHODS.key?(mime)
437 | [mime, File.read(obj.path)]
438 | end
439 | end
440 | end
441 | end
442 | end
443 |
--------------------------------------------------------------------------------
/lib/iruby/input/README.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "markdown",
5 | "metadata": {},
6 | "source": [
7 | "# IRuby Input\n",
8 | "\n",
9 | "The `IRuby::Input` class makes it easier for IRuby users to get input from users. For example:"
10 | ]
11 | },
12 | {
13 | "cell_type": "code",
14 | "execution_count": null,
15 | "metadata": {
16 | "collapsed": false
17 | },
18 | "outputs": [],
19 | "source": [
20 | "name = IRuby.input 'Enter your name'"
21 | ]
22 | },
23 | {
24 | "cell_type": "markdown",
25 | "metadata": {},
26 | "source": [
27 | "The following input methods are supported on the `IRuby` module:\n",
28 | "\n",
29 | "| method | description |\n",
30 | "| -------- | -------- |\n",
31 | "| `input(prompt)` | Prompts the user for a line of input |\n",
32 | "| `password(prompt)` | Prompts the user for a password |\n",
33 | "| `form(&block)` | Presents a form to the user |\n",
34 | "| `popup(title,&block)` | Displays a form to the user as a popup |"
35 | ]
36 | },
37 | {
38 | "cell_type": "markdown",
39 | "metadata": {},
40 | "source": [
41 | "## Forms\n",
42 | "\n",
43 | "Forms are groups of inputs to be collected from the user. For example:"
44 | ]
45 | },
46 | {
47 | "cell_type": "code",
48 | "execution_count": null,
49 | "metadata": {
50 | "collapsed": false
51 | },
52 | "outputs": [],
53 | "source": [
54 | "result = IRuby.form do \n",
55 | " input :username\n",
56 | " password :password\n",
57 | " button\n",
58 | "end"
59 | ]
60 | },
61 | {
62 | "cell_type": "markdown",
63 | "metadata": {},
64 | "source": [
65 | "The following methods are available to build forms: \n",
66 | "\n",
67 | "| method | description |\n",
68 | "| -------- | -------- |\n",
69 | "| `input(key=:input)` | Prompts the user for a line of input |\n",
70 | "| `textarea(key=:textarea),` | Adds a textarea to the form |\n",
71 | "| `password(key=:password)` | Prompts the user for a password |\n",
72 | "| `button(key=:done, color: :blue)` | Adds a submit button to the form |\n",
73 | "| `cancel(prompt='Cancel')` | Adds a cancel button to the form |\n",
74 | "| `text(string)` | Adds text to the form |\n",
75 | "| `html(&block)` | Inserts HTML from the given [erector block](https://github.com/erector/erector) |\n",
76 | "| `file(key=:file)` | Adds a file input to the form |\n",
77 | "| `date(key=:date)` | Adds a date picker to the form |\n",
78 | "| `select(*options)` | Adds a dropdown select input to the form |\n",
79 | "| `radio(*options)` | Adds a radio select input to the form |\n",
80 | "| `checkbox(*options)` | Adds checkbox inputs to the form |"
81 | ]
82 | },
83 | {
84 | "cell_type": "markdown",
85 | "metadata": {},
86 | "source": [
87 | "## Popups\n",
88 | " \n",
89 | "Popups are just forms in a bootstrap modal. They are useful when users **Run All** in a notebook with a lot of inputs. The popups always appear in the same spot, so users don't have to scroll down to find the next input. \n",
90 | "\n",
91 | "Popups accept a `title` argument, for example: "
92 | ]
93 | },
94 | {
95 | "cell_type": "code",
96 | "execution_count": null,
97 | "metadata": {
98 | "collapsed": false
99 | },
100 | "outputs": [],
101 | "source": [
102 | "result = IRuby.popup 'Please enter your name' do \n",
103 | " input\n",
104 | " button\n",
105 | "end"
106 | ]
107 | },
108 | {
109 | "cell_type": "markdown",
110 | "metadata": {},
111 | "source": [
112 | "## Submit and cancel\n",
113 | "\n",
114 | "The enter key will submit an input or form and the escape key will cancel it. Canceled inputs are returned as `nil`. Inputs are automatically canceled if destroyed. An input can be destroyed by clearing its cell's output. The `cancel` button will cancel a form and all other buttons will submit it. \n",
115 | "\n",
116 | "After a form destroyed, the cell's output is cleared. Be careful not to prompt for input in a block that has previous output you would like to keep. Output is cleared to prevent forms from interfering with one another and to ensure that inputs are not inadvertently saved to the notebook. "
117 | ]
118 | },
119 | {
120 | "cell_type": "code",
121 | "execution_count": null,
122 | "metadata": {
123 | "collapsed": false
124 | },
125 | "outputs": [],
126 | "source": [
127 | "result = IRuby.popup 'Confirm' do \n",
128 | " text 'Are you sure you want to continue?'\n",
129 | " cancel 'No'\n",
130 | " button 'Yes'\n",
131 | "end"
132 | ]
133 | },
134 | {
135 | "cell_type": "markdown",
136 | "metadata": {},
137 | "source": [
138 | "## Custom keys\n",
139 | "\n",
140 | "Every widget has an entry in the final results hash. A custom key can be passed as the first parameter to the hash. If no key is provided, the widget name is used as the key. The `cancel` widget has no key; it's first parameter is its label. "
141 | ]
142 | },
143 | {
144 | "cell_type": "code",
145 | "execution_count": null,
146 | "metadata": {
147 | "collapsed": false
148 | },
149 | "outputs": [],
150 | "source": [
151 | "result = IRuby.form do\n",
152 | " input :username\n",
153 | " password :password\n",
154 | "end"
155 | ]
156 | },
157 | {
158 | "cell_type": "markdown",
159 | "metadata": {},
160 | "source": [
161 | "## Custom labels\n",
162 | "\n",
163 | "Field labels appear to the left of the field. Button labels appear as the text on the button. `cancel` labels are passed as the first argument. All other widgets' labels are set using the `label` parameter. "
164 | ]
165 | },
166 | {
167 | "cell_type": "code",
168 | "execution_count": null,
169 | "metadata": {
170 | "collapsed": false
171 | },
172 | "outputs": [],
173 | "source": [
174 | "result = IRuby.form do \n",
175 | " input :name, label: 'Please enter your name'\n",
176 | " cancel 'None of your business!'\n",
177 | " button :submit, label: 'All done'\n",
178 | "end"
179 | ]
180 | },
181 | {
182 | "cell_type": "markdown",
183 | "metadata": {},
184 | "source": [
185 | "## Defaults\n",
186 | "\n",
187 | "Most inputs will accept a `default` parameter. If no default is given, the default is `nil`. Since checkboxes can have multiple values selected, you can pass an array of values. To check everything, pass `true` as the default. "
188 | ]
189 | },
190 | {
191 | "cell_type": "code",
192 | "execution_count": null,
193 | "metadata": {
194 | "collapsed": false
195 | },
196 | "outputs": [],
197 | "source": [
198 | "result = IRuby.form do \n",
199 | " checkbox :one, 'Fish', 'Cat', 'Dog', default: 'Fish'\n",
200 | " checkbox :many, 'Fish', 'Cat', 'Dog', default: ['Cat', 'Dog']\n",
201 | " checkbox :all, 'Fish', 'Cat', 'Dog', default: true\n",
202 | " button :submit, label: 'All done'\n",
203 | "end"
204 | ]
205 | },
206 | {
207 | "cell_type": "markdown",
208 | "metadata": {},
209 | "source": [
210 | "## Dates\n",
211 | "\n",
212 | "The `date` widget provides a calendar popup and returns a `Time` object. It's default should also be a `Time` object. "
213 | ]
214 | },
215 | {
216 | "cell_type": "code",
217 | "execution_count": null,
218 | "metadata": {
219 | "collapsed": false
220 | },
221 | "outputs": [],
222 | "source": [
223 | "result = IRuby.form do \n",
224 | " date :birthday\n",
225 | " date :today, default: Time.now\n",
226 | " button\n",
227 | "end"
228 | ]
229 | },
230 | {
231 | "cell_type": "markdown",
232 | "metadata": {},
233 | "source": [
234 | "## Buttons\n",
235 | "\n",
236 | "Buttons do not appear in the final hash unless they are clicked. If clicked, their value is `true`. Here are the various colors a button can be:"
237 | ]
238 | },
239 | {
240 | "cell_type": "code",
241 | "execution_count": null,
242 | "metadata": {
243 | "collapsed": false
244 | },
245 | "outputs": [],
246 | "source": [
247 | "result = IRuby.form do \n",
248 | " IRuby::Input::Button::COLORS.each_key do |color|\n",
249 | " button color, color: color\n",
250 | " end\n",
251 | "end"
252 | ]
253 | },
254 | {
255 | "cell_type": "markdown",
256 | "metadata": {},
257 | "source": [
258 | "## Textareas\n",
259 | "\n",
260 | "Textareas are multiline inputs that are convenient for larger inputs. If you need a line return when typing in a textarea, use shift+enter since enter will submit the form."
261 | ]
262 | },
263 | {
264 | "cell_type": "code",
265 | "execution_count": null,
266 | "metadata": {
267 | "collapsed": false
268 | },
269 | "outputs": [],
270 | "source": [
271 | "result = IRuby.form do \n",
272 | " text 'Enter email addresses, one per line (use shift+enter for newlines)'\n",
273 | " textarea :emails\n",
274 | "end"
275 | ]
276 | },
277 | {
278 | "cell_type": "markdown",
279 | "metadata": {},
280 | "source": [
281 | "## Text and HTML\n",
282 | "\n",
283 | "You can insert lines of text or custom html using their respective helpers:"
284 | ]
285 | },
286 | {
287 | "cell_type": "code",
288 | "execution_count": null,
289 | "metadata": {
290 | "collapsed": false
291 | },
292 | "outputs": [],
293 | "source": [
294 | "result = IRuby.form do \n",
295 | " html { h1 'Choose a Stooge' }\n",
296 | " text 'Choose your favorite stooge'\n",
297 | " select :stooge, 'Moe', 'Larry', 'Curly'\n",
298 | " button\n",
299 | "end"
300 | ]
301 | },
302 | {
303 | "cell_type": "markdown",
304 | "metadata": {},
305 | "source": [
306 | "## Dropdowns\n",
307 | "\n",
308 | "A `select` is a dropdown of options. Use a `multiple` to allow multiple selections. `multiple` widgets accept an additional `size` parameters that determines the number of rows. The default is 4. "
309 | ]
310 | },
311 | {
312 | "cell_type": "code",
313 | "execution_count": null,
314 | "metadata": {
315 | "collapsed": false
316 | },
317 | "outputs": [],
318 | "source": [
319 | "result = IRuby.form do \n",
320 | " select :stooge, 'Moe', 'Larry', 'Curly'\n",
321 | " select :stooge, 'Moe', 'Larry', 'Curly', default: 'Moe'\n",
322 | " multiple :stooges, 'Moe', 'Larry', 'Curly', default: true, size: 3\n",
323 | " multiple :stooges, 'Moe', 'Larry', 'Curly', default: ['Moe','Curly']\n",
324 | " button\n",
325 | "end"
326 | ]
327 | },
328 | {
329 | "cell_type": "markdown",
330 | "metadata": {},
331 | "source": [
332 | "## Radio selects and checkboxes\n",
333 | "\n",
334 | "Like selects, radio selects and checkboxes take multiple arguments, each one an option. If the first argument is a symbol, it is used as the key. \n",
335 | "\n",
336 | "Note that the `checkbox` widget will always return `nil` or an array. "
337 | ]
338 | },
339 | {
340 | "cell_type": "code",
341 | "execution_count": null,
342 | "metadata": {
343 | "collapsed": false
344 | },
345 | "outputs": [],
346 | "source": [
347 | "result = IRuby.form do \n",
348 | " radio :children, *(0..12), label: 'How many children do you have?'\n",
349 | " checkbox :gender, 'Male', 'Female', 'Intersex', label: 'Select the genders of your children'\n",
350 | " button\n",
351 | "end"
352 | ]
353 | },
354 | {
355 | "cell_type": "markdown",
356 | "metadata": {},
357 | "source": [
358 | "## Files\n",
359 | "\n",
360 | "Since file widgets capture the enter key, you should include a button when creating forms that contain only a file input:"
361 | ]
362 | },
363 | {
364 | "cell_type": "code",
365 | "execution_count": null,
366 | "metadata": {
367 | "collapsed": true
368 | },
369 | "outputs": [],
370 | "source": [
371 | "IRuby.form do \n",
372 | " file :avatar, label: 'Choose an Avatar'\n",
373 | " button :submit\n",
374 | "end"
375 | ]
376 | },
377 | {
378 | "cell_type": "markdown",
379 | "metadata": {},
380 | "source": [
381 | "File widgets return a hash with three keys: \n",
382 | "\n",
383 | "* `data`: The contents of the file as a string\n",
384 | "* `content_type`: The type of file, such as `text/plain` or `image/jpeg`\n",
385 | "* `name`: The name of the uploaded file"
386 | ]
387 | },
388 | {
389 | "cell_type": "markdown",
390 | "metadata": {},
391 | "source": [
392 | "## Example\n",
393 | "\n",
394 | "Here is an example form that uses every built-in widget. "
395 | ]
396 | },
397 | {
398 | "cell_type": "code",
399 | "execution_count": null,
400 | "metadata": {
401 | "collapsed": false,
402 | "scrolled": false
403 | },
404 | "outputs": [],
405 | "source": [
406 | "result = IRuby.form do \n",
407 | " html { h1 'The Everything Form' }\n",
408 | " text 'Marvel at the strange and varied inputs!'\n",
409 | " date\n",
410 | " file\n",
411 | " input :username\n",
412 | " password\n",
413 | " textarea\n",
414 | " radio *(1..10)\n",
415 | " checkbox 'Fish', 'Cat', 'Dog', label: 'Animals'\n",
416 | " select :color, *IRuby::Input::Button::COLORS.keys\n",
417 | " cancel \n",
418 | " button \n",
419 | "end"
420 | ]
421 | },
422 | {
423 | "cell_type": "markdown",
424 | "metadata": {},
425 | "source": [
426 | "## Writing your own widget\n",
427 | "\n",
428 | "Most form methods are `IRuby::Input::Widget` instances. A `Widget` is an [`Erector::Widget`](https://github.com/erector/erector) with some additional helpers. Here is the `cancel` widget:\n",
429 | "\n",
430 | "```ruby\n",
431 | "module IRuby\n",
432 | " module Input\n",
433 | " class Cancel < Widget\n",
434 | " needs :label\n",
435 | "\n",
436 | " builder :cancel do |label='Cancel'|\n",
437 | " add_button Cancel.new(label: label)\n",
438 | " end\n",
439 | "\n",
440 | " def widget_css\n",
441 | " \".iruby-cancel { margin-left: 5px; }\"\n",
442 | " end\n",
443 | "\n",
444 | " def widget_js\n",
445 | " <<-JS\n",
446 | " $('.iruby-cancel').click(function(){\n",
447 | " $('#iruby-form').remove();\n",
448 | " });\n",
449 | " JS\n",
450 | " end\n",
451 | "\n",
452 | " def widget_html\n",
453 | " button(\n",
454 | " @label,\n",
455 | " type: 'button', \n",
456 | " :'data-dismiss' => 'modal',\n",
457 | " class: \"btn btn-danger pull-right iruby-cancel\"\n",
458 | " )\n",
459 | " end\n",
460 | " end\n",
461 | " end\n",
462 | "end\n",
463 | "```\n",
464 | "\n",
465 | "The following methods are available for widgets to use or override: \n",
466 | "\n",
467 | "| method | description |\n",
468 | "| -------- | -------- |\n",
469 | "| `widget_js` | Returns the widget's Javascript |\n",
470 | "| `widget_css` | Returns the widget's CSS |\n",
471 | "| `widget_html` | Returns the widget's |\n",
472 | "| `builder(method,&block)` | Class method to add form building helpers. |\n",
473 | "\n",
474 | "The following methods are available in the `builder` block:\n",
475 | "\n",
476 | "| method | description |\n",
477 | "| -------- | -------- |\n",
478 | "| `add_field(field)` | Adds a widget to the form's field area |\n",
479 | "| `add_button(button)` | Adds a button to the form's button area |\n",
480 | "| `process(key,&block)` | Register a custom processing block for the given key in the results hash |\n",
481 | "| `unique_key(key)` | Returns a unique key for the given key. Use this to make sure that there are no key collisions in the final results hash. |\n",
482 | "\n",
483 | "A canceled form always returns `nil`. Otherwise, the form collects any element with a `data-iruby-key` and non-falsey `data-iruby-value` and passes those to the processor proc registered for the key. See the `File` widget for a more involved example of processing results."
484 | ]
485 | }
486 | ],
487 | "metadata": {
488 | "kernelspec": {
489 | "display_name": "Ruby 2.2.1",
490 | "language": "ruby",
491 | "name": "ruby"
492 | },
493 | "language_info": {
494 | "file_extension": ".rb",
495 | "mimetype": "application/x-ruby",
496 | "name": "ruby",
497 | "version": "2.2.3"
498 | }
499 | },
500 | "nbformat": 4,
501 | "nbformat_minor": 0
502 | }
503 |
--------------------------------------------------------------------------------