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