atli 0.1.9 → 0.1.10

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/thor/base.rb CHANGED
@@ -12,7 +12,6 @@ require 'nrser'
12
12
  # -----------------------------------------------------------------------
13
13
  require "thor/command"
14
14
  require "thor/core_ext/hash_with_indifferent_access"
15
- require "thor/core_ext/ordered_hash"
16
15
  require "thor/error"
17
16
  require "thor/invocation"
18
17
  require "thor/parser"
@@ -21,6 +20,9 @@ require "thor/line_editor"
21
20
  require "thor/util"
22
21
  require 'thor/execution'
23
22
  require 'thor/base/class_methods'
23
+ require 'thor/base/arguments_concern'
24
+ require 'thor/base/shared_options_concern'
25
+ require 'thor/base/shared_concern'
24
26
 
25
27
 
26
28
  # Refinements
@@ -48,8 +50,75 @@ class Thor
48
50
  # Shared base behavior included in {Thor} and {Thor::Group}.
49
51
  #
50
52
  module Base
53
+
54
+ # Module Methods
55
+ # ============================================================================
56
+
57
+ # Hook called when {Thor::Base} is mixed in ({Thor} and {Thor::Group}).
58
+ #
59
+ # Extends `base` with {Thor::Base::ClassMethods}, and includes
60
+ # {Thor::Invocation} and {Thor::Shell} in `base` as well.
61
+ #
62
+ # @param [Module] base
63
+ # Module (or Class) that included {Thor::Base}.
64
+ #
65
+ # @return [void]
66
+ #
67
+ def self.included base
68
+ base.extend ClassMethods
69
+ base.send :include, Invocation
70
+ base.send :include, Shell
71
+ base.send :include, ArgumentsConcern
72
+ base.send :include, SharedOptionsConcern
73
+ base.send :include, SharedConcern
74
+
75
+ base.no_commands {
76
+ base.send :include, NRSER::Log::Mixin
77
+ }
78
+
79
+ end
80
+
81
+ # Returns the classes that inherits from Thor or Thor::Group.
82
+ #
83
+ # ==== Returns
84
+ # Array[Class]
85
+ #
86
+ def self.subclasses
87
+ @subclasses ||= []
88
+ end
89
+
90
+ # Returns the files where the subclasses are kept.
91
+ #
92
+ # ==== Returns
93
+ # Hash[path<String> => Class]
94
+ #
95
+ def self.subclass_files
96
+ @subclass_files ||= Hash.new { |h, k| h[k] = [] }
97
+ end
98
+
99
+ # Whenever a class inherits from Thor or Thor::Group, we should track the
100
+ # class and the file on Thor::Base. This is the method responsible for it.
101
+ #
102
+ def self.register_klass_file(klass) #:nodoc:
103
+ file = caller[1].match(/(.*):\d+/)[1]
104
+ unless Thor::Base.subclasses.include?(klass)
105
+ Thor::Base.subclasses << klass
106
+ end
107
+
108
+ file_subclasses = Thor::Base.subclass_files[File.expand_path(file)]
109
+ file_subclasses << klass unless file_subclasses.include?(klass)
110
+ end
111
+
112
+
113
+ # Attributes
114
+ # ========================================================================
115
+
51
116
  attr_accessor :options, :parent_options, :args
52
117
 
118
+
119
+ # Construction
120
+ # ========================================================================
121
+
53
122
  # It receives arguments in an Array and two hashes, one for options and
54
123
  # other for configuration.
55
124
  #
@@ -67,50 +136,92 @@ class Thor
67
136
  #
68
137
  # config<Hash>:: Configuration for this Thor class.
69
138
  #
