toys-core 0.20.0 → 0.21.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f391c9aad35948c882c03809a80079b592c59208547080ae68bf2e0052896f9c
4
- data.tar.gz: 4aacba23fe5a5a1560b4f38c6a109e5af7b6c98b9777b0c1ce3906bd6e42810c
3
+ metadata.gz: f2805bf71091b4ea8bf6f6a670281171dc6a5df4edec5196d80d64124ce2a199
4
+ data.tar.gz: b5c860449af3a0566e9b1fe8dc527cba4df6750ccc3b259365faf99fa79682fb
5
5
  SHA512:
6
- metadata.gz: c8a4e6ec594002577f1e81b937c1fdd23aed6ffe9adb2535073eba4d2eabe1c0e9e1e0062ea135062e7169734f65f20d0c9a3138dceca4b514ad86a67c215016
7
- data.tar.gz: c00f4cf11e0af3807cdc0559275ab79f3af4f1fe1eff83cedcda125eedabc04f7ec439d95fa65b7f3763ca86b10e30fd6a5cf8d6412f02cd2e8dae9170d9e7fd
6
+ metadata.gz: 95490a978d1a3ae04be668a2fd318de2499e9610bb6072a053776fb47214d2444c0d93a73fa690b46a6d4706e2fec1f20b76f8d111100ec65216f0c274efe77a
7
+ data.tar.gz: b9421ebfa989c45444024222cc3576a166d5797d7c982033265a11aa149c71ef0ab069441c70da490819e5bbafe95eb017c92289d6b46d7917c19634ee3eaf64
data/CHANGELOG.md CHANGED
@@ -1,5 +1,36 @@
1
1
  # Release History
2
2
 
3
+ ### v0.21.0 / 2026-03-23
4
+
5
+ This release includes a variety of small fixes and updates toward improving product polish in preparation for a 1.0 release. It focuses on the following areas:
6
+
7
+ * Updates to the help system
8
+ * FIXED / BREAKING CHANGE: The help middleware omits the subtool filter flags on runnable tools that have subtools
9
+ * Updates to bundler integration
10
+ * FIXED: Fixed some cleanup issues when bundler setup fails
11
+ * Updates to the exec utility
12
+ * ADDED: Support for the `:unbundle` option which removes any existing bundle for a subprocess
13
+ * FIXED: Fixed several thread-safety issues with the controller
14
+ * FIXED: The result callback is now called in background mode if the result is never explicitly obtained from the controller
15
+ * Updates to the git_cache utility
16
+ * ADDED: Support for filtering lookups of ref and source info
17
+ * ADDED: Support for SHA-256 repos
18
+ * FIXED: Fixed failure to get a parent directory after previously getting a descendant object
19
+ * FIXED: GitCache no longer unnecessarily sets the write bit on files copied into a specified directory
20
+ * FIXED: GitCache no longer cleans out existing files when writing to a shared source, which should reduce issues with concurrent clients
21
+ * Updates to the XDG utility
22
+ * FIXED / BREAKING CHANGE: The XDG utility returns an empty array (instead of the system defaults) from the corresponding methods if `XDG_DATA_DIRS` or `XDG_CONFIG_DIRS` is nonempty but contains only relative paths
23
+ * ADDED: Provided `lookup_state` and `lookup_cache` methods on the XDG utility
24
+ * Updates to the settings system:
25
+ * FIXED: Block certain reserved names from being used as Settings field names
26
+ * FIXED: Fixed `Settings#load_data!` when subclassing another settings class
27
+ * FIXED: Reject non-String values in Settings regexp type spec
28
+ * FIXED: Settings guards against unknown classes when loading YAML on older Ruby versions
29
+ * FIXED: Settings guards against `ILLEGAL_VALUE` being passed to `range.member?`
30
+ * FIXED: Settings does a better job choosing exact-match values when using a union type
31
+ * FIXED: Settings reject non-numeric strings in Integer and Float converters
32
+ * FIXED: Settings propagates nested group errors in `Settings#load_data!`
33
+
3
34
  ### v0.20.0 / 2026-03-09
4
35
 
