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