70
- def initialize(args = [], local_options = {}, config = {})
71
- logger.debug "#{ self.class.name }#initialize",
72
- args: args,
73
- local_options: local_options
74
-
75
- parse_options = self.class.class_options
76
-
77
- # The start method splits inbound arguments at the first argument
78
- # that looks like an option (starts with - or --). It then calls
79
- # new, passing in the two halves of the arguments Array as the
80
- # first two parameters.
81
-
82
- command_options = config.delete(:command_options) # hook for start
83
- parse_options = parse_options.merge(command_options) if command_options
84
- if local_options.is_a?(Array)
85
- array_options = local_options
86
- hash_options = {}
139
+ def initialize args = [], options_args_or_defaults = {}, config = {}
140
+ # Before {#initialize} is called arguments have been split out into
141
+ # either:
142
+ #
143
+ # 1. Positional `args` and args that need to be parsed for options.
144
+ #
145
+ # 2. Positional `args` and already parsed option values that we call
146
+ # "defaults".
147
+
148
+ if options_args_or_defaults.is_a? Array
149
+ # Case (1)
150
+ #
151
+ # The start method splits inbound arguments at the first argument
152
+ # that looks like an option (starts with - or --). It then calls
153
+ # new, passing in the two halves of the arguments Array as the
154
+ # first two parameters.
155
+ #
156
+ # In this case the first Array stays as `args` and the second Array
157
+ # gets assigned to `args_to_parse_for_options` to be parsed for
158
+ # options:
159
+ #
160
+ args_to_parse_for_options = options_args_or_defaults
161
+ option_defaults = {}
87
162
  else
163
+ # Case (2)
164
+ #
88
165
  # Handle the case where the class was explicitly instantiated
89
166
  # with pre-parsed options.
90
- array_options = []
91
- hash_options = local_options
167
+ #
168
+ args_to_parse_for_options = []
169
+ option_defaults = options_args_or_defaults
92
170
  end
93
171
 
94
- # Let Thor::Options parse the options first, so it can remove
172
+ logger.debug "#{ self.class.name }#initialize",
173
+ args: args,
174
+ options_args_or_defaults: options_args_or_defaults,
175
+ args_to_parse_for_options: args_to_parse_for_options,
176
+ option_defaults: option_defaults
177
+
178
+ # Let {Thor::Options} parse the options first, so it can remove
95
179
  # declared options from the array. This will leave us with
96
180
  # a list of arguments that weren't declared.
181
+
182
+ # Hash of {Thor::Option} that we can parse from the CLI args
183
+ # keyed by their {Thor::Option#name}.
184
+ #
185
+ # @type [HashWithIndifferentAccess<String, Thor::Option>]
186
+ #
187
+ options_to_parse_by_name = [
188
+ # Add class-level options
189
+ self.class.class_options,
190
+
191
+ # Options passed in by {Thor.dispatch} for the command to be
192
+ # invoked. May be `nil`, though I'm not totally sure when... in that
193
+ # case those options won't be parsed.
194
+ #
195
+ # This was
196
+ #
197
+ # config.delete( :command_options ),
198
+ #
199
+ # though I can't figure out why. Specs still pass without it..
200
+ #
201
+ config[:command_options],
202
+ ].compact.reduce( HashWithIndifferentAccess.new, :merge! )
203
+
97
204
  stop_on_unknown = \
98
205
  self.class.stop_on_unknown_option? config[:current_command]
206
+
99
207
  disable_required_check = \
100
208
  self.class.disable_required_check? config[:current_command]
209
+
210
+ logger.trace "Ready to create options",
211
+ options_to_parse_by_name: options_to_parse_by_name,
212
+ option_defaults: option_defaults,
213
+ stop_on_unknown: stop_on_unknown,
214
+ disable_required_check: disable_required_check,
215
+ args: args,
216
+ args_to_parse_for_options: args_to_parse_for_options
101
217
 
102
- logger.debug "Ready to create options",
103
- array_options: array_options,
104
- hash_options: hash_options,
105
- stop_on_unknown: stop_on_unknown,
106
- disable_required_check: disable_required_check
107
-
108
- opts = Thor::Options.new( parse_options,
109
- hash_options,
110
- stop_on_unknown,
111
- disable_required_check )
218
+ options_parser = Thor::Options.new \
219
+ options_to_parse_by_name,
220
+ option_defaults,
221
+ stop_on_unknown,
222
+ disable_required_check
112
223
 
113
- self.options = opts.parse(array_options)
224
+ self.options = options_parser.parse args_to_parse_for_options
114
225
 
115
226
  if config[:class_options]
116
227
  self.options = config[:class_options].merge(options)
@@ -118,7 +229,9 @@ class Thor
118
229
 
119
230
  # If unknown options are disallowed, make sure that none of the
120
231
  # remaining arguments looks like an option.
121
- opts.check_unknown! if self.class.check_unknown_options?(config)
232
+ if self.class.check_unknown_options? config
233
+ options_parser.check_unknown!
234
+ end
122
235
 
