Class: Tap::Task

Inherits:
Object show all
Includes:
Configurable, Support::Executable
Defined in:
lib/tap/task.rb

Overview

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.

class NoInput < Tap::Task
  def process(); []; end
end

class OneInput < Tap::Task
  def process(input); [input]; end
end

class MixedInputs < Tap::Task
  def process(a, b, *args); [a,b,args]; end
end

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 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] }

no_inputs.execute                             # => []
one_input.execute(:a)                         # => [:a]
mixed_inputs.execute(:a, :b)                  # => [:a, :b, []]
mixed_inputs.execute(:a, :b, 1, 2, 3)         # => [:a, :b, [1,2,3]]

Configuration

Tasks are configurable. By default each task will be configured as specified in the class definition. Configurations may be accessed through config, or through accessors.

class ConfiguredTask < Tap::Task
  config :one, 'one'
  config :two, 'two'
end

t = ConfiguredTask.new
t.config                     # => {:one => 'one', :two => 'two'}
t.one                        # => 'one'
t.one = 'ONE'
t.config                     # => {:one => 'ONE', :two => 'two'}

Overrides and even unspecified configurations may be provided during initialization. Unspecified configurations do not have accessors.

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.

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)

  # integer config; string inputs are converted using YAML
  config :integer, 1, &c.yaml(Integer)
end 

t = ValidatingTask.new
t.string = 1           # !> ValidationError
t.integer = 1.1        # !> ValidationError

t.integer = "1"
t.integer == 1         # => true

See the Configurable documentation for more information.

Subclassing

Tasks may be subclassed normally, but be sure to call super as necessary, in particular when overriding the following methods:

class Subclass < Tap::Task
  class << self
    def inherited(child)
      super
    end
  end

  def initialize(*args)
    super
  end

  def initialize_copy(orig)
    super
  end
end

Constant Summary collapse

DEFAULT_HELP_TEMPLATE =
%Q{<% manifest = task_class.manifest %>
<%= task_class %><%= manifest.empty? ? '' : ' -- ' %><%= manifest.to_s %>

<% desc = manifest.kind_of?(Lazydoc::Comment) ? manifest.wrap(77, 2, nil) : [] %>
<% unless desc.empty? %>
<%= '-' * 80 %>

<% desc.each do |line| %>
  <%= line %>
<% end %>
<%= '-' * 80 %>
<% end %>

}

Class Attribute Summary collapse

Instance Attribute Summary collapse

Attributes included from Support::Executable

#app, #batch, #dependencies, #method_name, #on_complete_block

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Support::Executable

#_execute, #batch_index, #batch_with, #batched?, #check_terminate, #depends_on, #enq, #execute, #fork, initialize, #merge, #on_complete, #reset_dependencies, #resolve_dependencies, #sequence, #switch, #sync_merge, #unbatched_depends_on, #unbatched_enq, #unbatched_on_complete

Constructor Details

#initialize(config = {}, name = nil, app = App.instance) ⇒ Task

Initializes a new Task.



489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
# File 'lib/tap/task.rb', line 489

def initialize(config={}, name=nil, app=App.instance)
  super()

  @name = name || self.class.default_name
  @app = app
  @method_name = :execute_with_callbacks
  @on_complete_block = nil
  @dependencies = []
  @batch = [self]
  
  case config
  when DelegateHash
    # update is prudent to ensure all configs have an input
    # (and hence, all configs will be initialized)
    @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
end

Class Attribute Details

.default_nameObject

Returns the default name for the class: to_s.underscore



123
124
125
126
127
128
129
# File 'lib/tap/task.rb', line 123

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

.dependenciesObject (readonly)

Returns class dependencies



117
118
119
# File 'lib/tap/task.rb', line 117

def dependencies
  @dependencies
end

Instance Attribute Details

#nameObject

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.



486
487
488
# File 'lib/tap/task.rb', line 486

def name
  @name
end

Class Method Details

.execute(argv = ARGV) ⇒ Object

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.



224
225
226
227
# File 'lib/tap/task.rb', line 224

def execute(argv=ARGV)
  instance, args = parse(ARGV)
  instance.execute(*args)
end

.helpObject

Returns the class help.



245
246
247
# File 'lib/tap/task.rb', line 245

def help
  Tap::Support::Templater.new(DEFAULT_HELP_TEMPLATE, :task_class => self).build