5
36
  Toys-core 0.20 is a major release with several new features and a number of fixes, including a few minor breaking changes.
data/lib/toys/core.rb CHANGED
@@ -9,7 +9,7 @@ module Toys
9
9
  # Current version of Toys core.
10
10
  # @return [String]
11
11
  #
12
- VERSION = "0.20.0"
12
+ VERSION = "0.21.0"
13
13
  end
14
14
 
15
15
  ##
data/lib/toys/settings.rb CHANGED
@@ -35,8 +35,12 @@ module Toys
35
35
  # Attribute names must start with an ascii letter, and may contain only ascii
36
36
  # letters, digits, and underscores. Unlike method names, they may not include
37
37
  # non-ascii unicode characters, nor may they end with `!` or `?`.
38
- # Additionally, the name `method_missing` is not allowed because of its
39
- # special behavior in Ruby.
38
+ # Additionally, some names are reserved because they would shadow critical
39
+ # Ruby methods or interfere with Settings' internal behavior (see
40
+ # {Toys::Settings::RESERVED_FIELD_NAMES} for the complete list, which
41
+ # currently includes names such as `class`, `clone`, `dup`, `freeze`, `hash`,
42
+ # `initialize`, `method_missing`, `object_id`, `public_send`, `raise`,
43
+ # `require`, and `send`).
40
44
  #
41
45
  # Each attribute defines four methods: a getter, a setter, an unsetter, and a
42
46
  # set detector. In the above example, the attribute named `:endpoint` creates
@@ -297,12 +301,27 @@ module Toys
297
301
  # grandchild_settings.str # => "value_from_root"
298
302
  #
299
303
  class Settings
304
+ ##
300
305
  # A special value indicating a type check failure.
306
+ #
301
307
  ILLEGAL_VALUE = ::Object.new.freeze
302
308
 
309
+ ##
303
310
  # A special type specification indicating infer from the default value.
311
+ #
304
312
  DEFAULT_TYPE = ::Object.new.freeze
305
313
 
314
+ ##
315
+ # Field names that are not allowed because they would shadow critical Ruby
316
+ # methods or break Settings' own internal method calls.
317
+ #
318
+ # @return [Array<String>]
319
+ #
320
+ RESERVED_FIELD_NAMES = [
321
+ "class", "clone", "dup", "freeze", "hash", "initialize",
322
+ "method_missing", "object_id", "public_send", "raise", "require", "send"
323
+ ].freeze
324
+
306
325
  ##
307
326
  # Error raised when a value does not match the type constraint.
308
327
  #
@@ -447,15 +466,14 @@ module Toys
447
466
  range_class = (range.begin || range.end).class
448
467
  new("(#{range})") do |val|
449
468
  converted = convert(val, range_class)
450
- range.member?(converted) ? converted : ILLEGAL_VALUE
469
+ converted != ILLEGAL_VALUE && range.member?(converted) ? converted : ILLEGAL_VALUE
451
470
  end
452
471
  end
453
472
 
454
473
  def for_regexp(regexp)
455
474
  regexp_str = regexp.source.gsub("/", "\\/")
456
475
  new("/#{regexp_str}/") do |val|
457
- str = val.to_s
458
- regexp.match(str) ? str : ILLEGAL_VALUE
476
+ val.is_a?(::String) && regexp.match(val) ? val : ILLEGAL_VALUE
459
477
  end
460
478
  end
461
479
 
@@ -466,7 +484,7 @@ module Toys
466
484
  result = ILLEGAL_VALUE
467
485
  types.each do |type|
468
486
  converted = type.call(val)
469
- if converted == val
487
+ if converted.eql?(val)
470
488
  result = val
471
489
  break
472
490
  elsif result == ILLEGAL_VALUE
@@ -518,7 +536,7 @@ module Toys
518
536
  float_converter = proc do |val|
519
537
  case val
520
538
  when ::String
521
- val.to_f
539
+ Float(val)
522
540
  when ::Numeric
523
541
  converted = val.to_f
524
542
  converted == val ? converted : ILLEGAL_VALUE
@@ -530,7 +548,7 @@ module Toys
530
548
  integer_converter = proc do |val|
