toys-core 0.15.6 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/lib/toys/loader.rb CHANGED
@@ -49,7 +49,8 @@ module Toys
49
49
  mixin_lookup: nil,
50
50
  middleware_lookup: nil,
51
51
  template_lookup: nil,
52
- git_cache: nil)
52
+ git_cache: nil,
53
+ gems_util: nil)
53
54
  if index_file_name && ::File.extname(index_file_name) != ".rb"
54
55
  raise ::ArgumentError, "Illegal index file name #{index_file_name.inspect}"
55
56
  end
@@ -73,6 +74,7 @@ module Toys
73
74
  @middleware_stack = Middleware.stack(middleware_stack)
74
75
  @delimiter_handler = DelimiterHandler.new(extra_delimiters)
75
76
  @git_cache = git_cache
77
+ @gems_util = gems_util
76
78
  get_tool([], -999_999)
77
79
  end
78
80
 
@@ -204,8 +206,7 @@ module Toys
204
206
  high_priority: false,
205
207
  update: false,
206
208
  context_directory: nil)
207
- git_cache = @git_cache || Loader.default_git_cache
208
- path = git_cache.get(git_remote, path: git_path, commit: git_commit, update: update)
209
+ path = resolve_git_path(git_remote, git_path, git_commit, update)
209
210
  @mutex.synchronize do
210
211
  raise "Cannot add a git source after tool loading has started" if @loading_started
211
212
  priority = high_priority ? (@max_priority += 1) : (@min_priority -= 1)
@@ -219,6 +220,43 @@ module Toys
219
220
  self
220
221
  end
221
222
 
223
+ ##
224
+ # Add a configuration gem source to the loader.
225
+ #
226
+ # @param gem_name [String] The name of the gem
227
+ # @param gem_version [String,Array<String>] The version requirements
228
+ # @param gem_path [String] The path from the gem's toys directory to the
229
+ # relevant file or directory. Specify the empty string to use the
230
+ # entire toys directory.
231
+ # @param high_priority [Boolean] If true, add this path at the top of the
232
+ # priority list. Defaults to false, indicating the new path should be
233
+ # at the bottom of the priority list.
234
+ # @param gem_toys_dir [String] The name of the toys directory. Optional.
235
+ # Defaults to the directory specified in the gem's metadata, or the
236
+ # value "toys".
237
+ # @param context_directory [String,nil] The context directory for tools
238
+ # loaded from this source. You can pass a directory path as a string,
239
+ # or `nil` to denote no context. Defaults to `nil`.
240
+ # @return [self]
241
+ #
242
+ def add_gem(gem_name, gem_version, gem_path,
243
+ high_priority: false,
244
+ gem_toys_dir: nil,
245
+ context_directory: nil)
246
+ gem_version, gem_path, path = resolve_gem_info(gem_name, gem_version, gem_toys_dir, gem_path)
247
+ @mutex.synchronize do
248
+ raise "Cannot add a gem source after tool loading has started" if @loading_started
249
+ priority = high_priority ? (@max_priority += 1) : (@min_priority -= 1)
250
+ source = SourceInfo.create_gem_root(gem_name, gem_version, gem_path, path, priority,
251
+ context_directory: context_directory,
252
+ data_dir_name: @data_dir_name,
253
+ lib_dir_name: @lib_dir_name)
254
+ @roots_by_priority[priority] = source
255
+ @worklist << [source, [], priority]
256
+ end
257
+ self
258
+ end
259
+
222
260
  ##
223
261
  # Given a list of command line arguments, find the appropriate tool to
224
262
  # handle the command, loading it from the configuration if necessary.
@@ -288,7 +326,7 @@ module Toys
288
326
  found_tools = all_cur_definitions.find_all do |tool|
289
327
  name = tool.full_name
290
328
  name.length > len && name.slice(0, len) == words &&
