Skip to content

Commit 05ff0d4

Browse files
committed
Introduce the new Application classes
1 parent 2d695c9 commit 05ff0d4

File tree

8 files changed

+964
-0
lines changed

8 files changed

+964
-0
lines changed

lib/iruby/application.rb

Lines changed: 375 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,375 @@
1+
require "fileutils"
2+
require "json"
3+
require "optparse"
4+
require "rbconfig"
5+
require "singleton"
6+
7+
require_relative "kernel_app"
8+
9+
module IRuby
10+
class Application
11+
include Singleton
12+
13+
# Set the application instance up.
14+
def setup(argv=nil)
15+
@iruby_executable = File.expand_path($PROGRAM_NAME)
16+
parse_command_line(argv)
17+
end
18+
19+
# Parse the command line arguments
20+
#
21+
# @param argv [Array<String>, nil] The array of arguments.
22+
private def parse_command_line(argv)
23+
argv = ARGV.dup if argv.nil?
24+
@argv = argv # save the original
25+
26+
case argv[0]
27+
when "help"
28+
# turn `iruby help notebook` into `iruby notebook -h`
29+
argv = [*argv[1..-1], "-h"]
30+
when "version"
31+
# turn `iruby version` into `iruby -v`
32+
argv = ["-v", *argv[1..-1]]
33+
else
34+
argv = argv.dup # prevent to break @argv
35+
end
36+
37+
opts = OptionParser.new
38+
opts.program_name = "IRuby"
39+
opts.version = ::IRuby::VERSION
40+
opts.banner = "Usage: #{$PROGRAM_NAME} [options] [subcommand] [options]"
41+
42+
opts.on_tail("-h", "--help") do
43+
print_help(opts)
44+
exit
45+
end
46+
47+
opts.on_tail("-v", "--version") do
48+
puts opts.ver
49+
exit
50+
end
51+
52+
opts.order!(argv)
53+
54+
if argv.length == 0 || argv[0].start_with?("-")
55+
# If no subcommand is given, we use the console
56+
argv = ["console", *argv]
57+
end
58+
59+
parse_sub_command(argv) if argv.length > 0
60+
end
61+
62+
SUB_COMMANDS = {
63+
"register" => "Register IRuby kernel.",
64+
"unregister" => "Unregister the existing IRuby kernel.",
65+
"kernel" => "Launch IRuby kernel",
66+
"console" => "Launch jupyter console with IRuby kernel"
67+
}.freeze.each_value(&:freeze)
68+
69+
private_constant :SUB_COMMANDS
70+
71+
private def parse_sub_command(argv)
72+
sub_cmd, *sub_argv = argv
73+
case sub_cmd
74+
when *SUB_COMMANDS.keys
75+
@sub_cmd = sub_cmd.to_sym
76+
@sub_argv = sub_argv
77+
else
78+
$stderr.puts "Invalid sub-command name: #{sub_cmd}"
79+
print_help(opts, $stderr)
80+
abort
81+
end
82+
end
83+
84+
private def print_help(opts, out=$stdout)
85+
out.puts opts.help
86+
out.puts
87+
out.puts "Sub-commands"
88+
out.puts "============"
89+
SUB_COMMANDS.each do |name, description|
90+
out.puts "#{name}"
91+
out.puts " #{description}"
92+
end
93+
end
94+
95+
def run
96+
case @sub_cmd
97+
when :register
98+
register_kernel(@sub_argv)
99+
when :unregister
100+
unregister_kernel(@sub_argv)
101+
when :console
102+
exec_jupyter(@sub_cmd.to_s, @sub_argv)
103+
when :kernel
104+
@sub_app = KernelApplication.new(@sub_argv)
105+
@sub_app.run
106+
else
107+
raise "[IRuby][BUG] Unknown subcommand: #{@sub_cmd}; this must be treated in parse_command_line."
108+
end
109+
end
110+
111+
ruby_version_info = RUBY_VERSION.split('.')
112+
DEFAULT_KERNEL_NAME = "ruby#{ruby_version_info[0]}".freeze
113+
DEFAULT_DISPLAY_NAME = "Ruby #{ruby_version_info[0]} (iruby kernel)"
114+
115+
RegisterParams = Struct.new(
116+
:name,
117+
:display_name,
118+
:profile,
119+
:env,
120+
:user,
121+
:prefix,
122+
:sys_prefix,
123+
:force,
124+
:ipython_dir
125+
) do
126+
def initialize(*args, **kw)
127+
super
128+
self.name ||= DEFAULT_KERNEL_NAME
129+
self.force = false
130+
self.user = true
131+
end
132+
end
133+
134+
def register_kernel(argv)
135+
params = parse_register_command_line(argv)
136+
137+
if params.name != DEFAULT_KERNEL_NAME
138+
# `--name` is specified and `--display-name` is not
139+
# default `params.display_name` to `params.name`
140+
params.display_name ||= params.name
141+
end
142+
143+
check_and_warn_kernel_in_default_ipython_directory(params)
144+
145+
if installed_kernel_exist?(params.name, params.ipython_dir)
146+
unless params.force
147+
$stderr.puts "IRuby kernel named `#{params.name}` already exists!"
148+
$stderr.puts "Use --force to force register the new kernel."
149+
exit 1
150+
end
151+
end
152+
153+
Dir.mktmpdir("iruby_kernel") do |tmpdir|
154+
path = File.join(tmpdir, DEFAULT_KERNEL_NAME)
155+
FileUtils.mkdir_p(path)
156+
157+
# Stage assets
158+
assets_dir = File.expand_path("../assets", __FILE__)
159+
FileUtils.cp_r(Dir.glob(File.join(assets_dir, "*")), path)
160+
161+
kernel_dict = {
162+
"argv" => make_iruby_cmd(),
163+
"display_name" => params.display_name || DEFAULT_DISPLAY_NAME,
164+
"language" => "ruby",
165+
"metadata" => {"debugger": false}
166+
}
167+
168+
# TODO: Support params.profile
169+
# TODO: Support params.env
170+
171+
kernel_content = JSON.pretty_generate(kernel_dict)
172+
File.write(File.join(path, "kernel.json"), kernel_content)
173+
174+
args = ["--name=#{params.name}"]
175+
args << "--user" if params.user
176+
args << path
177+
178+
# TODO: Support params.prefix
179+
# TODO: Support params.sys_prefix
180+
181+
system("jupyter", "kernelspec", "install", *args)
182+
end
183+
end
184+
185+
# Warn the existence of the IRuby kernel in the default IPython's kernels directory
186+
private def check_and_warn_kernel_in_default_ipython_directory(params)
187+
default_ipython_kernels_dir = File.expand_path("~/.ipython/kernels")
188+
[params.name, "ruby"].each do |name|
189+
if File.exist?(File.join(default_ipython_kernels_dir, name, "kernel.json"))
190+
warn "IRuby kernel `#{name}` already exists in the deprecated IPython's data directory."
191+
end
192+
end
193+
end
194+
195+
alias __system__ system
196+
197+
private def system(*cmdline, dry_run: false)
198+
$stderr.puts "EXECUTE: #{cmdline.map {|x| x.include?(' ') ? x.inspect : x}.join(' ')}"
199+
__system__(*cmdline) unless dry_run
200+
end
201+
202+
private def installed_kernel_exist?(name, ipython_dir)
203+
kernels_dir = resolve_kernelspec_dir(ipython_dir)
204+
kernel_dir = File.join(kernels_dir, name)
205+
File.file?(File.join(kernel_dir, "kernel.json"))
206+
end
207+
208+
private def resolve_kernelspec_dir(ipython_dir)
209+
if ENV.has_key?("JUPYTER_DATA_DIR")
210+
if ENV.has_key?("IPYTHONDIR")
211+
warn "both JUPYTER_DATA_DIR and IPYTHONDIR are supplied; IPYTHONDIR is ignored."
212+
end
213+
jupyter_data_dir = ENV["JUPYTER_DATA_DIR"]
214+
return File.join(jupyter_data_dir, "kernels")
215+
end
216+
217+
if ipython_dir.nil? && ENV.key?("IPYTHONDIR")
218+
warn 'IPYTHONDIR is deprecated. Use JUPYTER_DATA_DIR instead.'
219+
ipython_dir = ENV["IPYTHONDIR"]
220+
end
221+
222+
if ipython_dir
223+
File.join(ipython_dir, 'kerenels')
224+
else
225+
Jupyter.kernelspec_dir
226+
end
227+
end
228+
229+
private def make_iruby_cmd(executable: nil, extra_arguments: nil)
230+
executable ||= default_executable
231+
extra_arguments ||= []
232+
[*Array(executable), "kernel", "-f", "{connection_file}", *extra_arguments]
233+
end
234+
235+
private def default_executable
236+
[RbConfig.ruby, @iruby_executable]
237+
end
238+
239+
private def parse_register_command_line(argv)
240+
opts = OptionParser.new
241+
opts.banner = "Usage: #{$PROGRAM_NAME} register [options]"
242+
243+
params = RegisterParams.new
244+
245+
opts.on(
246+
"--force",
247+
"Force register a new kernel spec. The existing kernel spec will be removed."
248+
) { params.force = true }
249+
250+
opts.on(
251+
"--user",
252+
"Register for the current user instead of system-wide."
253+
) { params.user = true }
254+
255+
opts.on(
256+
"--name=VALUE", String,
257+
"Specify a name for the kernelspec. This is needed to have multiple IRuby kernels at the same time."
258+
) {|v| params.name = v }
259+
260+
opts.on(
261+
"--display-name=VALUE", String,
262+
"Specify the display name for the kernelspec. This is helpful when you have multiple IRuby kernels."
263+
) {|v| kernel_display_name = v }
264+
265+
# TODO: --profile
266+
# TODO: --prefix
267+
# TODO: --sys-prefix
268+
# TODO: --env
269+
270+
define_ipython_dir_option(opts, params)
271+
272+
opts.order!(argv)
273+
274+
params
275+
end
276+
277+
UnregisterParams = Struct.new(
278+
:names,
279+
#:profile,
280+
#:user,
281+
#:prefix,
282+
#:sys_prefix,
283+
:ipython_dir,
284+
:force,
285+
:yes
286+
) do
287+
def initialize(*args, **kw)
288+
super
289+
self.names = []
290+
# self.user = true
291+
self.force = false
292+
self.yes = false
293+
end
294+
end
295+
296+
def unregister_kernel(argv)
297+
params = parse_unregister_command_line(argv)
298+
opts = []
299+
opts << "-y" if params.yes
300+
opts << "-f" if params.force
301+
system("jupyter", "kernelspec", "uninstall", *opts, *params.names)
302+
end
303+
304+
private def parse_unregister_command_line(argv)
305+
opts = OptionParser.new
306+
opts.banner = "Usage: #{$PROGRAM_NAME} unregister [options] NAME [NAME ...]"
307+
308+
params = UnregisterParams.new
309+
310+
opts.on(
311+
"-f", "--force",
312+
"Force removal, don't prompt for confirmation."
313+
) { params.force = true}
314+
315+
opts.on(
316+
"-y", "--yes",
317+
"Answer yes to any prompts."
318+
) { params.yes = true }
319+
320+
# TODO: --user
321+
# TODO: --profile
322+
# TODO: --prefix
323+
# TODO: --sys-prefix
324+
325+
define_ipython_dir_option(opts, params)
326+
327+
opts.order!(argv)
328+
329+
params.names = argv.dup
330+
331+
params
332+
end
333+
334+
def exec_jupyter(sub_cmd, argv)
335+
opts = OptionParser.new
336+
opts.banner = "Usage: #{$PROGRAM_NAME} unregister [options]"
337+
338+
kernel_name = resolve_installed_kernel_name(DEFAULT_KERNEL_NAME)
339+
opts.on(
340+
"--kernel=NAME", String,
341+
"The name of the default kernel to start."
342+
) {|v| kernel_name = v }
343+
344+
opts.order!(argv)
345+
346+
opts = ["--kernel=#{kernel_name}"]
347+
exec("jupyter", "console", *opts)
348+
end
349+
350+
private def resolve_installed_kernel_name(default_name)
351+
kernels = IO.popen(["jupyter", "kernelspec", "list", "--json"], "r", err: File::NULL) do |jupyter_out|
352+
JSON.load(jupyter_out.read)
353+
end
354+
unless kernels["kernelspecs"].key?(default_name)
355+
return "ruby" if kernels["kernelspecs"].key?("ruby")
356+
end
357+
default_name
358+
end
359+
360+
private def define_ipython_dir_option(opts, params)
361+
opts.on(
362+
"--ipython-dir=DIR", String,
363+
"Specify the IPython's data directory (DEPRECATED)."
364+
) do |v|
365+
if ENV.key?("JUPYTER_DATA_DIR")
366+
warn 'Both JUPYTER_DATA_DIR and --ipython-dir are supplied; --ipython-dir is ignored.'
367+
else
368+
warn '--ipython-dir is deprecated. Use JUPYTER_DATA_DIR environment variable instead.'
369+
end
370+
371+
params.ipython_dir = v
372+
end
373+
end
374+
end
375+
end

0 commit comments

Comments
 (0)