531
549
  case val
532
550
  when ::String
533
- val.to_i
551
+ Integer(val)
534
552
  when ::Numeric
535
553
  converted = val.to_i
536
554
  converted == val ? converted : ILLEGAL_VALUE
@@ -605,7 +623,7 @@ module Toys
605
623
  raise FieldError.new(value, self.class, name, nil) unless field
606
624
  if field.group?
607
625
  raise FieldError.new(value, self.class, name, "Hash") unless value.is_a?(::Hash)
608
- get!(field).load_data!(value)
626
+ errors.concat(get!(field).load_data!(value, raise_on_failure: raise_on_failure))
609
627
  else
610
628
  set!(field, value)
611
629
  end
@@ -628,7 +646,7 @@ module Toys
628
646
  #
629
647
  def load_yaml!(str, raise_on_failure: false)
630
648
  require "psych"
631
- load_data!(::Psych.load(str), raise_on_failure: raise_on_failure)
649
+ load_data!(::Psych.safe_load(str, permitted_classes: [::Symbol]), raise_on_failure: raise_on_failure)
632
650
  end
633
651
 
634
652
  ##
@@ -641,7 +659,7 @@ module Toys
641
659
  # @return [Array<FieldError>] An array of errors.
642
660
  #
643
661
  def load_yaml_file!(filename, raise_on_failure: false)
644
- load_yaml!(File.read(filename), raise_on_failure: raise_on_failure)
662
+ load_yaml!(::File.read(filename), raise_on_failure: raise_on_failure)
645
663
  end
646
664
 
647
665
  ##
@@ -668,7 +686,7 @@ module Toys
668
686
  # @return [Array<FieldError>] An array of errors.
669
687
  #
670
688
  def load_json_file!(filename, raise_on_failure: false, **json_opts)
671
- load_json!(File.read(filename), raise_on_failure: raise_on_failure, **json_opts)
689
+ load_json!(::File.read(filename), raise_on_failure: raise_on_failure, **json_opts)
672
690
  end
673
691
 
674
692
  ##
@@ -848,7 +866,12 @@ module Toys
848
866
  # all its instances.
849
867
  #
850
868
  def fields
851
- @fields ||= {}
869
+ @fields ||=
870
+ if superclass.respond_to?(:fields)
871
+ superclass.fields.dup
872
+ else
873
+ {}
874
+ end
852
875
  end
853
876
 
854
877
  ##
@@ -882,7 +905,7 @@ module Toys
882
905
 
883
906
  def interpret_name(name)
884
907
  name = name.to_s
885
- if name !~ /^[a-zA-Z]\w*$/ || name == "method_missing"
908
+ if name !~ /^[a-zA-Z]\w*$/ || RESERVED_FIELD_NAMES.include?(name)
886
909
  raise ::ArgumentError, "Illegal settings field name: #{name}"
887
910
  end
888
911
  existing = public_instance_methods(false)
@@ -53,43 +53,43 @@ module Toys
53
53
  # Key set when the show help flag is present
54
54
  # @return [Object]
55
55
  #
56
- SHOW_HELP_KEY = Object.new.freeze
56
+ SHOW_HELP_KEY = ::Object.new.freeze
57
57
 
58
58
  ##
59
59
  # Key set when the show usage flag is present
60
60
  # @return [Object]
61
61
  #
62
- SHOW_USAGE_KEY = Object.new.freeze
62
+ SHOW_USAGE_KEY = ::Object.new.freeze
63
63
 
64
64
  ##
65
65
  # Key set when the show subtool list flag is present
66
66
  # @return [Object]
67
67
  #
68
- SHOW_LIST_KEY = Object.new.freeze
68
+ SHOW_LIST_KEY = ::Object.new.freeze
69
69
 
70
70
  ##
71
71
  # Key for the recursive setting
72
72
  # @return [Object]
73
73
  #
74
- RECURSIVE_SUBTOOLS_KEY = Object.new.freeze
74
+ RECURSIVE_SUBTOOLS_KEY = ::Object.new.freeze
75
75
 
76
76
  ##
77
77
  # Key for the search string
78
78
  # @return [Object]
79
79
  #
