lib/tap/task.rb in bahuvrihi-tap-0.10.2 vs lib/tap/task.rb in bahuvrihi-tap-0.10.3
- old
+ new
@@ -1,6 +1,8 @@
-require 'tap/support/framework'
+require 'tap/support/batchable'
+require 'tap/support/executable'
+require 'tap/support/command_line'
module Tap
# Tasks are the basic organizational unit of Tap. Tasks provide
# a standard backbone for creating the working parts of an application
@@ -152,32 +154,322 @@
# t1 = SubclassTask.new
# t2 = t1.initialize_batch_obj
# t1.array == t2.array # => true
# t1.array.object_id == t2.array.object_id # => false
#
- class Task
+ class Task
+ include Support::Batchable
+ include Support::Configurable
include Support::Executable
- include Support::Framework
- attr_reader :task_block
+ class << self
+ # Returns the default name for the class: to_s.underscore
+ attr_accessor :default_name
+
+ # Returns class dependencies
+ attr_reader :dependencies
+
+ 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
+
+ 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))
+ end
+
+ #
+ # Define the subclass
+ #
+
+ subclass.define_configurations(configs)
+ subclass.define_dependencies(dependencies)
+ subclass.define_process(block) if block_given?
+
+ #
+ # Register documentation
+ #
+
+ const_name = current == Object ? subclass_const : "#{current}::#{subclass_const}"
+ caller.each_with_index do |line, index|
+ case line
+ when /\/tap\/support\/declarations.rb/ then next
+ when Support::Lazydoc::CALLER_REGEXP
+ subclass.source_file = File.expand_path($1)
+ lzd = subclass.lazydoc(false)
+ lzd[const_name, false]['manifest'] = lzd.register($3.to_i - 1)
+ break
+ end
+ end
+
+ arity = options[:arity] || (block_given? ? block.arity : -1)
+ comment = Support::Comment.new
+ comment.subject = case
+ when arity > 0
+ Array.new(arity, "INPUT").join(' ')
+ when arity < 0
+ array = Array.new(-1 * arity - 1, "INPUT")
+ array << "INPUTS..."
+ array.join(' ')
+ else ""
+ end
+ subclass.lazydoc(false)[const_name, false]['args'] ||= comment
+
+ subclass.default_name = const_name.underscore
+ subclass
+ end
+
+ def instantiate(argv, app=Tap::App.instance) # => instance, argv
+ opts = OptionParser.new
+
+ # Add configurations
+ config = {}
+ unless configurations.empty?
+ opts.separator ""
+ opts.separator "configurations:"
+ end
+
+ configurations.each do |receiver, key, configuration|
+ opts.on(*Support::CommandLine.configv(configuration)) do |value|
+ config[key] = value
+ end
+ end
+
+ # Add options on_tail, giving priority to configurations
+ opts.separator ""
+ opts.separator "options:"
+
+ opts.on_tail("-h", "--help", "Print this help") do
+ opts.banner = "#{help}usage: tap run -- #{to_s.underscore} #{args.subject}"
+ puts opts
+ exit
+ end
+
+ # Add option for name
+ name = default_name
+ opts.on_tail('--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
+ end
+
+ opts.parse!(argv)
+ 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|
+ obj.initialize_batch_obj(path_config, "#{name}_#{i}") unless i == 0
+ end
+ path_configs = path_configs[0]
+ end
+
+ argv = (argv + use_args).collect {|str| str =~ /\A---\s*\n/ ? YAML.load(str) : str }
+
+ [obj.reconfigure(path_configs).reconfigure(config), argv]
+ end
+
+ def lazydoc(resolve=true)
+ lazydoc = super(false)
+ lazydoc.register_method_pattern('args', :process) unless lazydoc.resolved?
+ super
+ end
+
+ DEFAULT_HELP_TEMPLATE = %Q{<% manifest = task_class.manifest %>
+<%= task_class %><%= manifest.subject.to_s.strip.empty? ? '' : ' -- ' %><%= manifest.subject %>
+
+<% unless manifest.empty? %>
+<%= '-' * 80 %>
+
+<% manifest.wrap(77, 2, nil).each do |line| %>
+ <%= line %>
+<% end %>
+<%= '-' * 80 %>
+<% end %>
+
+}
+ def help
+ Tap::Support::Templater.new(DEFAULT_HELP_TEMPLATE, :task_class => self).build
+ end
+
+ def depends_on(dependency_class, *args)
+ unless dependency_class.respond_to?(:instance)
+ raise ArgumentError, "dependency_class does not respond to instance: #{dependency_class}"
+ end
+ (dependencies << [dependency_class, args]).uniq!
+ self
+ end
+
+ protected
+
+ def dependency(name, dependency_class, *args)
+ depends_on(dependency_class, *args)
+
+ define_method(name) do
+ index = Support::Executable.index(dependency_class.instance, args)
+ Support::Executable.results[index]._current
+ end
+
+ public(name)
+ end
+
+ def define(name, klass=Tap::Task, &block)
+ instance_var = "@#{name}".to_sym
+
+ 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)
+ end
+
+ define_method("#{name}=") do |input|
+ input = {name => input} unless input.kind_of?(Hash)
+ instance_variable_set(instance_var, input)
+ 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)
+ 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
+ end
+
+ def define_dependencies(dependencies)
+ dependencies.each do |name, dependency_class, *args|
+ dependency(name, dependency_class, *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, [])
+ lazy_attr :manifest
+ lazy_attr :args
+
+ # The application used to load config_file templates
+ # (and hence, to initialize batched objects).
+ attr_reader :app
+
+ # 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)
- super(config, name, app)
+ super()
+ @app = app
+ @name = name || self.class.default_name
@task_block = (task_block == nil ? default_task_block : task_block)
+
+ @_method_name = :execute
@multithread = false
@on_complete_block = nil
- @_method_name = :execute
+ @dependencies = []
+
+ case config
+ when Support::InstanceConfiguration
+ @config = config
+ config.bind(self)
+ else
+ initialize_config(config)
+ end
+
+ self.class.dependencies.each do |task_class, args|
+ depends_on(task_class.instance, *args)
+ end
end
+ # Creates a new batched object and adds the object to batch. The batched object
+ # will be a duplicate of the current object but with a new name and/or
+ # configurations.
+ def initialize_batch_obj(overrides={}, name=nil)
+ obj = super().reconfigure(overrides)
+ obj.name = name if name
+ obj
+ end
+
# Enqueues self and self.batch to app with the inputs.
# The number of inputs provided should match the number
# of inputs specified by the arity of the _method_name method.
def enq(*inputs)
app.queue.enq(self, inputs)
end
-
+
batch_function :enq, :multithread=
batch_function(:on_complete) {}
# Executes self with the given inputs. Execute provides hooks for subclasses
# to insert standard execution code: before_execute, on_execute_error,
@@ -235,10 +527,30 @@
end
task_block.call(*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
+
protected
# Hook to set a default task block. By default, nil.
def default_task_block
nil
@@ -252,8 +564,16 @@
# Hook to handle unhandled errors from processing inputs on a task level.
# By default on_execute_error simply re-raises the unhandled error.
def on_execute_error(err)
raise err
+ end
+
+ private
+
+ 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