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