291
- (include_hidden || name[len..-1].none? { |word| word.start_with?("_") })
329
+ (include_hidden || name[len..].none? { |word| word.start_with?("_") })
292
330
  end
293
331
  found_tools.sort_by!(&:full_name)
294
332
  found_tools = filter_non_runnable_tools(found_tools, include_namespaces, include_non_runnable)
@@ -303,7 +341,7 @@ module Toys
303
341
  # @param words [Array<String>] The name of the parent tool
304
342
  # @return [Boolean]
305
343
  #
306
- def has_subtools?(words) # rubocop:disable Naming/PredicateName
344
+ def has_subtools?(words) # rubocop:disable Naming/PredicatePrefix
307
345
  load_for_prefix(words)
308
346
  len = words.length
309
347
  all_cur_definitions.any? do |tool|
@@ -439,8 +477,7 @@ module Toys
439
477
  #
440
478
  def load_path(parent_source, path, words, remaining_words, priority)
441
479
  if parent_source.git_remote
442
- raise LoaderError,
443
- "Git source #{parent_source.source_name} tried to load from the local file system"
480
+ raise LoaderError, "Git source #{parent_source.source_name} tried to load from the local file system"
444
481
  end
445
482
  source = parent_source.absolute_child(path)
446
483
  @mutex.synchronize do
@@ -454,16 +491,30 @@ module Toys
454
491
  #
455
492
  # @private This interface is internal and subject to change without warning.
456
493
  #
457
- def load_git(parent_source, git_remote, git_path, git_commit, words, remaining_words, priority,
458
- update: false)
459
- git_cache = @git_cache || Loader.default_git_cache
460
- path = git_cache.get(git_remote, path: git_path, commit: git_commit, update: update)
494
+ def load_git(parent_source, git_remote, git_path, git_commit, update,
495
+ words, remaining_words, priority)
496
+ path = resolve_git_path(git_remote, git_path, git_commit, update)
461
497
  source = parent_source.git_child(git_remote, git_path, git_commit, path)
462
498
  @mutex.synchronize do
463
499
  load_validated_path(source, words, remaining_words, priority)
464
500
  end
465
501
  end
466
502
 
503
+ ##
504
+ # Load configuration from the given gem. This is called from the `load_gem`
505
+ # directive in the DSL.
506
+ #
507
+ # @private This interface is internal and subject to change without warning.
508
+ #
509
+ def load_gem(parent_source, gem_name, gem_version, gem_toys_dir, gem_path,
510
+ words, remaining_words, priority)
511
+ gem_version, gem_path, path = resolve_gem_info(gem_name, gem_version, gem_toys_dir, gem_path)
512
+ source = parent_source.gem_child(gem_name, gem_version, gem_path, path)
513
+ @mutex.synchronize do
514
+ load_validated_path(source, words, remaining_words, priority)
515
+ end
516
+ end
517
+
467
518
  ##
468
519
  # Load a subtool block. Called from the `tool` directive in the DSL.
469
520
  #
@@ -478,6 +529,7 @@ module Toys
478
529
 
479
530
  @git_cache_mutex = ::Mutex.new
480
531
  @default_git_cache = nil
532
+ @default_gems_util = nil
481
533
 
482
534
  ##
483
535
  # Get a global default GitCache.
@@ -493,6 +545,20 @@ module Toys
493
545
  end
494
546
  end
495
547
 
548
+ ##
549
+ # Get a global default Gems utility.
550
+ #
551
+ # @private This interface is internal and subject to change without warning.
552
+ #
553
+ def self.default_gems_util
554
+ @git_cache_mutex.synchronize do
555
+ @default_gems_util ||= begin
556
+ require "toys/utils/gems"
557
+ Utils::Gems.new
558
+ end
559
+ end
560
+ end
561
+
496
562
  ##
497
563
  # Determine the next setting for remaining_words, given a word.
498
564
  #
@@ -640,6 +706,28 @@ module Toys
640
706
 
641
707
  private
642
708
 