123
236
  # Add the remaining arguments from the options parser to the
124
237
  # arguments passed in to initialize. Then remove any positional
@@ -126,16 +239,26 @@ class Thor
126
239
  # by Thor::Group). Tis will leave us with the remaining
127
240
  # positional arguments.
128
241
  to_parse = args
129
- unless self.class.strict_args_position?(config)
130
- to_parse += opts.remaining
242
+ unless self.class.strict_args_position? config
243
+ to_parse += options_parser.remaining
131
244
  end
132
245
 
133
- thor_args = Thor::Arguments.new(self.class.arguments)
134
- thor_args.parse(to_parse).each { |k, v| __send__("#{k}=", v) }
246
+ thor_args = Thor::Arguments.new [
247
+ *self.class.class_arguments,
248
+ *config[:current_command]&.arguments,
249
+ ]
250
+
251
+ # Set the arguments as instance variables.
252
+ thor_args.parse( to_parse ).each { |k, v| __send__ "#{k}=", v }
253
+
254
+ # Set whatever is left as args
135
255
  @args = thor_args.remaining
136
256
  end
137
257
 
138
258
 
259
+ # Instance Methods
260
+ # ========================================================================
261
+
139
262
  protected
140
263
  # ========================================================================
141
264
 
@@ -202,60 +325,5 @@ class Thor
202
325
 
203
326
  # end protected
204
327
 
205
-
206
- # Module Methods
207
- # ============================================================================
208
-
209
- # Hook called when {Thor::Base} is mixed in ({Thor} and {Thor::Group}).
210
- #
211
- # Extends `base` with {Thor::Base::ClassMethods}, and includes
212
- # {Thor::Invocation} and {Thor::Shell} in `base` as well.
213
- #
214
- # @param [Module] base
215
- # Module (or Class) that included {Thor::Base}.
216
- #
217
- # @return [void]
218
- #
219
- def self.included base
220
- base.extend ClassMethods
221
- base.send :include, Invocation
222
- base.send :include, Shell
223
-
224
- base.no_commands {
225
- base.send :include, SemanticLogger::Loggable
226
- }
227
-
228
- end
229
-
230
- # Returns the classes that inherits from Thor or Thor::Group.
231
- #
232
- # ==== Returns
233
- # Array[Class]
234
- #
235
- def self.subclasses
236
- @subclasses ||= []
237
- end
238
-
239
- # Returns the files where the subclasses are kept.
240
- #
241
- # ==== Returns
242
- # Hash[path<String> => Class]
243
- #
244
- def self.subclass_files
245
- @subclass_files ||= Hash.new { |h, k| h[k] = [] }
246
- end
247
-
248
- # Whenever a class inherits from Thor or Thor::Group, we should track the
249
- # class and the file on Thor::Base. This is the method responsible for it.
250
- #
251
- def self.register_klass_file(klass) #:nodoc:
252
- file = caller[1].match(/(.*):\d+/)[1]
253
- unless Thor::Base.subclasses.include?(klass)
254
- Thor::Base.subclasses << klass
255
- end
256
-
257
- file_subclasses = Thor::Base.subclass_files[File.expand_path(file)]
258
- file_subclasses << klass unless file_subclasses.include?(klass)
259
- end
260
328
  end # module Base
261
329
  end # class Thor
