lib/tap/task.rb in bahuvrihi-tap-0.11.2 vs lib/tap/task.rb in bahuvrihi-tap-0.12.0
- old
+ new
@@ -1,13 +1,14 @@
require 'tap/support/executable'
-require 'tap/support/lazydoc/method'
-require 'tap/support/lazydoc/definition'
require 'tap/support/intern'
-autoload(:OptionParser, 'optparse')
+autoload(:ConfigParser, 'config_parser')
module Tap
-
+ module Support
+ autoload(:Templater, 'tap/support/templater')
+ end
+
# === Task Definition
#
# Tasks specify executable code by overridding the process method in
# subclasses. The number of inputs to process corresponds to the inputs
# given to execute or enq.
@@ -27,13 +28,12 @@
# NoInput.new.execute # => []
# OneInput.new.execute(:a) # => [:a]
# MixedInputs.new.execute(:a, :b) # => [:a, :b, []]
# MixedInputs.new.execute(:a, :b, 1, 2, 3) # => [:a, :b, [1,2,3]]
#
- # Tasks may be create with new, or with intern. Intern overrides
- # process with a custom block that gets called with the task instance
- # and the inputs.
+ # Tasks may be create with new, or with intern. Intern overrides process
+ # using a block that receives the task instance and the inputs.
#
# no_inputs = Task.intern {|task| [] }
# one_input = Task.intern {|task, input| [input] }
# mixed_inputs = Task.intern {|task, a, b, *args| [a, b, args] }
#
@@ -65,12 +65,12 @@
# t = ConfiguredTask.new(:one => 'ONE', :three => 'three')
# t.config # => {:one => 'ONE', :two => 'two', :three => 'three'}
# t.respond_to?(:three) # => false
#
# Configurations can be validated/transformed using an optional block.
- # Tap::Support::Validation pre-packages many common blocks which may
- # be accessed through the class method 'c':
+ # Many common blocks are pre-packaged and may be accessed through the
+ # class method 'c':
#
# class ValidatingTask < Tap::Task
# # string config validated to be a string
# config :string, 'str', &c.check(String)
#
@@ -83,44 +83,35 @@
# t.integer = 1.1 # !> ValidationError
#
# t.integer = "1"
# t.integer == 1 # => true
#
- #--
+ # See the {Configurable}[http://tap.rubyforge.org/configurable/]
+ # documentation for more information.
+ #
# === Subclassing
- # Tasks can be subclassed normally, with one reminder related to batching.
+ # Tasks may be subclassed normally, but be sure to call super as necessary,
+ # in particular when overriding the following methods:
#
- # Batched tasks are generated by duplicating an existing instance, hence
- # all instance variables will point to the same object in the batched
- # and original task. At times (as with configurations), this is
- # undesirable; the batched task should have it's own copy of an
- # instance variable.
+ # class Subclass < Tap::Task
+ # class << self
+ # def inherited(child)
+ # super
+ # end
+ # end
#
- # In these cases, the <tt>initialize_copy</tt> should be overridden
- # and should re-initialize the appropriate variables. Be sure to call
- # super to invoke the default <tt>initialize_copy</tt>:
- #
- # class SubclassTask < Tap::Task
- # attr_accessor :array
# def initialize(*args)
- # @array = []
# super
# end
- #
+ #
# def initialize_copy(orig)
- # @array = orig.array.dup
# super
# end
# end
#
- # t1 = SubclassTask.new
- # t2 = t1.initialize_batch_obj
- # t1.array == t2.array # => true
- # t1.array.object_id == t2.array.object_id # => false
- #
class Task
- include Support::Configurable
+ include Configurable
include Support::Executable
class << self
# Returns class dependencies
attr_reader :dependencies
@@ -138,19 +129,19 @@
end
# Returns an instance of self; the instance is a kind of 'global'
# instance used in class-level dependencies. See depends_on.
def instance
- @instance ||= new
+ @instance ||= new.extend(Support::Dependency)
end
def inherited(child) # :nodoc:
unless child.instance_variable_defined?(:@source_file)
- caller.first =~ Support::Lazydoc::CALLER_REGEXP
+ caller[0] =~ Lazydoc::CALLER_REGEXP
child.instance_variable_set(:@source_file, File.expand_path($1))
end
-
+
child.instance_variable_set(:@dependencies, dependencies.dup)
super
end
# Instantiates a new task with the input arguments and overrides
@@ -166,150 +157,207 @@
end
instance
end
# Parses the argv into an instance of self and an array of arguments
- # (implicitly to be enqued to the instance). Yields a help string to
- # the block when the argv indicates 'help'.
- def parse(argv=ARGV, app=Tap::App.instance, &block) # :yields: help_str
- parse!(argv.dup, &block)
+ # (implicitly to be enqued to the instance).
+ def parse(argv=ARGV, app=Tap::App.instance)
+ parse!(argv.dup)
end
- # Same as parse, but removes switches destructively.
- def parse!(argv=ARGV, app=Tap::App.instance) # :yields: help_str
- opts = OptionParser.new
-
- # Add configurations
- argv_config = {}
- unless configurations.empty?
- opts.separator ""
- opts.separator "configurations:"
- end
-
- configurations.each do |receiver, key, config|
- opts.on(*config.to_optparse_argv) do |value|
- argv_config[key] = value
- end
- end
-
- # Add options on_tail, giving priority to configurations
+ # Same as parse, but removes switches destructively.
+ def parse!(argv=ARGV, app=Tap::App.instance)
+ opts = ConfigParser.new
+ opts.separator "configurations:"
+ opts.add(configurations)
+
opts.separator ""
opts.separator "options:"
-
- opts.on_tail("-h", "--help", "Print this help") do
+
+ # Add option to print help
+ opts.on("-h", "--help", "Print this help") do
prg = case $0
when /rap$/ then 'rap'
else 'tap run --'
end
- opts.banner = "#{help}usage: #{prg} #{to_s.underscore} #{args.subject}"
- if block_given?
- yield(opts.to_s)
- else
- puts opts
- exit
- end
+ puts "#{help}usage: #{prg} #{to_s.underscore} #{args}"
+ puts
+ puts opts
+ exit
end
-
- # Add option for name
+
+ # Add option to specify a config file
name = default_name
- opts.on_tail('--name NAME', /^[^-].*/, 'Specify a name') do |value|
+ opts.on('--name NAME', 'Specify a name') do |value|
name = value
end
-
+
# Add option to add args
use_args = []
- opts.on_tail('--use FILE', /^[^-].*/, 'Loads inputs from file') do |value|
- obj = YAML.load_file(value)
- case obj
- when Hash
- obj.values.each do |array|
- # error if value isn't an array
- use_args.concat(array)
- end
- when Array
- use_args.concat(obj)
- else
- use_args << obj
- end
+ opts.on('--use FILE', 'Loads inputs from file') do |path|
+ use(path, use_args)
end
- # parse the argv
- opts.parse!(argv)
-
# build and reconfigure the instance and any associated
# batch objects as specified in the file configurations
- obj = new({}, name, app)
- path_configs = load_config(app.config_filepath(name))
- if path_configs.kind_of?(Array)
- path_configs.each_with_index do |path_config, i|
- next if i == 0
- batch_obj = obj.initialize_batch_obj(path_config, "#{name}_#{i}")
- batch_obj.reconfigure(argv_config)
- end
- path_configs = path_configs[0]
+ argv = opts.parse!(argv)
+ configs = load(app.config_filepath(name))
+ configs = [configs] unless configs.kind_of?(Array)
+
+ obj = new(configs.shift, name, app)
+ configs.each do |config|
+ obj.initialize_batch_obj(config, "#{name}_#{obj.batch.length}")
+ end
+
+ obj.batch.each do |batch_obj|
+ batch_obj.reconfigure(opts.config)
end
- obj.reconfigure(path_configs).reconfigure(argv_config)
[obj, (argv + use_args)]
end
# A convenience method to parse the argv and execute the instance
# with the remaining arguments. If 'help' is specified in the argv,
# execute prints the help and exits.
+ #
+ # Returns the non-audited result.
def execute(argv=ARGV)
- instance, args = parse(ARGV) do |help|
- puts help
- exit
- end
-
+ instance, args = parse(ARGV)
instance.execute(*args)
end
-
- # Returns the class lazydoc, resolving if specified.
- def lazydoc(resolve=true)
- lazydoc = super(false)
- lazydoc[self.to_s]['args'] ||= lazydoc.register_method(:process, Support::Lazydoc::Method)
- super
- end
DEFAULT_HELP_TEMPLATE = %Q{<% manifest = task_class.manifest %>
-<%= task_class %><%= manifest.subject.to_s.strip.empty? ? '' : ' -- ' %><%= manifest.subject %>
+<%= task_class %><%= manifest.empty? ? '' : ' -- ' %><%= manifest.to_s %>
-<% unless manifest.empty? %>
+<% desc = manifest.kind_of?(Lazydoc::Comment) ? manifest.wrap(77, 2, nil) : [] %>
+<% unless desc.empty? %>
<%= '-' * 80 %>
-<% manifest.wrap(77, 2, nil).each do |line| %>
+<% desc.each do |line| %>
<%= line %>
<% end %>
<%= '-' * 80 %>
<% end %>
}
+
# Returns the class help.
def help
Tap::Support::Templater.new(DEFAULT_HELP_TEMPLATE, :task_class => self).build
end
+ # Recursively loads path into a nested configuration file.
+ #--
+ # TODO: move the logic of this to Configurable
+ def load(path, recursive=true)
+ base = Root.trivial?(path) ? {} : (YAML.load_file(path) || {})
+
+ if recursive
+ # determine the files/dirs to load recursively
+ # and add them to paths by key (ie the base
+ # name of the path, minus any extname)
+ paths = {}
+ files, dirs = Dir.glob("#{path.chomp(File.extname(path))}/*").partition do |sub_path|
+ File.file?(sub_path)
+ end
+
+ # directories are added to paths first so they can be
+ # overridden by the files (appropriate since the file
+ # will recursively load the directory if it exists)
+ dirs.each do |dir|
+ paths[File.basename(dir)] = dir
+ end
+
+ # when adding files, check that no two files map to
+ # the same key (ex a.yml, a.yaml).
+ files.each do |filepath|
+ key = File.basename(filepath).chomp(File.extname(filepath))
+ if existing = paths[key]
+ if File.file?(existing)
+ confict = [File.basename(paths[key]), File.basename(filepath)].sort
+ raise "multiple files load the same key: #{confict.inspect}"
+ end
+ end
+
+ paths[key] = filepath
+ end
+
+ # recursively load each file and reverse merge
+ # the result into the base
+ paths.each_pair do |key, recursive_path|
+ value = nil
+ each_hash_in(base) do |hash|
+ unless hash.has_key?(key)
+ hash[key] = (value ||= load(recursive_path, true))
+ end
+ end
+ end
+ end
+
+ base
+ end
+
+ # Loads the contents of path onto argv.
+ def use(path, argv=ARGV)
+ obj = Root.trivial?(path) ? [] : (YAML.load_file(path) || [])
+
+ case obj
+ when Array then argv.concat(obj)
+ else argv << obj
+ end
+
+ argv
+ end
+
protected
- # Sets a class-level dependency. When task class B depends_on another task
- # class A, instances of B are initialized to depend on A.instance, with the
- # specified arguments. Returns self.
+ # Sets a class-level dependency; when task class B depends_on another
+ # task class A, instances of B are initialized to depend on A.instance.
+ # If a non-nil name is specified, depends_on will create a reader of
+ # the resolved dependency value.
+ #
+ # class A < Tap::Task
+ # def process
+ # "result"
+ # end
+ # end
+ #
+ # class B < Tap::Task
+ # depends_on :a, A
+ # end
+ #
+ # b = B.new
+ # b.dependencies # => [A.instance]
+ # b.a # => "result"
+ #
+ # A.instance.resolved? # => true
+ #
+ # Normally class-level dependencies are not added to existing instances
+ # but, as a special case, depends_on updates instance to depend on
+ # dependency_class.instance.
+ #
+ # Returns self.
def depends_on(name, dependency_class)
unless dependencies.include?(dependency_class)
dependencies << dependency_class
end
- # returns the resolved result of the dependency
- define_method(name) do
- instance = dependency_class.instance
- instance.resolve
- instance._result._current
+ # update instance with the dependency if necessary
+ if instance_variable_defined?(:@instance)
+ instance.depends_on(dependency_class.instance)
end
- public(name)
+ if name
+ # returns the resolved result of the dependency
+ define_method(name) do
+ dependency_class.instance.resolve.value
+ end
+
+ public(name)
+ end
+
self
end
# Defines a task subclass with the specified configurations and process block.
# During initialization the subclass is instantiated and made accessible
@@ -392,49 +440,46 @@
if block_given?
subclass.send(:define_method, :process, &block)
end
- # define methods
- instance_var = "@#{name}".to_sym
- reader = (options[:reader] ||= "#{name}_config".to_sym)
- writer = (options[:writer] ||= "#{name}_config=".to_sym)
-
- attr_reader name
-
- define_method(reader) do
- # return the config for the instance
- instance_variable_get(instance_var).config
- end
-
- define_method(writer) do |value|
- # initialize or reconfigure the instance of subclass
- if instance_variable_defined?(instance_var)
- instance_variable_get(instance_var).reconfigure(value)
- else
- instance_variable_set(instance_var, subclass.new(value))
- end
- end
- public(name, reader, writer)
-
# add the configuration
if options[:desc] == nil
- caller[0] =~ Support::Lazydoc::CALLER_REGEXP
- desc = Support::Lazydoc.register($1, $3.to_i - 1, Support::Lazydoc::Definition)
- desc.subclass = subclass
+ caller[0] =~ Lazydoc::CALLER_REGEXP
+ desc = Lazydoc.register($1, $3.to_i - 1)#, Lazydoc::Definition)
+ #desc.subclass = subclass
options[:desc] = desc
end
- configurations.add(name, subclass.configurations.instance_config, options)
+ nest(name, subclass, options) {|overrides| subclass.new(overrides) }
end
+
+ private
+
+ # helper for load_config. yields each hash in the collection (ie each
+ # member of an Array, or the collection if it is a hash). returns
+ # the collection.
+ def each_hash_in(collection) # :nodoc:
+ case collection
+ when Hash then yield(collection)
+ when Array
+ collection.each do |hash|
+ yield(hash) if hash.kind_of?(Hash)
+ end
+ end
+
+ collection
+ end
end
instance_variable_set(:@source_file, __FILE__)
instance_variable_set(:@default_name, 'tap/task')
instance_variable_set(:@dependencies, [])
+
lazy_attr :manifest
- lazy_attr :args
+ lazy_attr :args, :process
+ lazy_register :process, Lazydoc::Arguments
# The name of self.
#--
# Currently names may be any object. Audit makes use of name
# via to_s, as does app when figuring configuration filepaths.
@@ -444,24 +489,24 @@
def initialize(config={}, name=nil, app=App.instance)
super()
@name = name || self.class.default_name
@app = app
- @_method_name = :execute_with_callbacks
+ @method_name = :execute_with_callbacks
@on_complete_block = nil
@dependencies = []
@batch = [self]
case config
- when Support::InstanceConfiguration
+ when DelegateHash
# update is prudent to ensure all configs have an input
# (and hence, all configs will be initialized)
- @config = config.update(self.class.configurations)
- config.bind(self)
+ @config = config.update.bind(self)
else
initialize_config(config)
end
+ # setup class dependencies
self.class.dependencies.each do |dependency_class|
depends_on(dependency_class.instance)
end
workflow
\ No newline at end of file