709
+ ##
710
+ # Resolve the file system path to the given object in the git cache
711
+ #
712
+ def resolve_git_path(git_remote, git_path, git_commit, update)
713
+ git_cache = @git_cache || Loader.default_git_cache
714
+ git_cache.get(git_remote, path: git_path, commit: git_commit, update: update)
715
+ end
716
+
717
+ ##
718
+ # Resolve information for a gem source.
719
+ #
720
+ def resolve_gem_info(gem_name, gem_version, gem_toys_dir, gem_path)
721
+ gems_util = @gems_util || Loader.default_gems_util
722
+ gems_util.activate(gem_name, *Array(gem_version))
723
+ gem_spec = ::Gem.loaded_specs[gem_name]
724
+ raise LoaderError, "Unable to find gem #{gem_name}" unless gem_spec&.gem_dir
725
+ gem_toys_dir ||= gem_spec.metadata["toys_dir"] || "toys"
726
+ gem_path = gem_path ? ::File.join(gem_toys_dir, gem_path) : gem_toys_dir
727
+ path = ::File.join(gem_spec.gem_dir, gem_path)
728
+ [gem_spec.version, gem_path, path]
729
+ end
730
+
643
731
  ##
644
732
  # Return a snapshot of all the current tool definitions that have been
645
733
  # loaded. No additional loading is done. The returned array is not in any
@@ -204,7 +204,7 @@ module Toys
204
204
  else
205
205
  klass = @name
206
206
  end
207
- Compat.instantiate(klass, @args, @kwargs, @block)
207
+ klass.new(*@args, **@kwargs, &@block)
208
208
  end
209
209
 
210
210
  ##
data/lib/toys/settings.rb CHANGED
@@ -769,10 +769,19 @@ module Toys
769
769
  attr_reader :default
770
770
  attr_reader :group_class
771
771
 
772
+ ##
773
+ # @return [boolean] Whether the field is a group
774
+ #
772
775
  def group?
773
776
  !@group_class.nil?
774
777
  end
775
778
 
779
+ ##
780
+ # Validate the given value.
781
+ #
782
+ # @return [Object] The validated value
783
+ # @raise [FieldError] If the value cannot be validated
784
+ #
776
785
  def validate(value)
777
786
  validated_value = @type.call(value)
778
787
  if validated_value == ILLEGAL_VALUE
@@ -877,8 +886,8 @@ module Toys
877
886
  raise ::ArgumentError, "Illegal settings field name: #{name}"
878
887
  end
879
888
  existing = public_instance_methods(false)
880
- if existing.include?(name.to_sym) || existing.include?("#{name}=".to_sym) ||
881
- existing.include?("#{name}_set?".to_sym) || existing.include?("#{name}_unset!".to_sym)
889
+ if existing.include?(name.to_sym) || existing.include?(:"#{name}=") ||
890
+ existing.include?(:"#{name}_set?") || existing.include?(:"#{name}_unset!")
882
891
  raise ::ArgumentError, "Settings field already exists: #{name}"
883
892
  end
884
893
  name.to_sym
@@ -11,6 +11,7 @@ module Toys
11
11
  # * A toys directory
12
12
  # * A single toys file
13
13
  # * A file or directory loaded from git
14
+ # * A file or directory loaded from a gem
14
15
  # * A config block passed directly to the CLI
15
16
  # * A tool block within a toys file
16
17
  #
@@ -143,6 +144,32 @@ module Toys
143
144
  #
144
145
  attr_reader :git_commit
145
146
 