80
- SEARCH_STRING_KEY = Object.new.freeze
80
+ SEARCH_STRING_KEY = ::Object.new.freeze
81
81
 
82
82
  ##
83
83
  # Key for the show-all-subtools setting
84
84
  # @return [Object]
85
85
  #
86
- SHOW_ALL_SUBTOOLS_KEY = Object.new.freeze
86
+ SHOW_ALL_SUBTOOLS_KEY = ::Object.new.freeze
87
87
 
88
88
  ##
89
89
  # Key for the tool name
90
90
  # @return [Object]
91
91
  #
92
- TOOL_NAME_KEY = Object.new.freeze
92
+ TOOL_NAME_KEY = ::Object.new.freeze
93
93
 
94
94
  ##
95
95
  # Create a ShowHelp middleware.
@@ -214,7 +214,7 @@ module Toys
214
214
  list_flags = has_subtools ? add_list_flags(tool) : []
215
215
  can_display_help = !help_flags.empty? || !list_flags.empty? ||
216
216
  !usage_flags.empty? || @fallback_execution
217
- if can_display_help && has_subtools
217
+ if can_display_help && has_subtools && !tool.runnable?
218
218
  add_recursive_flags(tool)
219
219
  add_search_flags(tool)
220
220
  add_show_all_subtools_flags(tool)
@@ -23,7 +23,7 @@ module Toys
23
23
  # Key set when the version flag is present
24
24
  # @return [Object]
25
25
  #
26
- SHOW_VERSION_KEY = Object.new.freeze
26
+ SHOW_VERSION_KEY = ::Object.new.freeze
27
27
 
28
28
  ##
29
29
  # Create a ShowVersion middleware
@@ -23,7 +23,8 @@ module Toys
23
23
  # The following parameters can be passed when including this mixin:
24
24
  #
25
25
  # * `:static` (Boolean) Has the same effect as passing `:static` to the
26
- # `:setup` parameter.
26
+ # `:setup` parameter. This is present largely for historical
27
+ # compatibility, but it is supported and _not_ deprecated.
27
28
  #
28
29
  # * `:setup` (:auto,:manual,:static) A symbol indicating when the bundle
29
30
  # should be installed. Possible values are:
@@ -114,7 +115,7 @@ module Toys
114
115
  # will be a hash of parameters if the bundle has not been set up yet, or
115
116
  # nil if the bundle has already been set up.
116
117
  #
117
- SETUP_PARAMS_KEY = Object.new.freeze
118
+ SETUP_PARAMS_KEY = ::Object.new.freeze
118
119
 
119
120
  ##
120
121
  # @private
@@ -152,6 +153,8 @@ module Toys
152
153
  ::Toys::StandardMixins::Bundler.setup_bundle(context_directory, source_info, **kwargs)
153
154
  when :manual
154
155
  self[::Toys::StandardMixins::Bundler::SETUP_PARAMS_KEY] = kwargs
156
+ when :static
157
+ # Already set up at include time
155
158
  end
156
159
  end
157
160
 
@@ -161,7 +164,7 @@ module Toys
161
164
  when :static
162
165
  ::Toys::StandardMixins::Bundler.setup_bundle(context_directory, source_info, **kwargs)
163
166
  when :manual
164
- # @private
167
+ # @private Defined dynamically for the tool but not visible to YARD
165
168
  def bundler_setup(**kwargs)
166
169
  original_kwargs = self[::Toys::StandardMixins::Bundler::SETUP_PARAMS_KEY]
167
170
  raise ::Toys::Utils::Gems::AlreadyBundledError unless original_kwargs
@@ -172,7 +175,7 @@ module Toys
172
175
  self[::Toys::StandardMixins::Bundler::SETUP_PARAMS_KEY] = nil
173
176
  end
174
177
 
175
- # @private
178
+ # @private Defined dynamically for the tool but not visible to YARD
176
179
  def bundler_setup?
177
180
  self[::Toys::StandardMixins::Bundler::SETUP_PARAMS_KEY].nil?
178
181
  end
@@ -193,7 +196,7 @@ module Toys
193
196
  def initialize(context_directory, source_info, gemfile_names, toys_gemfile_names)