@@ -0,0 +1,298 @@
1
+ # encoding: UTF-8
2
+ # frozen_string_literal: true
3
+ ##############################################################################
4
+ # Support for {Thor::Argument} in {Thor::Base} Classes
5
+ # ============================================================================
6
+ #
7
+ # With this file I've started to split the "Thor classes" stuff out by feature
8
+ # to make things easier to find and keep the files shorter.
9
+ #
10
+ ##############################################################################
11
+
12
+ # Requirements
13
+ # =======================================================================
14
+
15
+ # Deps
16
+ # -----------------------------------------------------------------------
17
+
18
+ require 'active_support/concern'
19
+
20
+
21
+ # Refinements
22
+ # =======================================================================
23
+
24
+ require 'nrser/refinements/types'
25
+ using NRSER::Types
26
+
27
+ # Namespace
28
+ # =======================================================================
29
+
30
+ class Thor
31
+ module Base
32
+
33
+ # Definitions
34
+ # =======================================================================
35
+
36
+
37
+ # @todo document Arguments module.
38
+ #
39
+ module ArgumentsConcern
40
+ extend ActiveSupport::Concern
41
+
42
+ # Instance Methods
43
+ # ========================================================================
44
+
45
+
46
+ class_methods do
47
+ # ==========================================================================
48
+
49
+ def arguments command: nil
50
+ [*class_arguments, *command&.arguments]
51
+ end
52
+
53
+
54
+ protected
55
+ # ========================================================================
56
+
57
+ def build_argument name, options, scope
58
+ name = name.to_s
59
+
60
+ is_thor_reserved_word? name, :argument
61
+ no_commands { attr_accessor name }
62
+
63
+ required = if options.key?(:optional)
64
+ !options[:optional]
65
+ elsif options.key?(:required)
66
+ options[:required]
67
+ else
68
+ # If neither `:required` or `:optional` options were provided,
69
+ # default to the argument being required if no `:default` was provided.
70
+ options[:default].nil?
71
+ end
72
+
73
+ scope.delete_if { |argument| argument.name == name }
74
+
75
+ if required
76
+ scope.each do |argument|
77
+ next if argument.required?
78
+ raise ArgumentError,
79
+ "You cannot have #{ name.inspect } as required argument " \
80
+ "after the non-required argument #{ argument.human_name.inspect }."
81
+ end
82
+ end
83
+
84
+ options[:required] = required
85
+
86
+ scope << Thor::Argument.new( name, options )
87
+ end
88
+
89
+
90
+ def remove_argument_from *names, scope:, undefine: false
91
+ names.each do |name|
92
+ scope.delete_if { |a| a.name == name.to_s }
93
+ undef_method name, "#{name}=" if undefine
94
+ end
95
+ end
96
+
97
+ public # end protected ***************************************************
98
+
99
+
100
+ # @!group Class-Level Argument Class Methods
101
+ # ------------------------------------------------------------------------
102
+
103
+ # Returns this class' class-level arguments, looking up in the ancestors
104
+ # chain.
105
+ #
106
+ # @return [Array<Thor::Argument>]
107
+ #
108
+ def class_arguments
109
+ @class_arguments ||= from_superclass( :class_arguments, [] )
110
+ end
111
+
112
+
113
+ # Adds an argument to the class and creates an attr_accessor for it.
114
+ #
115
+ # @note
116
+ # This used to just be called `.argument`, and apparently arguments
117
+ # were class-level **only**. I don't know why.
118
+ #
119
+ # Atli switches them to mirror options, with class and command levels.
120
+ #
121
+ # Arguments are different from options in several aspects. The first one
122
+ # is how they are parsed from the command line, arguments are retrieved
123
+ # from position:
124
+ #
125
+ # thor command NAME
126
+ #
127
+ # Instead of:
128
+ #
129
+ # thor command --name=NAME
130
+ #
131
+ # Besides, arguments are used inside your code as an accessor
132
+ # (self.argument), while options are all kept in a hash (self.options).
133
+ #
134
+ # Finally, arguments cannot have type :default or :boolean but can be
135
+ # optional (supplying :optional => :true or :required => false), although
136
+ # you cannot have a required argument after a non-required argument. If
137
+ # you try it, an error is raised.
138
+ #
139
+ # @param [String | Symbol] name
140
+ # The name of the argument.
141
+ #
142
+ # @param [Hash] options
143
+ #
144
+ # @option options [String] :desc
145
+ # Description for the argument.
146
+ #
147
+ # @option options [Boolean] :required
148
+ # If the argument is required or not (opposite of `:optional`).
149
+ #
150
+ # @option options [Boolean] :optional
151
+ # If the argument is optional or not (opposite of `:required`).
152
+ #
153
+ # @option options [:string | :hash | :array | :numeric] :type
154
+ # The type of the argument.
155
+ #
156
+ # @option options [Object] :default
157
+ # Default value for this argument. It cannot be required and
158
+ # have default values.
159
+ #
160
+ # @option options [String] :banner
161
+ # String to show on usage notes.
162
+ #
163
+ # @return (see #class_arguments)
164
+ #
165
+ # @raise [ArgumentError]
166
+ # Raised if you supply a required argument after a non-required one.
167
+ #
168
+ def class_argument name, **options
169
+ build_argument name, options, class_arguments
170
+ end
171
+
172
+
173
+ # Removes a previous defined class-level argument. If `:undefine` option is
174
+ # given, un-defines accessors as well.
175
+ #
176
+ # @param [Array<String | Symbol>] *names
177
+ # Arguments to be removed
178
+ #
179
+ # @param [Boolean] undefine:
180
+ # Un-defines the arguments' setter methods as well.
181
+ #
182
+ # @example
183
+ # remove_class_argument :foo
184
+ #
185
+ # @example
186
+ # remove_class_argument :foo, :bar, :baz, :undefine => true
187
+ #
188
+ def remove_class_argument *names, undefine: false
189
+ remove_argument_from *names, scope: class_arguments, undefine: undefine
190
+ end
191
+
192
+ # @!endgroup Class-Level Argument Class Methods # ************************
193
+
194
+
195
+ # @!group Method-Specific Argument Class Methods
196
+ # ------------------------------------------------------------------------
197
+
198
+ # Returns this class' class-level arguments, looking up in the ancestors
199
+ # chain.
200
+ #
201
+ # @return [Array<Thor::Argument>]
202
+ #
203
+ def method_arguments
204
+ @method_arguments ||= []
205
+ end
206
+
207
+
208
+ # Adds an argument to the class and creates an attr_accessor for it.
209
+ #
210
+ # @note
211
+ # This used to just be called `.argument`, and apparently arguments
212
+ # were class-level **only**. I don't know why.
213
+ #
214
+ # Atli switches them to mirror options, with class and command levels.
215
+ #
216
+ # Arguments are different from options in several aspects. The first one
217
+ # is how they are parsed from the command line, arguments are retrieved
218
+ # from position:
219
+ #
220
+ # thor command NAME
221
+ #
222
+ # Instead of:
223
+ #
224
+ # thor command --name=NAME
225
+ #
226
+ # Besides, arguments are used inside your code as an accessor
227
+ # (self.argument), while options are all kept in a hash (self.options).
228
+ #
229
+ # Finally, arguments cannot have type :default or :boolean but can be
230
+ # optional (supplying :optional => :true or :required => false), although
231
+ # you cannot have a required argument after a non-required argument. If
232
+ # you try it, an error is raised.
233
+ #
234
+ # @param [String | Symbol] name
235
+ # The name of the argument.
236
+ #
237
+ # @param [Hash] options
238
+ #
239
+ # @option options [String] :desc
240
+ # Description for the argument.
241
+ #
242
+ # @option options [Boolean] :required
243
+ # If the argument is required or not (opposite of `:optional`).
244
+ #
245
+ # @option options [Boolean] :optional
246
+ # If the argument is optional or not (opposite of `:required`).
247
+ #
248
+ # @option options [:string | :hash | :array | :numeric] :type
249
+ # The type of the argument.
250
+ #
251
+ # @option options [Object] :default
252
+ # Default value for this argument. It cannot be required and
253
+ # have default values.
254
+ #
255
+ # @option options [String] :banner
256
+ # String to show on usage notes.
257
+ #
258
+ # @return (see #class_arguments)
259
+ #
260
+ # @raise [ArgumentError]
261
+ # Raised if you supply a required argument after a non-required one.
262
+ #
263
+ def method_argument name, **options
264
+ build_argument( name, options, method_arguments )
265
+ end
266
+
267
+ alias_method :argument, :method_argument
268
+ alias_method :arg, :method_argument
269
+
270
+
271
+ # Removes a previous defined class-level argument. If `:undefine` option is
272
+ # given, un-defines accessors as well.
273
+ #
274
+ # @param [Array<String | Symbol>] *names
275
+ # Arguments to be removed
276
+ #
277
+ # @param [Boolean] undefine:
278
+ # Un-defines the arguments' setter methods as well.
279
+ #
280
+ # @example
281
+ #
282
+ # remove_argument :foo remove_argument :foo, :bar, :baz, :undefine => true
283
+ #
284
+ def remove_method_argument *names, undefine: false
285
+ remove_argument_from *names, scope: method_arguments, undefine: undefine
286
+ end
287
+
288
+ # @!endgroup Method-Specific Argument Class Methods # ********************
289
+
290
+ end # class_methods ********************************************************
291
+
292
+ end # module ArgumentsConcern
293
+
294
+ # /Namespace
295
+ # =======================================================================
296
+
297
+ end # module Base
298
+ end # class Thor