147
+ ##
148
+ # The gem name. This is set if the source, or one of its ancestors, comes
149
+ # from a gem.
150
+ #
151
+ # @return [String] The gem name.
152
+ # @return [nil] if this source is not from a gem.
153
+ #
154
+ attr_reader :gem_name
155
+
156
+ ##
157
+ # The gem version. This is set if the source, or one of its ancestors,
158
+ # comes from a gem.
159
+ #
160
+ # @return [Gem::Version] The gem version.
161
+ # @return [nil] if this source is not from a gem.
162
+ #
163
+ attr_reader :gem_version
164
+
165
+ ##
166
+ # The path within the gem, including the toys root directory in the gem.
167
+ #
168
+ # @return [String] The path.
169
+ # @return [nil] if this source is not from a gem.
170
+ #
171
+ attr_reader :gem_path
172
+
146
173
  ##
147
174
  # A user-visible name of this source.
148
175
  #
@@ -191,8 +218,10 @@ module Toys
191
218
  #
192
219
  # @private This interface is internal and subject to change without warning.
193
220
  #
194
- def initialize(parent, priority, context_directory, source_type, source_path, source_proc,
195
- git_remote, git_path, git_commit, source_name, data_dir_name, lib_dir_name)
221
+ def initialize(parent, priority, context_directory,
222
+ source_type, source_path, source_proc,
223
+ git_remote, git_path, git_commit, gem_name, gem_version, gem_path,
224
+ source_name, data_dir_name, lib_dir_name)
196
225
  @parent = parent
197
226
  @root = parent&.root || self
198
227
  @priority = priority
@@ -204,7 +233,10 @@ module Toys
204
233
  @git_remote = git_remote
205
234
  @git_path = git_path
206
235
  @git_commit = git_commit
207
- @source_name = source_name
236
+ @gem_name = gem_name
237
+ @gem_version = gem_version
238
+ @gem_path = gem_path
239
+ @source_name = source_name || default_source_name
208
240
  @data_dir_name = data_dir_name
209
241
  @lib_dir_name = lib_dir_name
210
242
  @data_dir = find_special_dir(data_dir_name)
@@ -220,18 +252,12 @@ module Toys
220
252
  unless source_type == :directory
221
253
  raise LoaderError, "relative_child is valid only on a directory source"
222
254
  end
223
- child_path = ::File.join(source_path, filename)
224
- child_path, type = SourceInfo.check_path(child_path, true)
255
+ child_path, type = SourceInfo.check_path(::File.join(source_path, filename), true)
225
256
  return nil unless child_path
226
- child_git_path = ::File.join(git_path, filename) if git_path
227
- source_name ||=
228
- if git_path
229
- "git(remote=#{git_remote} path=#{child_git_path} commit=#{git_commit})"
230
- else
231
- child_path
232
- end
257
+ child_git_path = git_path.empty? ? filename : ::File.join(git_path, filename) if git_path
258
+ child_gem_path = gem_path.empty? ? filename : ::File.join(gem_path, filename) if gem_path
233
259
  SourceInfo.new(self, priority, context_directory, type, child_path, nil,
234
- git_remote, child_git_path, git_commit,
260
+ git_remote, child_git_path, git_commit, gem_name, gem_version, child_gem_path,
235
261
  source_name, @data_dir_name, @lib_dir_name)
236
262
  end
237
263
 
@@ -242,8 +268,8 @@ module Toys
242
268
  #
243
269
  def absolute_child(child_path, source_name: nil)
244
270
  child_path, type = SourceInfo.check_path(child_path, false)
