lib/tap/task.rb in bahuvrihi-tap-0.10.7 vs lib/tap/task.rb in bahuvrihi-tap-0.10.8
- old
+ new
@@ -1,7 +1,9 @@
require 'tap/support/executable'
require 'tap/support/lazydoc/method'
+require 'tap/support/lazydoc/definition'
+require 'tap/support/intern'
autoload(:OptionParser, 'optparse')
module Tap
# Tasks are the basic organizational unit of Tap. Tasks provide
@@ -104,11 +106,11 @@
# Tasks can be assembled into batches that enque and execute collectively.
# Batched tasks are often alternatively-configured derivatives of one
# parent task, although they can be manually assembled using Task.batch.
#
# app = Tap::App.instance
- # t1 = Tap::Task.new(:key => 'one') do |task, input|
+ # t1 = Tap::Task.intern(:key => 'one') do |task, input|
# input + task.config[:key]
# end
# t1.batch # => [t1]
#
# t2 = t1.initialize_batch_obj(:key => 'two')
@@ -159,68 +161,47 @@
class Task
include Support::Configurable
include Support::Executable
class << self
- # Returns the default name for the class: to_s.underscore
- attr_accessor :default_name
-
# Returns class dependencies
attr_reader :dependencies
+ # Returns the default name for the class: to_s.underscore
+ attr_writer :default_name
+
+ def default_name
+ # lazy-setting default_name like this (rather than
+ # within inherited, for example) is an optimization
+ # since many subclass operations end up setting
+ # default_name themselves.
+ @default_name ||= to_s.underscore
+ 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
+ end
+
def inherited(child)
unless child.instance_variable_defined?(:@source_file)
caller.first =~ Support::Lazydoc::CALLER_REGEXP
child.instance_variable_set(:@source_file, File.expand_path($1))
end
- child.instance_variable_set(:@default_name, child.to_s.underscore)
child.instance_variable_set(:@dependencies, dependencies.dup)
super
end
- #--
- # use with caution... should reset dependencies?
- attr_writer :instance
-
- # 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
- end
-
- # Generates or updates the specified subclass of self.
- def subclass(const_name, configs={}, dependencies=[], options={}, &block)
- #
- # Lookup or create the subclass constant.
- #
-
- current, constants = const_name.to_s.constants_split
- subclass = if constants.empty?
- # The constant exists; validate the constant is a subclass of self.
- unless current.kind_of?(Class) && current.ancestors.include?(self)
- raise ArgumentError, "#{current} is already defined and is not a subclass of #{self}!"
- end
- current
- else
- # Generate the nesting module
- subclass_const = constants.pop
- constants.each {|const| current = current.const_set(const, Module.new)}
-
- # Create and set the subclass constant
- current.const_set(subclass_const, Class.new(self))
+ def intern(*args, &block)
+ instance = new(*args)
+ if block_given?
+ instance.extend Support::Intern
+ instance.process_block = block
end
-
- #
- # Define the subclass
- #
-
- subclass.define_configurations(configs)
- subclass.define_dependencies(dependencies)
- subclass.define_process(block) if block_given?
- subclass.default_name = subclass.to_s.underscore
- subclass
+ instance
end
# Parses the argv into an instance of self and an array of arguments (implicitly
# to be enqued to the instance and run by app). Yields a help string to the
# block when the argv indicates 'help'.
@@ -298,14 +279,11 @@
end
path_configs = path_configs[0]
end
obj.reconfigure(path_configs).reconfigure(argv_config)
- # recollect arguments
- argv = (argv + use_args).collect {|str| str =~ /\A---\s*\n/ ? YAML.load(str) : str }
-
- [obj, argv]
+ [obj, (argv + use_args)]
end
def execute(argv=ARGV)
instance, args = parse(ARGV) do |help|
puts help
@@ -336,82 +314,146 @@
}
def help
Tap::Support::Templater.new(DEFAULT_HELP_TEMPLATE, :task_class => self).build
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.
- def depends_on(dependency_class, *args)
- unless dependency_class.respond_to?(:instance)
- raise ArgumentError, "dependency_class does not respond to instance: #{dependency_class}"
+ def depends_on(name, dependency_class)
+ unless dependencies.include?(dependency_class)
+ dependencies << dependency_class
end
- (dependencies << [dependency_class, args]).uniq!
- self
- end
-
- protected
-
- def dependency(name, dependency_class, *args)
- depends_on(dependency_class, *args)
-
+
+ # returns the resolved result of the dependency
define_method(name) do
- index = app.dependencies.index(dependency_class.instance, args)
- app.dependencies.resolve([index])
- app.dependencies.results[index]._current
+ instance = dependency_class.instance
+ instance.resolve
+ instance._result._current
end
public(name)
+ self
end
- def define(name, klass=Tap::Task, &block)
- instance_var = "@#{name}".to_sym
+ # Defines a task subclass with the specified configurations and process block.
+ # During initialization the subclass is instantiated and made accessible
+ # through a reader by the specified name.
+ #
+ # Defined tasks may be configured during initialization, through config, or
+ # directly through the instance; in effect you get tasks with nested configs
+ # which greatly facilitates workflows. Indeed, defined tasks are often
+ # joined in the workflow method.
+ #
+ # class AddALetter < Tap::Task
+ # config :letter, 'a'
+ # def process(input); input << letter; end
+ # end
+ #
+ # class AlphabetSoup < Tap::Task
+ # define :a, AddALetter, {:letter => 'a'}
+ # define :b, AddALetter, {:letter => 'b'}
+ # define :c, AddALetter, {:letter => 'c'}
+ #
+ # def workflow
+ # a.sequence(b, c)
+ # end
+ #
+ # def process
+ # a.execute("")
+ # end
+ # end
+ #
+ # AlphabetSoup.new.process # => 'abc'
+ #
+ # i = AlphabetSoup.new(:a => {:letter => 'x'}, :b => {:letter => 'y'}, :c => {:letter => 'z'})
+ # i.process # => 'xyz'
+ #
+ # i.config[:a] = {:letter => 'p'}
+ # i.config[:b][:letter] = 'q'
+ # i.c.letter = 'r'
+ # i.process # => 'pqr'
+ #
+ # ==== Usage
+ #
+ # Define is basically the equivalent of:
+ #
+ # class Sample < Tap::Task
+ # Name = baseclass.subclass(config, &block)
+ #
+ # # accesses an instance of Name
+ # attr_reader :name
+ #
+ # # register name as a config, but with a
+ # # non-standard reader and writer
+ # config :name, {}, {:reader => :name_config, :writer => :name_config=}.merge(options)
+ #
+ # # reader for name.config
+ # def name_config; ...; end
+ #
+ # # reconfigures name with input
+ # def name_config=(input); ...; end
+ #
+ # def initialize(*args)
+ # super
+ # @name = Name.new(config[:name])
+ # end
+ # end
+ #
+ # Note the following:
+ # * define will set a constant like name.camelize
+ # * the block defines the process method in the subclass
+ # * three methods are created by define: name, name_config, name_config=
+ #
+ def define(name, baseclass=Tap::Task, configs={}, options={}, &block)
+ # define the subclass
+ const_name = options.delete(:const_name) || name.to_s.camelize
+ subclass = const_set(const_name, Class.new(baseclass))
+ subclass.default_name = name.to_s
- define_method(name) do |*args|
- raise ArgumentError, "wrong number of arguments (#{args.length} for 1)" if args.length > 1
-
- instance_name = args[0] || name
- instance_variable_set(instance_var, {}) unless instance_variable_defined?(instance_var)
- instance_variable_get(instance_var)[instance_name] ||= config_task(instance_name, klass, &block)
+ configs.each_pair do |key, value|
+ subclass.send(:config, key, value)
end
- define_method("#{name}=") do |input|
- input = {name => input} unless input.kind_of?(Hash)
- instance_variable_set(instance_var, input)
+ if block_given?
+ subclass.send(:define_method, :process, &block)
end
- public(name, "#{name}=")
- end
-
- def define_configurations(configs)
- case configs
- when Hash
- # hash configs are simply added as default configurations
- attr_accessor(*configs.keys)
- configs.each_pair do |key, value|
- configurations.add(key, value)
+ # 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
- public(*configs.keys)
- when Array
- # array configs define configuration methods
- configs.each do |method, key, value, opts, config_block|
- send(method, key, value, opts, &config_block)
- end
- else
- raise ArgumentError, "cannot define configurations from: #{configs}"
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
+ options[:desc] = desc
+ end
+
+ configurations.add(name, subclass.configurations.instance_config, options)
end
-
- def define_dependencies(dependencies)
- dependencies.each do |name, dependency_class, args|
- dependency(name, dependency_class, *(args ? args : []))
- end if dependencies
- end
-
- def define_process(block)
- send(:define_method, :process, &block)
- end
end
instance_variable_set(:@source_file, __FILE__)
instance_variable_set(:@default_name, 'tap/task')
instance_variable_set(:@dependencies, [])
@@ -421,40 +463,37 @@
# 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.
attr_accessor :name
-
- # The task block provided during initialization.
- attr_reader :task_block
# Initializes a new instance and associated batch objects. Batch
# objects will be initialized for each configuration template
# specified by app.each_config_template(config_file) where
# config_file = app.config_filepath(name).
- def initialize(config={}, name=nil, app=App.instance, &task_block)
+ def initialize(config={}, name=nil, app=App.instance)
super()
@name = name || self.class.default_name
- @task_block = (task_block == nil ? default_task_block : task_block)
-
@app = app
@_method_name = :execute_with_callbacks
@on_complete_block = nil
@dependencies = []
@batch = [self]
case config
- when Support::InstanceConfiguration
- @config = config
+ when Support::InstanceConfiguration
+ # 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)
else
initialize_config(config)
end
- self.class.dependencies.each do |task_class, args|
- depends_on(task_class.instance, *args)
+ self.class.dependencies.each do |dependency_class|
+ depends_on(dependency_class.instance)
end
workflow
end
@@ -490,49 +529,20 @@
# t = TaskWithTwoInputs.new
# t.enq(1,2).enq(3,4)
# t.app.run
# t.app.results(t) # => [[2,1], [4,3]]
#
- # By default process passes self and the input(s) to the task_block
- # provided during initialization. In this case the task block dictates
- # the number of arguments enq should receive. Simply returns the inputs
- # if no task_block is set.
- #
- # # two arguments in addition to task are specified
- # # so this Task must be enqued with two inputs...
- # t = Task.new {|task, a, b| [b,a] }
- # t.enq(1,2).enq(3,4)
- # t.app.run
- # t.app.results(t) # => [[2,1], [4,3]]
- #
+ # By default, process simply returns the inputs.
def process(*inputs)
- return inputs if task_block == nil
- inputs.unshift(self)
-
- arity = task_block.arity
- n = inputs.length
- unless n == arity || (arity < 0 && (-1-n) <= arity)
- raise ArgumentError.new("wrong number of arguments (#{n} for #{arity})")
- end
-
- task_block.call(*inputs)
+ inputs
end
# Logs the inputs to the application logger (via app.log)
def log(action, msg="", level=Logger::INFO)
# TODO - add a task identifier?
app.log(action, msg, level)
end
-
- # Raises a TerminateError if app.state == State::TERMINATE.
- # check_terminate may be called at any time to provide a
- # breakpoint in long-running processes.
- def check_terminate
- if app.state == App::State::TERMINATE
- raise App::TerminateError.new
- end
- end
# Returns self.name
def to_s
name.to_s
end
@@ -542,16 +552,11 @@
def inspect
"#<#{self.class.to_s}:#{object_id} #{name} #{config.to_hash.inspect} >"
end
protected
-
- # Hook to set a default task block. By default, nil.
- def default_task_block
- nil
- end
-
+
# Hook to define a workflow for defined tasks.
def workflow
end
# Hook to execute code before inputs are processed.
@@ -576,15 +581,9 @@
on_execute_error($!)
end
after_execute
result
- end
-
- def config_task(name, klass=Tap::Task, &block)
- configs = config[name] || {}
- raise ArgumentError, "config '#{name}' is not a hash" unless configs.kind_of?(Hash)
- klass.new(configs, name, &block)
end
end
end
\ No newline at end of file