Skip to content

Commit

Permalink
Added more specs to class method invocations.
Browse files Browse the repository at this point in the history
  • Loading branch information
josevalim committed Jul 14, 2009
1 parent e415af7 commit 364d6e3
Show file tree
Hide file tree
Showing 7 changed files with 335 additions and 221 deletions.
5 changes: 1 addition & 4 deletions lib/thor.rb
Expand Up @@ -4,9 +4,7 @@
require 'thor/actions'

class Thor

class << self

# Sets the default task when thor is executed without an explicit task to be called.
#
# ==== Parameters
Expand Down Expand Up @@ -108,7 +106,7 @@ def method_options(options=nil)
# :group - The group for this options. Use by class options to output options in different levels.
# :banner - String to show on usage notes.
#
def method_option(name, options)
def method_option(name, options={})
scope = if options[:for]
find_and_refresh_task(options[:for]).options
else
Expand Down Expand Up @@ -220,7 +218,6 @@ def normalize_task_name(meth) #:nodoc:
meth = mapping || meth || default_task
meth.to_s.gsub('-','_') # treat foo-bar > foo_bar
end

end

include Thor::Base
Expand Down
60 changes: 29 additions & 31 deletions lib/thor/base.rb
Expand Up @@ -189,7 +189,7 @@ def class_options(options=nil)
# :type - The type of the argument, can be :string, :hash, :array, :numeric or :boolean.
# :banner - String to show on usage notes.
#
def class_option(name, options)
def class_option(name, options={})
build_option(name, options, class_options)
end

Expand Down Expand Up @@ -372,44 +372,42 @@ def start(given_args=ARGV, config={}) #:nodoc:
# requires two options: the group name and the array of options.
#
def class_options_help(shell, ungrouped_name=nil, extra_group=nil) #:nodoc:
unless self.class_options.empty?
groups = {}
groups = {}

class_options.each do |_, value|
groups[value.group] ||= []
groups[value.group] << value
end

printer = proc do |group_name, options|
list = []
padding = options.collect{ |o| o.aliases.size }.max.to_i * 4
class_options.each do |_, value|
groups[value.group] ||= []
groups[value.group] << value
end

options.each do |option|
list << [ option.usage(padding), option.description || "" ]
list << [ "", "Default: #{option.default}" ] if option.show_default?
end
printer = proc do |group_name, options|
list = []
padding = options.collect{ |o| o.aliases.size }.max.to_i * 4

unless list.empty?
if group_name
shell.say "#{group_name} options:"
else
shell.say "Options:"
end
options.each do |option|
list << [ option.usage(padding), option.description || "" ]
list << [ "", "Default: #{option.default}" ] if option.show_default?
end

shell.print_table(list, :emphasize_last => true, :ident => 2)
shell.say ""
unless list.empty?
if group_name
shell.say "#{group_name} options:"
else
shell.say "Options:"
end

shell.print_table(list, :emphasize_last => true, :ident => 2)
shell.say ""
end
end

# Deal with default group
global_options = groups.delete(nil) || []
printer.call(ungrouped_name, global_options) if global_options
# Deal with default group
global_options = groups.delete(nil) || []
printer.call(ungrouped_name, global_options) if global_options

# Print all others
groups = extra_group.merge(groups) if extra_group
groups.each(&printer)
printer
end
# Print all others
groups = extra_group.merge(groups) if extra_group
groups.each(&printer)
printer
end

# Raises an error if the word given is a Thor reserved word.
Expand Down
172 changes: 170 additions & 2 deletions lib/thor/group.rb
@@ -1,7 +1,10 @@
# Thor has a special class called Thor::Group. The main difference to Thor class
# is that it invokes all tasks at once. It also include some methods that allows
# invocations to be done at the class method, which are not available to Thor
# tasks.
#
class Thor::Group

class << self

# The descrition for this Thor::Group. If none is provided, but a source root
# exists, tries to find the USAGE one folder above it, otherwise searches
# in the superclass.
Expand Down Expand Up @@ -50,6 +53,171 @@ def help(shell, options={})
end
end

# Stores invocations for this class merging with superclass values.
#
def invocations #:nodoc:
@invocations ||= from_superclass(:invocations, {})
end

# Stores invocation blocks used on invoke_from_option.
#
def invocation_blocks #:nodoc:
@invocation_blocks ||= from_superclass(:invocation_blocks, {})
end