245
- source_name ||= child_path
246
- SourceInfo.new(self, priority, context_directory, type, child_path, nil, nil, nil, nil,
271
+ SourceInfo.new(self, priority, context_directory, type, child_path, nil,
272
+ nil, nil, nil, nil, nil, nil,
247
273
  source_name, @data_dir_name, @lib_dir_name)
248
274
  end
249
275
 
@@ -252,13 +278,22 @@ module Toys
252
278
  #
253
279
  # @private This interface is internal and subject to change without warning.
254
280
  #
255
- def git_child(child_git_remote, child_git_path, child_git_commit, child_path,
256
- source_name: nil)
281
+ def git_child(child_git_remote, child_git_path, child_git_commit, child_path, source_name: nil)
282
+ child_path, type = SourceInfo.check_path(child_path, false)
283
+ SourceInfo.new(self, priority, context_directory, type, child_path, nil,
284
+ child_git_remote, child_git_path, child_git_commit, nil, nil, nil,
285
+ source_name, @data_dir_name, @lib_dir_name)
286
+ end
287
+
288
+ ##
289
+ # Create a child SourceInfo with a gem source.
290
+ #
291
+ # @private This interface is internal and subject to change without warning.
292
+ #
293
+ def gem_child(child_gem_name, child_gem_version, child_gem_path, child_path, source_name: nil)
257
294
  child_path, type = SourceInfo.check_path(child_path, false)
258
- source_name ||=
259
- "git(remote=#{child_git_remote} path=#{child_git_path} commit=#{child_git_commit})"
260
295
  SourceInfo.new(self, priority, context_directory, type, child_path, nil,
261
- child_git_remote, child_git_path, child_git_commit,
296
+ nil, nil, nil, child_gem_name, child_gem_version, child_gem_path,
262
297
  source_name, @data_dir_name, @lib_dir_name)
263
298
  end
264
299
 
@@ -270,7 +305,7 @@ module Toys
270
305
  def proc_child(child_proc, source_name: nil)
271
306
  source_name ||= self.source_name
272
307
  SourceInfo.new(self, priority, context_directory, :proc, source_path, child_proc,
273
- git_remote, git_path, git_commit,
308
+ git_remote, git_path, git_commit, gem_name, gem_version, gem_path,
274
309
  source_name, @data_dir_name, @lib_dir_name)
275
310
  end
276
311
 
@@ -291,8 +326,8 @@ module Toys
291
326
  when :path
292
327
  context_directory = source_path
293
328
  end
294
- source_name ||= source_path
295
- new(nil, priority, context_directory, type, source_path, nil, nil, nil, nil,
329
+ new(nil, priority, context_directory, type, source_path, nil,
330
+ nil, nil, nil, nil, nil, nil,
296
331
  source_name, data_dir_name, lib_dir_name)
297
332
  end
298
333
 
@@ -307,9 +342,25 @@ module Toys
307
342
  lib_dir_name: nil,
308
343
  source_name: nil)
309
344
  source_path, type = check_path(source_path, false)
310
- source_name ||= "git(remote=#{git_remote} path=#{git_path} commit=#{git_commit})"
311
- new(nil, priority, context_directory, type, source_path, nil, git_remote,
312
- git_path, git_commit, source_name, data_dir_name, lib_dir_name)
345
+ new(nil, priority, context_directory, type, source_path, nil,
346
+ git_remote, git_path, git_commit, nil, nil, nil,
347
+ source_name, data_dir_name, lib_dir_name)
348
+ end
349
+
350
+ ##
351
+ # Create a root source info for a loaded gem.
352
+ #
353
+ # @private This interface is internal and subject to change without warning.
354
+ #
355
+ def self.create_gem_root(gem_name, gem_version, gem_path, source_path, priority,
356
+ context_directory: nil,
357
+ data_dir_name: nil,
358
+ lib_dir_name: nil,
359
+ source_name: nil)
360
+ source_path, type = check_path(source_path, false)
361
+ new(nil, priority, context_directory, type, source_path, nil,
362
+ nil, nil, nil, gem_name, gem_version, gem_path,
363
+ source_name, data_dir_name, lib_dir_name)
313
364
  end
314
365
 
315
366
  ##
@@ -322,9 +373,9 @@ module Toys
322
373
  data_dir_name: nil,
323
374
  lib_dir_name: nil,
324
375
  source_name: nil)
325
- source_name ||= "(code block #{source_proc.object_id})"
326
- new(nil, priority, context_directory, :proc, nil, source_proc, nil, nil,
327
- nil, source_name, data_dir_name, lib_dir_name)
376
+ new(nil, priority, context_directory, :proc, nil, source_proc,
377
+ nil, nil, nil, nil, nil, nil,
378
+ source_name, data_dir_name, lib_dir_name)
328
379
  end