194
197
  @context_directory = context_directory
195
198
  @source_info = source_info
196
- @gemfile_names = gemfile_names
199
+ @gemfile_names = gemfile_names || ::Toys::Utils::Gems::DEFAULT_GEMFILE_NAMES
197
200
  @toys_gemfile_names = toys_gemfile_names || DEFAULT_TOYS_GEMFILE_NAMES
198
201
  end
199
202
 
@@ -35,36 +35,6 @@ module Toys
35
35
  # but you can also retrieve the service object itself by calling
36
36
  # {Toys::Context#get} with the key {Toys::StandardMixins::Exec::KEY}.
37
37
  #
38
- # ### Controlling processes
39
- #
40
- # A process can be started in the *foreground* or the *background*. If you
41
- # start a foreground process, it will "take over" your standard input and
42
- # output streams by default, and it will keep control until it completes.
43
- # If you start a background process, its streams will be redirected to null
44
- # by default, and control will be returned to you immediately.
45
- #
46
- # While a process is running, you can control it using a
47
- # {Toys::Utils::Exec::Controller} object. Use a controller to interact with
48
- # the process's input and output streams, send it signals, or wait for it
49
- # to complete.
50
- #
51
- # When running a process in the foreground, the controller will be yielded
52
- # to an optional block. For example, the following code starts a process in
53
- # the foreground and passes its output stream to a controller.
54
- #
55
- # exec(["git", "init"], out: :controller) do |controller|
56
- # loop do
57
- # line = controller.out.gets
58
- # break if line.nil?
59
- # puts "Got line: #{line}"
60
- # end
61
- # end
62
- #
63
- # When running a process in the background, the controller is returned from
64
- # the method that starts the process:
65
- #
66
- # controller = exec(["git", "init"], background: true)
67
- #
68
38
  # ### Stream handling
69
39
  #
70
40
  # By default, subprocess streams are connected to the corresponding streams
@@ -84,7 +54,7 @@ module Toys
84
54
  #
85
55
  # * **Inherit parent stream:** You can inherit the corresponding stream
86
56
  # in the parent process by passing `:inherit` as the option value. This
87
- # is the default if the subprocess is *not* run in the background.
57
+ # is the default if the subprocess is run in the foreground.
88
58
  #
89
59
  # * **Redirect to null:** You can redirect to a null stream by passing
90
60
  # `:null` as the option value. This connects to a stream that is not
@@ -136,7 +106,7 @@ module Toys
136
106
  # the setting `:controller`. You can then manipulate the stream via the
137
107
  # controller. If you pass a block to {Toys::StandardMixins::Exec#exec},
138
108
  # it yields the {Toys::Utils::Exec::Controller}, giving you access to
139
- # streams.
109
+ # streams. See the section below on controlling processes.
140
110
  #
141
111
  # * **Make copies of an output stream:** You can "tee," or duplicate the
142
112
  # `:out` or `:err` stream and redirect those copies to various
@@ -156,6 +126,68 @@ module Toys
156
126
  # the tee. Larger buffers may allow higher throughput. The default
157
127
  # is 65536.
158
128
  #