# Invoke the given namespace or class given. It adds an instance
# method that will invoke the klass and task. You can give a block to
# configure how it will be invoked.
#
# The namespace/class given will have its options showed on the help
# usage. Check invoke_from_option for more information.
#
def invoke(*names, &block)
options = names.last.is_a?(Hash) ? names.pop : {}
verbose = options.fetch(:verbose, :white)

names.each do |name|
invocations[name] = false
invocation_blocks[name] = block if block_given?

class_eval <<-METHOD, __FILE__, __LINE__
def _invoke_#{name.to_s.gsub(/\W/, '_')}
klass, task = self.class.prepare_for_invocation(nil, #{name.inspect})
if klass
say_status :invoke, #{name.inspect}, #{verbose.inspect}
block = self.class.invocation_blocks[#{name.inspect}]
invoke_with_padding klass, task, &block
else
say_status :error, %(#{name.inspect} [not found]), :red
end
end
METHOD
end
end

# Invoke a thor class based on the value supplied by the user to the
# given option named "name". A class option must be created before this
# method is invoked for each name given.
#
# ==== Examples
#
# class GemGenerator < Thor::Group
# class_option :test_framework, :type => :string
# invoke_from_option :test_framework
# end
#
# ==== Boolean options
#
# In some cases, you want to invoke a thor class if some option is true or
# false. This is automatically handled by invoke_from_option. Then the
# option name is used to invoke the generator.
#
# ==== Preparing for invocation
#
# In some cases you want to customize how a specified hook is going to be
# invoked. You can do that by overwriting the class method
# prepare_for_invocation. The class method must necessarily return a klass
# and an optional task.
#
# ==== Custom invocations
#
# You can also supply a block to customize how the option is giong to be
# invoked. The block receives two parameters, an instance of the current
# class and the klass to be invoked.
#
def invoke_from_option(*names, &block)
options = names.last.is_a?(Hash) ? names.pop : {}
verbose = options.fetch(:verbose, :white)

names.each do |name|
unless class_options.key?(name)
raise ArgumentError, "You have to define the option #{name.inspect} " <<
"before setting invoke_from_option."
end

invocations[name] = true
invocation_blocks[name] = block if block_given?

class_eval <<-METHOD, __FILE__, __LINE__
def _invoke_from_option_#{name.to_s.gsub(/\W/, '_')}
return unless options[#{name.inspect}]
value = options[#{name.inspect}]
value = #{name.inspect} if TrueClass === value
klass, task = self.class.prepare_for_invocation(#{name.inspect}, value)
if klass
say_status :invoke, value, #{verbose.inspect}
block = self.class.invocation_blocks[#{name.inspect}]
invoke_with_padding klass, task, &block
else
say_status :error, %(\#{value} [not found]), :red
end
end
METHOD
end
end

# Remove a previously added invocation.
#
# ==== Examples
#
# remove_invocation :test_framework
#
def remove_invocation(*names)
names.each do |name|
remove_task(name)
remove_class_option(name)
invocations.delete(name)
invocation_blocks.delete(name)
end
end

# Overwrite class options help to allow invoked generators options to be
# shown recursively when invoking a generator.
#
def class_options_help(shell, ungrouped_name=nil, extra_group=nil) #:nodoc:
group_options = {}

get_options_from_invocations(group_options, class_options) do |klass|
klass.send(:get_options_from_invocations, group_options, class_options)
end

group_options.merge!(extra_group) if extra_group
super(shell, ungrouped_name, group_options)
end

# Get invocations array and merge options from invocations. Those
# options are added to group_options hash. Options that already exists
# in base_options are not added twice.
#
def get_options_from_invocations(group_options, base_options) #:nodoc:
invocations.each do |name, from_option|
value = if from_option
option = class_options[name]
option.type == :boolean ? name : option.default
else
name
end
next unless value

klass, task = prepare_for_invocation(name, value)
next unless klass && klass.respond_to?(:class_options)

value = value.to_s
human_name = value.respond_to?(:classify) ? value.classify : value

group_options[human_name] ||= []
group_options[human_name] += klass.class_options.values.select do |option|
base_options[option.name.to_sym].nil? && option.group.nil? &&
!group_options.values.flatten.any? { |i| i.name == option.name }
end

yield klass if block_given?
end
end

protected

# The banner for this class. You can customize it if you are invoking the
Expand Down

0 comments on commit 364d6e3

Please sign in to comment.