end

.inherited(child) ⇒ Object

:nodoc:



137
138
139
140
141
142
143
144
145
# File 'lib/tap/task.rb', line 137

def inherited(child) # :nodoc:
  unless child.instance_variable_defined?(:@source_file)
    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

.instanceObject

Returns an instance of self; the instance is a kind of ‘global’ instance used in class-level dependencies. See depends_on.



133
134
135
# File 'lib/tap/task.rb', line 133

def instance
  @instance ||= new.extend(Support::Dependency)
end

.intern(*args, &block) ⇒ Object

Instantiates a new task with the input arguments and overrides process with the block. The block will be called with the instance, plus any inputs.

Simply instantiates a new task if no block is given.



152
153
154
155
156
157
158
159
# File 'lib/tap/task.rb', line 152

def intern(*args, &block) # :yields: task, inputs...
  instance = new(*args)
  if block_given?
    instance.extend Support::Intern
    instance.process_block = block
  end
  instance
end

.load(path, recursive = true) ⇒ Object

Recursively loads path into a nested configuration file. – TODO: move the logic of this to Configurable



252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
# File 'lib/tap/task.rb', line 252

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

.parse(argv = ARGV, app = Tap::App.instance) ⇒ Object

Parses the argv into an instance of self and an array of arguments (implicitly to be enqued to the instance).



163
164
165
# File 'lib/tap/task.rb', line 163

def parse(argv=ARGV, app=Tap::App.instance)
  parse!(argv.dup)
end

.parse!(argv = ARGV, app = Tap::App.instance) ⇒ Object

Same as parse, but removes switches destructively.



168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
# File 'lib/tap/task.rb', line 168

def parse!(argv=ARGV, app=Tap::App.instance)
  opts = ConfigParser.new
  opts.separator "configurations:"
  opts.add(configurations)
  
  opts.separator ""
  opts.separator "options:"
  
  # Add option to print help
  opts.on("-h", "--help", "Print this help") do
    prg = case $0
    when /rap$/ then 'rap'
    else 'tap run --'
    end
    
    puts "#{help}usage: #{prg} #{to_s.underscore} #{args}"
    puts          
    puts opts
    exit
  end
 
  # Add option to specify a config file
  name = default_name
  opts.on('--name NAME', 'Specify a name') do |value|
    name = value
  end
 
  # Add option to add args
  use_args = []
  opts.on('--use FILE', 'Loads inputs from file') do |path|
    use(path, use_args)
  end
  
  # build and reconfigure the instance and any associated
  # batch objects as specified in the file configurations
  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, (argv + use_args)]
end

.use(path, argv = ARGV) ⇒ Object

Loads the contents of path onto argv.



301
302
303
304
305
306
307
308
309
310
# File 'lib/tap/task.rb', line 301

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

Instance Method Details

#initialize_batch_obj(overrides = {}, name = nil) ⇒ Object

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.



519
520
521
522
523
# File 'lib/tap/task.rb', line 519

def initialize_batch_obj(overrides={}, name=nil)
  obj = super().reconfigure(overrides)
  obj.name = name if name
  obj 
end

#inspectObject

Provides an abbreviated version of the default inspect, with only the task class, object_id, name, and configurations listed.



559
560
561
# File 'lib/tap/task.rb', line 559

def inspect
  "#<#{self.class.to_s}:#{object_id} #{name} #{config.to_hash.inspect} >"
end

#log(action, msg = "", level = Logger::INFO) ⇒ Object

Logs the inputs to the application logger (via app.log)



547
548
549
550
# File 'lib/tap/task.rb', line 547

def log(action, msg="", level=Logger::INFO)
  # TODO - add a task identifier?
  app.log(action, msg, level)
end

#process(*inputs) ⇒ Object

The method for processing inputs into outputs. Override this method in subclasses to provide class-specific process logic. The number of arguments specified by process corresponds to the number of arguments the task should have when enqued or executed.

class TaskWithTwoInputs < Tap::Task
  def process(a, b)
    [b,a]
  end
end

t = TaskWithTwoInputs.new
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.



542
543
544
# File 'lib/tap/task.rb', line 542

def process(*inputs)
  inputs
end

#to_sObject

Returns self.name



553
554
555
# File 'lib/tap/task.rb', line 553

def to_s
  name.to_s
end