129
+ # ### Controlling processes
130
+ #
131
+ # A process can be started in the *foreground* or the *background*. If you
132
+ # start a foreground process, it will inherit your standard input and
133
+ # output streams by default, and it will keep control until it completes.
134
+ # If you start a background process, its streams will be redirected to null
135
+ # by default, and control will be returned to you immediately.
136
+ #
137
+ # While a process is running, you can control it using a
138
+ # {Toys::Utils::Exec::Controller} object. Use a controller to interact with
139
+ # the process's input and output streams, send it signals, or wait for it
140
+ # to complete.
141
+ #
142
+ # When running a process in the foreground, the controller will be yielded
143
+ # to an optional block. For example, the following code starts a process in
144
+ # the foreground and passes its output stream to a controller.
145
+ #
146
+ # exec(["git", "init"], out: :controller) do |controller|
147
+ # loop do
148
+ # line = controller.out.gets
149
+ # break if line.nil?
150
+ # puts "Got line: #{line}"
151
+ # end
152
+ # end
153
+ #
154
+ # At the end of the block, if the controller is handling the process's
155
+ # input stream, that stream will automatically be closed. The following
156
+ # example programmatically sends data to the `wc` unix program, and
157
+ # captures its output. Because the controller is handling the input stream,
158
+ # it automatically closes the stream at the end of the block, which causes
159
+ # `wc` to end.
160
+ #
161
+ # result = exec(["wc"], in: :controller, out: :capture) do |controller|
162
+ # controller.in.puts "Hello, world!"
163
+ # end
164
+ # puts "Results: #{result.captured_out}"
165
+ #
166
+ # Otherwise, depending on the process's behavior, it may continue to run
167
+ # after the end of the block. Control will not be returned to the caller
168
+ # until the process actually terminates. Conversely, it is also possible
169
+ # the process could terminate by itself while the block is still executing.
170
+ # You can call controller methods to obtain the process's actual current
171
+ # state.
172
+ #
173
+ # When running a process in the background, the controller is returned
174
+ # immediately from the method that starts the process. In the following
175
+ # example, git init is kicked off in the background and the output is
176
+ # thrown away to /dev/null.
177
+ #
178
+ # controller = exec(["git", "init"], background: true)
179
+ #
180
+ # In this mode, use the returned controller to query the process's state
181
+ # and interact with it. Streams directed to the controller are not
182
+ # automatically closed, so you will need to do so yourself. Following is an
183
+ # example of running `wc` in the background:
184
+ #
185
+ # controller = exec(["wc"], background: true,
186
+ # in: :controller, out: :controller)
187
+ # controller.in.puts "Hello, world!"
188
+ # controller.in.close # Do this explicitly to cause wc to finish
189
+ # puts "Results: #{controller.out.read}" # Read the entire stream
190
+ #
159
191
  # ### Result handling
160
192
  #
161
193
  # A subprocess result is represented by a {Toys::Utils::Exec::Result}
@@ -193,6 +225,12 @@ module Toys
193
225
  # puts "exit code: #{result.exit_code}"
194
226
  # end
195
227
  #
228
+ # In foreground mode, the callback is executed in the calling thread, after
229
+ # the process terminates (and after any controller block has completed) but
230
+ # before control is returned to the caller. In background mode, the
231
+ # callback is executed asynchronously in a separate thread after the
232
+ # process terminates.
233
+ #
196
234
  # Finally, you can force your tool to exit if a subprocess fails, similar
197
235
  # to setting the `set -e` option in bash, by setting the
198
236
  # `:exit_on_nonzero_status` option. This is often set as a default
@@ -216,6 +254,10 @@ module Toys
216
254
  #
217
255
  # * `:background` (Boolean) Runs the process in the background if `true`.
218
256
  #
257
+ # * `:unbundle` (Boolean) Disables any existing bundle when running the
258
+ # subprocess. Has no effect if Bundler isn't active at the call point.
259
+ # Cannot be used when executing in a fork, e.g. via {#exec_proc}.
260
+ #
219
261
  # Options related to handling results
220
262
  #
221
263
  # * `:result_callback` (Proc,Symbol) A procedure that is called, and
@@ -837,12 +879,7 @@ module Toys
837
879
  context = self
838
880
  opts = Exec._setup_exec_opts(opts, context)
839
881
  context[KEY] = Utils::Exec.new(**opts) do |k|
840
- case k
841
- when :logger
842
- context[Context::Key::LOGGER]
843
- when :cli
844
- context[Context::Key::CLI]
845
- end
882
+ k == :logger ? context[Context::Key::LOGGER] : nil
846
883
  end
847
884
  end
848
885
  end
@@ -10,7 +10,7 @@ module Toys
10
10
  # utility methods that locate base directories and search paths for
11
11
  # application state, configuration, caches, and other data, according to
12
12
  # the [XDG Base Directory Spec version
13
- # 0.8](https://specifications.freedesktop.org/basedir-spec/0.8/).
13
+ # 0.8](https://specifications.freedesktop.org/basedir/0.8/).
14
14
  #
15
15
  # @example
16
16
  #