329
380
 
330
381
  ##
@@ -354,6 +405,18 @@ module Toys
354
405
 
355
406
  private
356
407
 
408
+ def default_source_name
409
+ if @git_remote
410
+ "git(remote=#{@git_remote} path=#{@git_path} commit=#{@git_commit})"
411
+ elsif @gem_name
412
+ "gem(name=#{@gem_name} version=#{@gem_version} path=#{@gem_path})"
413
+ elsif @source_type == :proc
414
+ "(code block #{@source_proc.object_id})"
415
+ else
416
+ @source_path
417
+ end
418
+ end
419
+
357
420
  def find_special_dir(dir_name)
358
421
  return nil if @source_type != :directory || dir_name.nil?
359
422
  dir = ::File.join(@source_path, dir_name)
@@ -22,8 +22,8 @@ module Toys
22
22
  #
23
23
  # tool "my_tool" do
24
24
  # include :gems
25
- # gem "nokogiri", "~> 1.15"
26
25
  # def run
26
+ # gem "nokogiri", "~> 1.15"
27
27
  # # Do stuff with Nokogiri
28
28
  # end
29
29
  # end
@@ -33,6 +33,18 @@ module Toys
33
33
  #
34
34
  # include :gems, on_missing: :error
35
35
  #
36
+ # You can also pass options to the {#gem} mixin method itself:
37
+ #
38
+ # tool "my_tool" do
39
+ # include :gems
40
+ # def run
41
+ # # If the gem is not installed, error out instead of asking to
42
+ # # install it.
43
+ # gem "nokogiri", "~> 1.15", on_missing: :error
44
+ # # Do stuff with Nokogiri
45
+ # end
46
+ # end
47
+ #
36
48
  # See {Toys::Utils::Gems#initialize} for a list of supported options.
37
49
  #
38
50
  module Gems
@@ -54,8 +66,8 @@ module Toys
54
66
  # @param requirements [String...] Version requirements
55
67
  # @return [void]
56
68
  #
57
- def gem(name, *requirements)
58
- self.class.gems.activate(name, *requirements)
69
+ def gem(name, *requirements, **options)
70
+ self.class.gem(name, *requirements, **options)
59
71
  end
60
72
 
61
73
  on_include do |**opts|
@@ -76,8 +88,15 @@ module Toys
76
88
  ##
77
89
  # @private
78
90
  #
79
- def self.gem(name, *requirements)
80
- gems.activate(name, *requirements)
91
+ def self.gem(name, *requirements, **options)
92
+ gems_util =
93
+ if options.empty?
94
+ gems
95
+ else
96
+ require "toys/utils/gems"
97
+ Utils::Gems.new(**options)
98
+ end
99
+ gems_util.activate(name, *requirements)
81
100
  end
82
101
  end
83
102
  end
@@ -48,43 +48,43 @@ module Toys
48
48
  ##
49
49
  # Calls [HighLine#agree](https://www.rubydoc.info/gems/highline/HighLine:agree)
50
50
  #
51
- def agree(*args, &block)
52
- self[KEY].agree(*args, &block)
51
+ def agree(...)
52
+ self[KEY].agree(...)
53
53
  end
54
54
 
55
55
  ##
56
56
  # Calls [HighLine#ask](https://www.rubydoc.info/gems/highline/HighLine:ask)
57
57
  #
58
- def ask(*args, &block)
59
- self[KEY].ask(*args, &block)
58
+ def ask(...)
59
+ self[KEY].ask(...)
60
60
  end
61
61
 
62
62
  ##
63
63
  # Calls [HighLine#choose](https://www.rubydoc.info/gems/highline/HighLine:choose)
64
64
  #
