josevalim-thor 0.10.7 → 0.10.8

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/actions.rb CHANGED
@@ -304,5 +304,13 @@ class Thor
304
304
  end
305
305
  end
306
306
 
307
+ protected
308
+
309
+ # Update dump_config to dump also behavior and root.
310
+ #
311
+ def _dump_config #:nodoc:
312
+ super.merge!(:behavior => self.behavior, :root => @root_stack[0])
313
+ end
314
+
307
315
  end
308
316
  end
data/lib/thor/base.rb CHANGED
@@ -7,7 +7,8 @@ require 'thor/task'
7
7
  require 'thor/util'
8
8
 
9
9
  class Thor
10
- HELP_MAPPINGS = ["-h", "-?", "--help", "-D"]
10
+ HELP_MAPPINGS = %w(-h -? --help -D)
11
+ RESERVED_TASK_NAMES = %w(all invoke shell options behavior root destination_root relative_root source_root)
11
12
 
12
13
  class Maxima < Struct.new(:usage, :options, :class_options)
13
14
  end
@@ -215,19 +216,6 @@ class Thor
215
216
  end
216
217
  end
217
218
 
218
- # Retrieve a specific task from this Thor class. If the desired Task cannot
219
- # be found, returns a dynamic Thor::Task that will map to the given method.
220
- #
221
- # ==== Parameters
222
- # meth<Symbol>:: the name of the task to be retrieved
223
- #
224
- # ==== Returns
225
- # Task
226
- #
227
- def [](meth)
228
- all_tasks[meth.to_s] || Thor::Task.dynamic(meth)
229
- end
230
-
231
219
  # All methods defined inside the given block are not added as tasks.
232
220
  #
233
221
  # So you can do:
@@ -384,6 +372,11 @@ class Thor
384
372
  end
385
373
 
386
374
  return if @no_tasks || !valid_task?(meth)
375
+
376
+ if RESERVED_TASK_NAMES.include?(meth)
377
+ raise ScriptError, "'#{meth}' is a Thor reserved word and cannot be defined as task"
378
+ end
379
+
387
380
  Thor::Base.register_klass_file(self)
388
381
  create_task(meth)
389
382
  end
@@ -456,20 +449,10 @@ class Thor
456
449
  end
457
450
 
458
451
  self.options = Thor::CoreExt::HashWithIndifferentAccess.new(options).freeze
459
- self.shell = config[:shell]
460
452
 
461
- # Add base to shell if an accessor is provided.
462
- self.shell.base = self if self.shell.respond_to?(:base)
463
- end
464
-
465
- # Common methods that are delegated to the shell.
466
- #
467
- SHELL_DELEGATED_METHODS.each do |method|
468
- module_eval <<-METHOD, __FILE__, __LINE__
469
- def #{method}(*args)
470
- shell.#{method}(*args)
471
- end
472
- METHOD
453
+ # Configure shell and set base if not already
454
+ self.shell = config[:shell]
455
+ self.shell.base ||= self if self.shell.respond_to?(:base)
473
456
  end
474
457
 
475
458
  # Holds the shell for the given Thor instance. If no shell is given,
@@ -485,15 +468,133 @@ class Thor
485
468
  @shell = shell
486
469
  end
487
470
 
488
- # Finds a task with the name given and invokes it with the given arguments.
489
- # This is the default interface to invoke tasks. You can always run a task
490
- # directly, but the invocation system will be implemented in a fashion
491
- # that a same task cannot be invoked twice (a la rake).
471
+ # Common methods that are delegated to the shell.
492
472
  #
493
- def invoke(name, *args)
494
- self.class[name].run(self, *args)
473
+ SHELL_DELEGATED_METHODS.each do |method|
474
+ module_eval <<-METHOD, __FILE__, __LINE__
475
+ def #{method}(*args)
476
+ shell.#{method}(*args)
477
+ end
478
+ METHOD
495
479
  end
496
- end
497
480
 
