ihelp 0.3.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (2) hide show
  1. data/lib/ihelp.rb +661 -0
  2. metadata +45 -0
@@ -0,0 +1,661 @@
1
+ require 'rdoc/ri/ri_driver'
2
+ require 'rexml/document'
3
+
4
+ # Ri bindings for interactive use from within Ruby.
5
+ # Does a bit of second-guessing (Instance method? Class method?
6
+ # Try both unless explicitly defined. Not found in this class? Try the
7
+ # ancestor classes.)
8
+ #
9
+ # The goal is that help is given for all methods that have help.
10
+ #
11
+ # Examples:
12
+ #
13
+ # require 'ihelp'
14
+ #
15
+ # a = "string"
16
+ # a.help
17
+ # a.help :reverse
18
+ # a.help :map
19
+ # String.help
20
+ # String.help :new
21
+ # String.help :reverse
22
+ # String.help :map
23
+ # String.instance_help :reverse
24
+ # String.instance_help :new # => No help found.
25
+ # a.help :new
26
+ # help "String#reverse"
27
+ # help "String.reverse"
28
+ # a.method(:reverse).help # gets help for Method
29
+ # help "Hash#map"
30
+ #
31
+ #
32
+ # Custom help renderers:
33
+ #
34
+ # The help-method calls IHelp::Renderer's method defined by
35
+ # IHelp.renderer with the RI info object. You can print help
36
+ # out the way you want by defining your own renderer method
37
+ # in IHelp::Renderer and setting IHelp.renderer to the name
38
+ # of the method.
39
+ #
40
+ # Example:
41
+ #
42
+ # require 'ihelp'
43
+ #
44
+ # IHelp.renderers
45
+ # # => ["emacs", "rubydoc", "ri", "source", "html"]
46
+ #
47
+ # class IHelp::Renderer
48
+ # def print_name(info)
49
+ # puts info.full_name
50
+ # end
51
+ # end
52
+ #
53
+ # IHelp.renderers
54
+ # # => ["emacs", "rubydoc", "ri", "source", "print_name", "html"]
55
+ #
56
+ # IHelp.renderer = :print_name
57
+ #
58
+ # [1,2,3].help:reject
59
+ # # Array#reject
60
+ # # => nil
61
+ #
62
+ # The current renderers are:
63
+ #
64
+ # ri -- the default renderer
65
+ # html -- creates a HTML document for the help and opens it
66
+ # with the program named in IHelp.web_browser
67
+ # rubydoc -- opens the corresponding www.ruby-doc.org class
68
+ # documentation page with the program named in
69
+ # IHelp.web_browser
70
+ # emacs -- uses gnudoit and ri-emacs to display help in an Emacs buffer.
71
+ # The Emacs commands that I got it running with were:
72
+ # ;; make ri-emacs autoload according to its instructions
73
+ # M-x ruby-mode
74
+ # M-x gnuserv-start
75
+ # M-x run-ruby
76
+ # > IHelp.renderer = :emacs
77
+ # > String.help:center
78
+ # source -- uses RubyToRuby to print the source for the method
79
+ # (experimental)
80
+ #
81
+ #
82
+ # Changelog:
83
+ #
84
+ # 0.3.2
85
+ # Added support for ruby 1.8.5, added emacs renderer from
86
+ # rubykitch <rubykitch@ruby-lang.org>, made source renderer use the
87
+ # released version of RubyToRuby.
88
+ #
89
+ # License: Ruby's
90
+ #
91
+ # Author: Ilmari Heikkinen <kig misfiring net>
92
+ #
93
+ module IHelp
94
+ HELP_VERSION = "0.3.2"
95
+ end
96
+
97
+
98
+ module IHelp
99
+
100
+ # Returns list of available renderers.
101
+ def self.renderers
102
+ Renderer.instance_methods(false)
103
+ end
104
+
105
+ # Contains the help renderer methods to be used by IHelp#help.
106
+ # The help-method creates a new instance of Renderer and calls
107
+ # the method defined by IHelp.renderer with the RI info object.
108
+ #
109
+ class Renderer
110
+
111
+ # Default renderer method, opens the help using the IHelpDriver
112
+ # gotten from IHelp.ri_driver.
113
+ #
114
+ def ri(info)
115
+ IHelp.ri_driver.display_info(info)
116
+ end
117
+
118
+ # Opens the class documentation page on www.ruby-doc.org using
119
+ # the program defined in IHelp::Renderer.web_browser.
120
+ #
121
+ def rubydoc(info)
122
+ require 'uri'
123
+ class_name = parse_ruby_doc_url(info.full_name)
124
+ puts "Opening help for: #{class_name.gsub(/\//,"::")}"
125
+ system(IHelp.web_browser, "http://www.ruby-doc.org/core/classes/#{class_name}.html")
126
+ end
127
+
128
+ # Show sources -renderer using RubyToRuby.
129
+ #
130
+ # http://seattlerb.rubyforge.org/
131
+ #
132
+ # sudo gem install ruby2ruby
133
+ #
134
+ def source(info)
135
+ require 'ruby2ruby'
136
+ class_name = info.full_name.split(/[#\.]/).first
137
+ klass = class_name.split("::").inject(Object){|o,i| o.const_get(i)}
138
+ args = [klass]
139
+ args << info.name if info.is_a? RI::MethodDescription
140
+ puts RubyToRuby.translate(*args)
141
+ end
142
+
143
+ # XEmacs renderer.
144
+ # Uses ri-emacs to show the ri output in Emacs.
145
+ #
146
+ # http://rubyforge.org/projects/ri-emacs/
147
+ # http://www.rubyist.net/~rubikitch/computer/irbsh/index.en.html
148
+ #
149
+ def emacs(info)
150
+ system "gnudoit", %Q[(progn (ri "#{info.full_name}") "#{info.full_name}")]
151
+ end
152
+
153
+ def html(info)
154
+ puts "Opening help for: #{info.full_name}"
155
+ doc = REXML::Document.new
156
+ root = doc.add_element("html")
157
+ head = root.add_element("head")
158
+ title = head.add_element("title")
159
+ title.add_text("#{info.full_name} - RI Documentation")
160
+ body = root.add_element("body")
161
+ body.add_element(info.to_html.root)
162
+ tmp = Tempfile.new("#{info.full_name.gsub(/\W/,"_")}_doc.html")
163
+ tmp.write( doc.to_s(2) )
164
+ tmp.flush
165
+ pid = fork{
166
+ system(IHelp.web_browser, "file://#{tmp.path}")
167
+ tmp.close!
168
+ }
169
+ Process.detach(pid)
170
+ pid
171
+ end
172
+
173
+ private
174
+ def parse_ruby_doc_url(item_name)
175
+ item_name.split(/\.|#/,2).first.gsub(/::/,"/")
176
+ end
177
+
178
+ end
179
+
180
+
181
+ # Print out help for self.
182
+ #
183
+ # If method_name is given, prints help for that method.
184
+ # If instance is true, tries to find help only for the instance method.
185
+ # If instance is false, tries to find help for the object's method only.
186
+ # If instance is nil, checks object's method first, then instance method.
187
+ #
188
+ # Uses help_description(method_name, instance).
189
+ #
190
+ def help(method_name=nil, instance=nil)
191
+ info = help_description(method_name, instance)
192
+ if not info
193
+ puts "No help found."
194
+ return
195
+ end
196
+ IHelp.render(info)
197
+ end
198
+
199
+ # Print out help for instance method method_name.
200
+ # If no method_name given, behaves like #help.
201
+ #
202
+ def instance_help(method_name = nil)
203
+ help(method_name, true)
204
+ end
205
+
206
+ # Returns help string in YAML for self.
207
+ #
208
+ # If method_name is given, prints help for that method.
209
+ # If instance is true, tries to find help only for the instance method.
210
+ # If instance is false, tries to find help for the object's method only.
211
+ # If instance is nil, checks object's method first, then instance method.
212
+ # Returns nil if there is no help to be found.
213
+ #
214
+ def help_yaml(method_name=nil, instance=nil)
215
+ info = help_description(method_name, instance)
216
+ info.to_yaml if info
217
+ end
218
+
219
+ # Returns help string as a HTML REXML::Document with a DIV element as the root.
220
+ #
221
+ # If method_name is given, prints help for that method.
222
+ # If instance is true, tries to find help only for the instance method.
223
+ # If instance is false, tries to find help for the object's method only.
224
+ # If instance is nil, checks object's method first, then instance method.
225
+ # Returns nil if there is no help to be found.
226
+ #
227
+ def help_html(method_name=nil, instance=nil)
228
+ info = help_description(method_name, instance)
229
+ info.to_html if info
230
+ end
231
+
232
+ # Return RI::ClassDescription / RI::MethodDescription for self
233
+ # or its method meth, or its instance method meth if instance == true.
234
+ #
235
+ def help_description(method_name=nil, instance=nil)
236
+ IHelp.generate_help_description(self, method_name, instance)
237
+ end
238
+
239
+
240
+ class << self
241
+
242
+ # Help renderer to use.
243
+ attr_accessor :renderer
244
+
245
+ # Web browser to use for html and rubydoc renderers.
246
+ attr_accessor :web_browser
247
+
248
+ IHelp.renderer ||= :ri
249
+ IHelp.web_browser ||= 'firefox'
250
+
251
+ IHelp::RI_ARGS = []
252
+ if ENV["RI"]
253
+ IHelp::RI_ARGS = ENV["RI"].split.concat(ARGV)
254
+ end
255
+
256
+ # Render the RI info object a renderer method in IHelp::Renderer.
257
+ # The name of the renderer method to use is returned by IHelp.renderer,
258
+ # and can be set with IHelp.renderer=.
259
+ #
260
+ def render(info)
261
+ IHelp::Renderer.new.send(renderer, info)
262
+ end
263
+
264
+ def ri_driver
265
+ @ri_driver ||= IHelpDriver.new(RI_ARGS)
266
+ end
267
+
268
+ # Return RI::ClassDescription / RI::MethodDescription for klass
269
+ # or its method meth, or its instance method meth if instance == true.
270
+ #
271
+ def generate_help_description(klass, meth=nil, instance=nil)
272
+ meth_str = nil
273
+ double_colon = false
274
+ if meth
275
+ meth_str = meth.to_s
276
+ if /::|\.|#/ === meth_str # called with e.g."Array#str","String.new"
277
+ meth_str, klass_name, instance_help, double_colon =
278
+ get_help_klass_info_for_name(meth_str)
279
+ klass_ancs = find_ancestors(klass_name, instance)
280
+ else
281
+ klass_name, klass_ancs, instance_help =
282
+ get_help_klass_info(klass, instance)
283
+ end
284
+ else
285
+ klass_name, klass_ancs, instance_help =
286
+ get_help_klass_info(klass, instance)
287
+ end
288
+ info = get_help_info(meth_str, klass_name, klass_ancs, instance_help,
289
+ instance)
290
+ # Retry with method as class if double_colon-splitted and no info
291
+ if info.nil? and double_colon
292
+ klass_name = [klass_name, meth_str].join("::")
293
+ meth_str = nil
294
+ klass_ancs = find_ancestors(klass_name, instance)
295
+ info = get_help_info(
296
+ meth_str, klass_name, klass_ancs, instance_help, instance)
297
+ end
298
+ info
299
+ end
300
+
301
+ private
302
+ def get_help_klass_info(klass,instance)
303
+ if klass.is_a? Class or klass.is_a? Module
304
+ klass_ancs = klass.ancestors + klass.class.ancestors
305
+ klass_ancs -= [klass, klass.class]
306
+ instance = false if instance.nil?
307
+ # If we are an instance, set klass to our class
308
+ #
309
+ else
310
+ klass = klass.class
311
+ klass_ancs = klass.ancestors - [klass]
312
+ instance = true if instance.nil?
313
+ end
314
+ klass_name = klass.name
315
+ [klass_name, klass_ancs, instance]
316
+ end
317
+
318
+ def get_help_klass_info_for_name(meth_str)
319
+ double_colon = false
320
+ # Maybe we are being called with something like "Array#slice"
321
+ if /#/ === meth_str
322
+ klass_name, meth_str = meth_str.split(/#/, 2)
323
+ instance = true
324
+
325
+ # Or maybe the requested item is "Ri::RiDriver.new"
326
+ elsif /\./ === meth_str
327
+ klass_name, meth_str = meth_str.reverse.split(/\./, 2).
328
+ reverse.map{|i| i.reverse}
329
+ instance = false
330
+
331
+ # And the problematic case of "Test::Unit" (is Unit a class name or
332
+ # a method name? Why does Ri even care?)
333
+ else
334
+ klass_name, meth_str = meth_str.reverse.split(/::/, 2).
335
+ reverse.map{|i| i.reverse}
336
+ double_colon = true
337
+ instance = false
338
+ end
339
+ [meth_str, klass_name, instance, double_colon]
340
+ end
341
+
342
+ # Find ancestors for klass_name (works if the class has been loaded)
343
+ def find_ancestors(klass_name, instance)
344
+ similarily_named_class = nil
345
+ ObjectSpace.each_object(Class){|k|
346
+ similarily_named_class = k if k.name == klass_name
347
+ break if similarily_named_class
348
+ }
349
+ if similarily_named_class
350
+ klass_ancs = similarily_named_class.ancestors
351
+ klass_ancs += similarily_named_class.class.ancestors unless instance
352
+ else
353
+ klass_ancs = []
354
+ end
355
+ klass_ancs
356
+ end
357
+
358
+ def get_help_info(meth_str, klass_name, klass_ancs, instance_help, instance)
359
+ info = get_help_info_str(meth_str, klass_name, klass_ancs, instance_help)
360
+ # If instance is undefined, try both the class methods and instance
361
+ # methods.
362
+ if info.nil? and instance.nil?
363
+ info = get_help_info_str(
364
+ meth_str, klass_name, klass_ancs, (not instance_help))
365
+ end
366
+ info
367
+ end
368
+
369
+ def get_help_info_str(meth_str, klass_name, klass_ancs, instance)
370
+ info_str = ri_driver.get_info_str(klass_name, meth_str, instance)
371
+ if not info_str
372
+ # Walk through class hierarchy to find an inherited method
373
+ ancest = klass_ancs.find{|anc|
374
+ info_str = ri_driver.get_info_str(anc.name, meth_str, instance)
375
+ }
376
+ # Avoid returning Object in case of no help.
377
+ if ancest == Object and meth_str.nil? and klass_name != Object.name
378
+ info_str = nil
379
+ end
380
+ end
381
+ info_str
382
+ end
383
+ end
384
+
385
+
386
+ # Version of RiDriver that takes its options
387
+ # as parameter to #initialize.
388
+ #
389
+ class IHelpDriver < RiDriver
390
+
391
+ # Create new IHelpDriver, with the given args
392
+ # passed to @options, which is a RI::Options.instance
393
+ #
394
+ def initialize(args = [])
395
+ @options = RI::Options.instance
396
+ @options.parse(args)
397
+
398
+ paths = (if RUBY_VERSION > "1.8.4"
399
+ @options.doc_dir
400
+ else
401
+ @options.paths
402
+ end) || RI::Paths::PATH
403
+ if paths.empty?
404
+ report_missing_documentation(paths)
405
+ end
406
+ @ri_reader = RI::RiReader.new(RI::RiCache.new(paths))
407
+ @display = @options.displayer
408
+ end
409
+
410
+ # Get info string from ri database for klass_name [method_name]
411
+ #
412
+ def get_info_str(klass_name, method_name = nil, instance = false)
413
+ is_class_method = (not instance)
414
+ top_level_namespace = @ri_reader.top_level_namespace
415
+ namespaces = klass_name.split(/::/).inject(top_level_namespace){
416
+ |ns, current_name|
417
+ @ri_reader.lookup_namespace_in(current_name, ns)
418
+ }
419
+ return nil if namespaces.empty?
420
+ if method_name.nil?
421
+ get_class_info_str(namespaces)
422
+ else
423
+ methods = @ri_reader.find_methods(
424
+ method_name, is_class_method, namespaces)
425
+ return nil if methods.empty?
426
+ get_method_info_str(method_name, methods)
427
+ end
428
+ end
429
+
430
+ # Display the info based on if it's
431
+ # for a class or a method. Using ri's pager.
432
+ #
433
+ def display_info(info)
434
+ case [info.class] # only info.class doesn't work
435
+ when [RI::ClassDescription]
436
+ @display.display_class_info(info, @ri_reader)
437
+ when [RI::MethodDescription]
438
+ @display.display_method_info(info)
439
+ end
440
+ end
441
+
442
+ # Get info for the class in the given namespaces.
443
+ #
444
+ def get_class_info_str(namespaces)
445
+ return nil if namespaces.empty?
446
+ klass = nil
447
+ namespaces.find{|ns|
448
+ begin
449
+ klass = @ri_reader.get_class(ns)
450
+ rescue TypeError
451
+ nil
452
+ end
453
+ }
454
+ klass
455
+ end
456
+
457
+ # Get info for the method in the given methods.
458
+ #
459
+ def get_method_info_str(requested_method_name, methods)
460
+ if methods.size == 1
461
+ @ri_reader.get_method(methods.first)
462
+ else
463
+ entries = methods.find_all {|m| m.name == requested_method_name}
464
+ return nil if entries.empty?
465
+ method = nil
466
+ entries.find{|entry| method = @ri_reader.get_method(entry)}
467
+ method
468
+ end
469
+ end
470
+
471
+ end
472
+
473
+
474
+ end
475
+
476
+
477
+ module RI
478
+
479
+ class MethodDescription
480
+
481
+ # Creates HTML element from the MethodDescription.
482
+ # Uses container_tag as the root node name and header_tag
483
+ # as the tag for the header element that contains the method's name.
484
+ #
485
+ # Returns a REXML document with container_tag as the root element name.
486
+ #
487
+ def to_html(container_tag="div", header_tag="h1")
488
+ doc = REXML::Document.new
489
+ root = doc.add_element(container_tag)
490
+ header = root.add_element(header_tag)
491
+ header.add_text(full_name)
492
+ comment.each{|c|
493
+ tag = c.class.to_s.split("::").last
494
+ tag = "PRE" if tag == "VERB"
495
+ xmlstr = "<#{tag}>#{c.body}</#{tag}>"
496
+ c_doc = REXML::Document.new(xmlstr)
497
+ root.add_element( c_doc.root )
498
+ }
499
+ doc
500
+ end
501
+
502
+ end
503
+
504
+
505
+ class ClassDescription
506
+
507
+ # Creates HTML element from the ClassDescription.
508
+ # Uses container_tag as the root node name and header_tag
509
+ # as the tag for the header element that contains the classes name.
510
+ # Uses methods_header_tag as the tag for the "Class/Instance Methods"
511
+ # method list headers.
512
+ # Uses methods_tag as the tag for the method lists.
513
+ #
514
+ # Returns a REXML document with container_tag as the root element name.
515
+ #
516
+ def to_html(container_tag="div", header_tag="h1",
517
+ methods_header_tag="h2", methods_tag="p")
518
+ doc = REXML::Document.new
519
+ root = doc.add_element(container_tag)
520
+ header = root.add_element(header_tag)
521
+ header.add_text(full_name)
522
+ comment.each{|c|
523
+ tag = c.class.to_s.split("::").last
524
+ tag = "PRE" if tag == "VERB"
525
+ xmlstr = "<#{tag}>#{c.body}</#{tag}>"
526
+ c_doc = REXML::Document.new(xmlstr)
527
+ root.add_element( c_doc.root )
528
+ }
529
+ root.add_element(methods_header_tag).add_text("Class Methods")
530
+ cmethods = root.add_element(methods_tag)
531
+ class_methods[0...-1].each{|m|
532
+ cmethods.add(m.to_html.root)
533
+ cmethods.add_text(", ")
534
+ }
535
+ cmethods.add(class_methods.last.to_html.root)
536
+ root.add_element(methods_header_tag).add_text("Instance Methods")
537
+ imethods = root.add_element(methods_tag)
538
+ instance_methods[0...-1].each{|m|
539
+ imethods.add(m.to_html.root)
540
+ imethods.add_text(", ")
541
+ }
542
+ imethods.add(instance_methods.last.to_html.root)
543
+ doc
544
+ end
545
+
546
+ end
547
+
548
+
549
+ class MethodSummary
550
+
551
+ # Creates HTML element from the ClassDescription.
552
+ # Puts the method's name inside the tag named in
553
+ # container_tag.
554
+ #
555
+ # Returns a REXML document with container_tag as the root element name.
556
+ #
557
+ def to_html(container_tag="em")
558
+ doc = REXML::Document.new
559
+ doc.add_element(container_tag).add_text(name)
560
+ doc
561
+ end
562
+
563
+ end
564
+
565
+ end
566
+
567
+
568
+ class Object
569
+ include IHelp
570
+ extend IHelp
571
+ end
572
+
573
+
574
+ if __FILE__ == $0
575
+ require 'test/unit'
576
+
577
+ # to get around rdoc documenting NoHelp
578
+ eval("module NoHelp; end")
579
+
580
+ class HelpTest < Test::Unit::TestCase
581
+
582
+ def no_warn
583
+ old_w = $-w
584
+ $-w = nil
585
+ yield
586
+ $-w = old_w
587
+ end
588
+
589
+ def setup
590
+ no_warn{
591
+ Object.const_set("ARGV", ["--readline", "--prompt-mode", "simple"])
592
+ }
593
+ IHelp.instance_variable_set(
594
+ :@ri_driver,
595
+ IHelp::IHelpDriver.new(IHelp::RI_ARGS))
596
+ end
597
+
598
+ def test_simple_help
599
+ assert("string".help_yaml)
600
+ end
601
+
602
+ def test_method_help
603
+ assert("string".help_yaml(:reverse))
604
+ end
605
+
606
+ def test_inherited_method_help
607
+ assert("string".help_yaml(:map))
608
+ end
609
+
610
+ def test_class_help
611
+ assert(String.help_yaml)
612
+ end
613
+
614
+ def test_class_method_help
615
+ assert(String.help_yaml(:new))
616
+ end
617
+
618
+ def test_class_inherited_method_help
619
+ assert(String.help_yaml(:map))
620
+ end
621
+
622
+ def test_method_equalities
623
+ assert(String.help_yaml(:new) ==
624
+ "string".help_yaml(:new))
625
+ assert(String.help_yaml(:reverse) ==
626
+ "string".help_yaml(:reverse))
627
+ end
628
+
629
+ def test_method_constraints
630
+ assert((not "string".help_yaml(:new,true)))
631
+ assert((not "string".help_yaml(:reverse,false)))
632
+ assert((not String.help_yaml(:new,true)))
633
+ assert((not String.help_yaml(:reverse,false)))
634
+ end
635
+
636
+ def test_help_yamlings
637
+ assert("string".help_yaml(:reverse) ==
638
+ help_yaml("String#reverse"))
639
+ assert(String.help_yaml(:new) ==
640
+ help_yaml("String::new"))
641
+ end
642
+
643
+ def test_multipart_namespaces
644
+ assert(Test::Unit.help_yaml)
645
+ assert(help_yaml("Test::Unit"))
646
+ assert(Test::Unit.help_yaml("run?"))
647
+ assert(help_yaml("Test::Unit.run?"))
648
+ assert(help_yaml("Test::Unit::run?"))
649
+ assert(help_yaml("Test::Unit#run?"))
650
+ end
651
+
652
+ def test_not_found
653
+ assert((NoHelp.help_yaml == nil))
654
+ assert((String.help_yaml(:nonexistent) == nil))
655
+ end
656
+
657
+ end
658
+
659
+
660
+ end
661
+
metadata ADDED
@@ -0,0 +1,45 @@
1
+ --- !ruby/object:Gem::Specification
2
+ rubygems_version: 0.8.11
3
+ specification_version: 1
4
+ name: ihelp
5
+ version: !ruby/object:Gem::Version
6
+ version: 0.3.2
7
+ date: 2006-11-13 00:00:00 +02:00
8
+ summary: Interactive help
9
+ require_paths:
10
+ - lib
11
+ email: kig@misfiring.net
12
+ homepage: ihelp.rubyforge.org
13
+ rubyforge_project: ihelp
14
+ description:
15
+ autorequire:
16
+ default_executable:
17
+ bindir: bin
18
+ has_rdoc: false
19
+ required_ruby_version: !ruby/object:Gem::Version::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: 1.8.1
24
+ version:
25
+ platform: ruby
26
+ signing_key:
27
+ cert_chain:
28
+ authors:
29
+ - Ilmari Heikkinen
30
+ files:
31
+ - lib/ihelp.rb
32
+ test_files: []
33
+
34
+ rdoc_options: []
35
+
36
+ extra_rdoc_files: []
37
+
38
+ executables: []
39
+
40
+ extensions: []
41
+
42
+ requirements: []
43
+
44
+ dependencies: []
45
+