65
- def choose(*args, &block)
66
- self[KEY].choose(*args, &block)
65
+ def choose(...)
66
+ self[KEY].choose(...)
67
67
  end
68
68
 
69
69
  ##
70
70
  # Calls [HighLine#list](https://www.rubydoc.info/gems/highline/HighLine:list)
71
71
  #
72
- def list(*args, &block)
73
- self[KEY].list(*args, &block)
72
+ def list(...)
73
+ self[KEY].list(...)
74
74
  end
75
75
 
76
76
  ##
77
77
  # Calls [HighLine#say](https://www.rubydoc.info/gems/highline/HighLine:say)
78
78
  #
79
- def say(*args, &block)
80
- self[KEY].say(*args, &block)
79
+ def say(...)
80
+ self[KEY].say(...)
81
81
  end
82
82
 
83
83
  ##
84
84
  # Calls [HighLine#indent](https://www.rubydoc.info/gems/highline/HighLine:indent)
85
85
  #
86
- def indent(*args, &block)
87
- self[KEY].indent(*args, &block)
86
+ def indent(...)
87
+ self[KEY].indent(...)
88
88
  end
89
89
 
90
90
  ##
@@ -552,9 +552,8 @@ module Toys
552
552
  # @return [true,false]
553
553
  #
554
554
  def runnable?
555
- @run_handler.is_a?(::Symbol) &&
556
- tool_class.public_instance_methods(false).include?(@run_handler) ||
557
- @run_handler.is_a?(::Proc)
555
+ @run_handler.is_a?(::Proc) ||
556
+ (@run_handler.is_a?(::Symbol) && tool_class.public_instance_methods(false).include?(@run_handler))
558
557
  end
559
558
 
560
559
  ##
@@ -608,8 +607,7 @@ module Toys
608
607
  # @return [true,false]
609
608
  #
610
609
  def includes_arguments?
611
- !default_data.empty? || !flags.empty? ||
612
- !required_args.empty? || !optional_args.empty? ||
610
+ !flags.empty? || !required_args.empty? || !optional_args.empty? ||
613
611
  !remaining_arg.nil? || flags_before_args_enforced?
614
612
  end
615
613
 
@@ -619,7 +617,7 @@ module Toys
619
617
  #
620
618
  def includes_definition?
621
619
  includes_arguments? || runnable? || argument_parsing_disabled? ||
622
- includes_modules? || includes_description?
620
+ includes_modules? || includes_description? || !default_data.empty?
623
621
  end
624
622
 
625
623
  ##
@@ -690,7 +688,7 @@ module Toys
690
688
  # @return [nil] if no acceptor of the given name is found.
691
689
  #
692
690
  def lookup_acceptor(name)
693
- @acceptors.fetch(name.to_s) { |k| @parent ? @parent.lookup_acceptor(k) : nil }
691
+ @acceptors.fetch(name.to_s) { |k| @parent&.lookup_acceptor(k) }
694
692
  end
695
693
 
696
694
  ##
@@ -701,7 +699,7 @@ module Toys
701
699
  # @return [nil] if no template of the given name is found.
702
700
  #
703
701
  def lookup_template(name)
704
- @templates.fetch(name.to_s) { |k| @parent ? @parent.lookup_template(k) : nil }
702
+ @templates.fetch(name.to_s) { |k| @parent&.lookup_template(k) }
705
703
  end
706
704
 
707
705
  ##
@@ -712,7 +710,7 @@ module Toys
712
710
  # @return [nil] if no mixin of the given name is found.
713
711
  #
714
712
  def lookup_mixin(name)
715
- @mixins.fetch(name.to_s) { |k| @parent ? @parent.lookup_mixin(k) : nil }
713
+ @mixins.fetch(name.to_s) { |k| @parent&.lookup_mixin(k) }
716
714
  end
717
715
 
718
716
  ##
@@ -723,7 +721,7 @@ module Toys
723
721
  # @return [nil] if no completion of the given name is found.
724
722
  #