481
+ # Receives a name and invokes it. The name can be either a namespaced name,
482
+ # a current class task or even a class. Arguments are given in an array and
483
+ # options given are merged with the invoker options.
484
+ #
485
+ # ==== Examples
486
+ #
487
+ # class A < Thor
488
+ # def foo
489
+ # invoke :bar
490
+ # invoke "b:lib", ["merb", "rails"]
491
+ # end
492
+ #
493
+ # def bar
494
+ # invoke "b:lib", ["merb", "rails"]
495
+ # # magic
496
+ # end
497
+ # end
498
+ #
499
+ # class B < Thor
500
+ # argument :preferred_framework, :type => :string
501
+ #
502
+ # def lib(second_framework)
503
+ # # magic
504
+ # end
505
+ # end
506
+ #
507
+ # You can notice that the method "foo" above invokes two tasks: "bar",
508
+ # which belongs to the same class and "lib" that belongs to the class B.
509
+ #
510
+ # By using an invocation system you ensure that a task is invoked only once.
511
+ # In the example above, invoking foo will invoke "b:lib" just once, even if
512
+ # it's invoked later by "bar" method.
513
+ #
514
+ # When invoking another class, there are a few things to keep in mind:
515
+ #
516
+ # 1) Class arguments are parsed first. In the example above, preferred
517
+ # framework is going to consume "merb" and second framework is going
518
+ # to be set to "rails".
519
+ #
520
+ # 2) All options and configurations are sent to the invoked class.
521
+ # So the invoked class is going to use the same shell instance, will
522
+ # have the same behavior (:invoke or :revoke) and so on.
523
+ #
524
+ # Invoking a Thor::Group happens in the same away as above:
525
+ #
526
+ # class C < Thor::Group
527
+ # def one
528
+ # end
529
+ # end
530
+ #
531
+ # Is invoked as:
532
+ #
533
+ # invoke "c"
534
+ #
535
+ # Or even as:
536
+ #
537
+ # invoke C
538
+ #
539
+ def invoke(name, method_args=[], options={})
540
+ @_invocations ||= Hash.new { |h,k| h[k] = [] }
541
+ instance, task = _setup_for_invoke(name, method_args, options)
542
+
543
+ current = @_invocations[instance.class]
544
+ return if current.include?("all")
545
+
546
+ if task
547
+ task = self.class.all_tasks[task.to_s] || Task.dynamic(task) unless task.is_a?(Thor::Task)
548
+ return if current.include?(task.name)
549
+
550
+ current << task.name
551
+ task.run(instance, method_args)
552
+ else
553
+ current << "all"
554
+ instance.class.all_tasks.collect { |_, task| task.run(instance) }
555
+ end
556
+ end
557
+
558
+ protected
559
+
560
+ # This is the method responsable for retrieving and setting up an
561
+ # instance to be used in invoke.
562
+ #
563
+ def _setup_for_invoke(name, method_args, options) #:nodoc:
564
+ case name
565
+ when NilClass, Thor::Task
566
+ # Do nothing, we already have what we want
567
+ when Class
568
+ klass = name
569
+ else
570
+ name = name.to_s
571
+ unless self.class.all_tasks[name]
572
+ klass, task = Thor::Util.namespace_to_thor_class(name) rescue Thor::Error
573
+ end
574
+ end
575
+
576
+ if klass.nil?
577
+ return self, name
578
+ elsif klass <= Thor::Base
579
+ size = klass.arguments.size
580
+ class_args = method_args.slice!(0, size)
581
+ instance = klass.new(class_args, self.options.merge(options), _dump_config)
582
+
583
+ task ||= klass.default_task if klass <= Thor
584
+ instance.instance_variable_set("@_invocations", @_invocations)
585
+
586
+ return instance, task
587
+ else
588
+ raise ScriptError, "Expected Thor class, got #{klass}"
589
+ end
590
+ end
591
+
592
+ # Dump the configuration values for this current class.
593
+ #
594
+ def _dump_config #:nodoc:
595
+ { :shell => self.shell }
596
+ end
597
+
598
+ end
498
599
  end
499
600
  end
@@ -11,15 +11,10 @@ class Thor
11
11
  #
12
12
  class HashWithIndifferentAccess < ::Hash
13
13
 
14
- def initialize(hash)
14
+ def initialize(hash={})
15
15
  super()
16
-
17
16
  hash.each do |key, value|
18
- if key.is_a?(Symbol)
19
- self[key.to_s] = value
20
- else
21
- self[key] = value
22
- end
17
+ self[convert_key(key)] = value
23
18
  end
24
19
  end
25
20
 
@@ -27,6 +22,10 @@ class Thor
27
22
  super(convert_key(key))
28
23
  end
29
24
 
25
+ def []=(key, value)
26
+ super(convert_key(key), value)
27
+ end
28
+
30
29
  def delete(key)
31
30
  super(convert_key(key))
32
31
  end
@@ -35,6 +34,17 @@ class Thor
35
34
  indices.collect { |key| self[convert_key(key)] }
36
35
  end
37
36
 
37
+ def merge(other)
38
+ dup.merge!(other)
39
+ end
40
+
41
+ def merge!(other)
42
+ other.each do |key, value|
43
+ self[convert_key(key)] = value
44
+ end
45
+ self
46
+ end
47
+
38
48
  protected
39
49
 
40
50
  def convert_key(key)
@@ -110,11 +110,7 @@ class Thor #:nodoc:
110
110
  end
111
111
 
112
112
  def merge(other)
113
- new = clone
114
- other.each do |key, value|
115
- new[key] = value
116
- end
117
- new
113
+ dup.merge!(other)
118
114
  end
119
115
 
120
116
  def merge!(other)
data/lib/thor/group.rb CHANGED
@@ -22,6 +22,71 @@ class Thor::Group
22
22
  end
23
23
  end
24
24
 
25
+ # Sets the condition for some task to be executed in the class level. Why
26
+ # is this important? Setting the conditions in the class level allows to
27
+ # an inherited class change the conditions and customize the Thor::Group as
28
+ # it wishes.
29
+ #
30
+ # The conditions given are retrieved from the options hash. Let's suppose
31
+ # that a task is only executed if --test-framework is rspec. You could do
32
+ # this:
33
+ #
34
+ # class_option :test_framework, :type => :string
35
+ #
36
+ # conditions :test_framework => :rspec
37
+ # def create_rspec_files
38
+ # # magic
39
+ # end
40
+ #
41
+ # Later someone creates a framework on top of rspec and need rspec files to
42
+ # generated as well. He could then change the conditions:
43
+ #
44
+ # conditions :test_framework => [ :rspec, :remarkable ], :for => :create_rspec_files
45
+ #
46
+ # He could also use remove_conditions and remove previous set conditions:
47
+ #
48
+ # remove_conditions :test_framework, :for => :create_rspec_files
49
+ #
50
+ # Conditions only work with the class option to be comparead to is a boolean,
51
+ # string or numeric (no array or hash comparisions).
52
+ #
53
+ # ==== Parameters
54
+ # conditions<Hash>:: the conditions for the task. The key is the option name
55
+ # and the value is the condition to be checked. If the
56
+ # condition is an array, it checkes if the current value
57
+ # is included in the array. If a regexp, checks if the
58
+ # value matches, all other values are simply compared (==).
59
+ #
60
+ def conditions(conditions=nil)
61
+ subject = if conditions && conditions[:for]
62
+ find_and_refresh_task(conditions.delete(:for)).conditions
63
+ else
64
+ @conditions ||= {}
65
+ end
66
+
67
+ subject.merge!(conditions) if conditions
68
+ subject
69
+ end
70
+
71
+ # Remove a previous specified condition. Check <tt>conditions</tt> above for
72
+ # a complete example.
73
+ #
74
+ # ==== Parameters
75
+ # conditions<Array>:: An array of conditions to be removed.
76
+ # for<Hash>:: A hash with :for as key indicating the task to remove the conditions from.
77
+ #
78
+ # ==== Examples
79
+ #
80
+ # remove_conditions :test_framework, :orm, :for => :create_app_skeleton
81
+ #
82
+ def remove_conditions(*conditions)
83
+ subject = find_and_refresh_task(conditions.pop[:for]).conditions
84
+ conditions.each do |condition|
85
+ subject.delete(condition)
86
+ end
87
+ subject
88
+ end
89
+
25
90
  # Start in Thor::Group works differently. It invokes all tasks inside the
26
91
  # class and does not have to parse task options.
27
92
  #
@@ -34,7 +99,7 @@ class Thor::Group
34
99
  opts = Thor::Options.new(class_options)
35
100
  opts.parse(args)
36
101
 
37
- new(opts.arguments, opts.options, config).invoke_all
102
+ new(opts.arguments, opts.options, config).invoke(:all)
38
103
  end
39
104
  rescue Thor::Error => e
40
105
  config[:shell].error e.message
@@ -75,15 +140,21 @@ class Thor::Group
75
140
  end
76
141
 
77
142
  def create_task(meth) #:nodoc:
78
- tasks[meth.to_s] = Thor::Task.new(meth, nil, nil, nil)
143
+ tasks[meth.to_s] = Thor::Task.new(meth, nil, nil, nil, @conditions)
144
+ @conditions = nil
79
145
  end
80
146
  end
81
147
 
82
- # Invokes all tasks in the instance.
83
- #
84
- def invoke_all
85
- self.class.all_tasks.map { |_, task| task.run(self) }
86
- end
87
-
88
148
  include Thor::Base
149
+
150
+ protected
151
+
152
+ # Overwrite _setup_for_invoke to force invocation of all tasks when :all is
153
+ # supplied.
154
+ #
155
+ def _setup_for_invoke(name, method_args, options)
156
+ name = nil if name.to_s == "all"
157
+ super(name, method_args, options)
158
+ end
159
+
89
160
  end
data/lib/thor/task.rb CHANGED
@@ -1,30 +1,31 @@
1
1
  class Thor
2
- class Task < Struct.new(:name, :description, :usage, :options)
2
+ class Task < Struct.new(:name, :description, :usage, :options, :conditions)
3
3
 
4
4
  # Creates a dynamic task. Dynamic tasks are created on demand to allow method
5
5
  # missing calls (since a method missing does not have a task object for it).
6
6
  #
7
7
  def self.dynamic(name)