725
723
  def lookup_completion(name)
726
- @completions.fetch(name.to_s) { |k| @parent ? @parent.lookup_completion(k) : nil }
724
+ @completions.fetch(name.to_s) { |k| @parent&.lookup_completion(k) }
727
725
  end
728
726
 
729
727
  ##
@@ -1014,15 +1012,16 @@ module Toys
1014
1012
  # @param default [Object] The default value. This is the value that will
1015
1013
  # be set in the context if this flag is not provided on the command
1016
1014
  # line. Defaults to `nil`.
1017
- # @param handler [Proc,nil,:set,:push] An optional handler for
1018
- # setting/updating the value. A handler is a proc taking two
1019
- # arguments, the given value and the previous value, returning the
1020
- # new value that should be set. You may also specify a predefined
1021
- # named handler. The `:set` handler (the default) replaces the
1022
- # previous value (effectively `-> (val, _prev) { val }`). The
1023
- # `:push` handler expects the previous value to be an array and
1024
- # pushes the given value onto it; it should be combined with setting
1025
- # `default: []` and is intended for "multi-valued" flags.
1015
+ # @param handler [Proc,nil,:set,:push] An optional handler that customizes
1016
+ # how a value is set or updated. A handler is a proc that takes up to
1017
+ # three arguments: the given value, the previous value, and a hash
1018
+ # containing all the data collected so far during argument parsing. It
1019
+ # must return the new value that should be set. You may also specify a
1020
+ # predefined named handler. The `:set` handler (the default) replaces
1021
+ # the previous value (effectively `-> (val) { val }`). The `:push`
1022
+ # handler expects the previous value to be an array and pushes the
1023
+ # given value onto it; it should be combined with setting `default: []`
1024
+ # and is intended for "multi-valued" flags.
1026
1025
  # @param complete_flags [Object] A specifier for shell tab completion
1027
1026
  # for flag names associated with this flag. By default, a
1028
1027
  # {Toys::Flag::DefaultCompletion} is used, which provides the flag's
@@ -1347,15 +1346,10 @@ module Toys
1347
1346
  "Cannot delegate tool #{display_name.inspect} to #{target.join(' ')} because it" \
1348
1347
  " already delegates to \"#{@delegate_target.join(' ')}\"."
1349
1348
  end
1350
- if includes_arguments?
1351
- raise ToolDefinitionError,
1352
- "Cannot delegate tool #{display_name.inspect} because" \
1353
- " arguments have already been defined."
1354
- end
1355
- if runnable?
1349
+ if includes_arguments? || runnable? || includes_modules? || !default_data.empty?
1356
1350
  raise ToolDefinitionError,
1357
1351
  "Cannot delegate tool #{display_name.inspect} because" \
1358
- " the run method has already been defined."
1352
+ " some implementation has already been created for it."
1359
1353
  end
1360
1354
  disable_argument_parsing
1361
1355
  self.run_handler = make_delegation_run_handler(target)
@@ -1393,7 +1387,7 @@ module Toys
1393
1387
  def finish_definition(loader)
1394
1388
  unless @definition_finished
1395
1389
  ContextualError.capture("Error installing tool middleware!", tool_name: full_name) do
1396
- config_proc = proc { nil }
1390
+ config_proc = proc {}
1397
1391
  @built_middleware.reverse_each do |middleware|
1398
1392
  config_proc = make_config_proc(middleware, loader, config_proc)
1399
1393
  end
@@ -1493,7 +1487,7 @@ module Toys
1493
1487
  case signal
1494
1488
  when ::String, ::Symbol
1495
1489
  sigstr = signal.to_s
1496
- sigstr = sigstr[3..-1] if sigstr.start_with?("SIG")
1490
+ sigstr = sigstr[3..] if sigstr.start_with?("SIG")
1497
1491
  signo = ::Signal.list[sigstr]
1498
1492
  return signo if signo
1499
1493
  when ::Integer