8
- new(name, "A dynamically-generated task", name.to_s, nil)
8
+ new(name, "A dynamically-generated task", name.to_s)
9
9
  end
10
10
 
11
- def initialize(name, description, usage, options)
12
- super(name, description, usage, options || {})
11
+ def initialize(name, description, usage, options=nil, conditions=nil)
12
+ super(name.to_s, description, usage, options || {}, conditions || {})
13
13
  end
14
14
 
15
15
  # Dup the options hash on clone.
16
16
  #
17
17
  def initialize_copy(other)
18
18
  super(other)
19
- self.options = other.options.dup if other.options
19
+ self.options = other.options.dup if other.options
20
+ self.conditions = other.conditions.dup if other.conditions
20
21
  end
21
22
 
22
23
  # By default, a task invokes a method in the thor class. You can change this
23
24
  # implementation to create custom tasks.
24
25
  #
25
- def run(instance, *args)
26
+ def run(instance, args=[])
26
27
  raise UndefinedTaskError, "the '#{name}' task of #{instance.class} is private" unless public_method?(instance)
27
- instance.send(name, *args)
28
+ instance.send(name, *args) if valid_conditions?(instance)
28
29
  rescue ArgumentError => e
29
30
  backtrace = sans_backtrace(e.backtrace, caller)
30
31
 
@@ -86,7 +87,30 @@ class Thor
86
87
  # Given a target, checks if this class name is not a private/protected method.
87
88
  #
88
89
  def public_method?(instance)
89
- !(instance.private_methods + instance.protected_methods).include?(name.to_s)
90
+ collection = instance.private_methods + instance.protected_methods
91
+ !(collection).include?(name.to_s) && !(collection).include?(name.to_sym) # For Ruby 1.9
92
+ end
93
+
94
+ # Check if the task conditions are met before invoking.
95
+ #
96
+ def valid_conditions?(instance)
97
+ return true if conditions.empty?
98
+
99
+ conditions.each do |key, expected|
100
+ actual = stringify!(instance.options[key])
101
+ expected = stringify!(expected)
102
+
103
+ return false if case expected
104
+ when Regexp
105
+ actual !~ expected
106
+ when Array
107
+ !expected.include?(actual)
108
+ else
109
+ actual != expected
110
+ end
111
+ end
112
+
113
+ true
90
114
  end
91
115
 
92
116
  # Clean everything that comes from the Thor gempath and remove the caller.
@@ -97,5 +121,18 @@ class Thor
97
121
  saned -= caller
98
122
  end
99
123
 
124
+ # Receives an object and convert any symbol to string.
125
+ #
126
+ def stringify!(duck)
127
+ case duck
128
+ when Array
129
+ duck.map!{ |i| i.is_a?(Symbol) ? i.to_s : i }
130
+ when Symbol
131
+ duck.to_s
132
+ else
133
+ duck
134
+ end
135
+ end
136
+
100
137
  end
101
138
  end
data/lib/thor.rb CHANGED
@@ -127,14 +127,14 @@ class Thor
127
127
  config[:shell] ||= Thor::Base.shell.new
128
128
 
129
129
  meth = normalize_task_name(args.shift)
130
- task = self[meth]
130
+ task = all_tasks[meth] || Task.dynamic(meth)
131
131
 
132
132
  options = class_options.merge(task.options)
133
133
  opts = Thor::Options.new(options)
134
134
  opts.parse(args)
135
135
 
136
136
  instance = new(opts.arguments, opts.options, config)
137
- instance.invoke(task.name, *opts.trailing)
137
+ instance.invoke(task, opts.trailing)
138
138
  rescue Thor::Error => e
139
139
  config[:shell].error e.message
140
140
  end
@@ -153,7 +153,7 @@ class Thor
153
153
  meth, options = nil, meth if meth.is_a?(Hash)
154
154
 
155
155
  if meth
156
- task = self.all_tasks[meth]
156
+ task = all_tasks[meth]
157
157
  raise UndefinedTaskError, "task '#{meth}' could not be found in namespace '#{self.namespace}'" unless task
158
158
 
159
159
  shell.say "Usage:"
@@ -198,7 +198,7 @@ class Thor
198
198
  end
199
199
 
200
200
  def create_task(meth) #:nodoc:
201
- tasks[meth.to_s] = Thor::Task.new(meth, @desc, @usage, method_options)
201
+ tasks[meth.to_s] = Thor::Task.new(meth, @desc, @usage, method_options, nil)
202
202
  @usage, @desc, @method_options = nil
203
203
  end
204
204
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: josevalim-thor
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.10.7
4
+ version: 0.10.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yehuda Katz
@@ -9,7 +9,7 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2009-06-17 00:00:00 -07:00
12
+ date: 2009-06-20 00:00:00 -07:00
13
13
  default_executable:
14
14
  dependencies: []
15
15