Class: Tap::Task

Inherits:
Object show all
Includes:
Support::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 with a custom block that gets called with 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.

Tap::Support::Validation pre-packages many common blocks which 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

Subclassing

Tasks can be subclassed normally, with one reminder related to batching.

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.

In these cases, the initialize_copy should be overridden and should re-initialize the appropriate variables. Be sure to call super to invoke the default initialize_copy:

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

Constant Summary collapse

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 %>

}

Class Attribute Summary collapse

Instance Attribute Summary collapse

Attributes included from Support::Executable

#_method_name, #app, #batch, #dependencies, #on_complete_block

Attributes included from Support::Configurable

#config

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

Methods included from Support::Configurable

included, #initialize_copy, #reconfigure

Constructor Details

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

Initializes a new Task.



444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
# File 'lib/tap/task.rb', line 444

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



132
133
134
135
136
137
138
# File 'lib/tap/task.rb', line 132

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



126
127
128
# File 'lib/tap/task.rb', line 126

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.



441
442
443
# File 'lib/tap/task.rb', line 441

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.



259
260
261
262
263
264
265
266
# File 'lib/tap/task.rb', line 259

def execute(argv=ARGV)
  instance, args = parse(ARGV) do |help|
    puts help
    exit
  end

  instance.execute(*args)
end

.helpObject

Returns the class help.



289
290
291
# File 'lib/tap/task.rb', line 289

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

.inherited(child) ⇒ Object

:nodoc:



146
147
148
149
150
151
152
153
154
# File 'lib/tap/task.rb', line 146

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



142
143
144
# File 'lib/tap/task.rb', line 142

def instance
  @instance ||= new
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.



161
162
163
164
165
166
167
168
# File 'lib/tap/task.rb', line 161

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

.lazydoc(resolve = true) ⇒ Object

Returns the class lazydoc, resolving if specified.



269
270
271
272
273
# File 'lib/tap/task.rb', line 269

def lazydoc(resolve=true)
  lazydoc = super(false)
  lazydoc[self.to_s]['args'] ||= lazydoc.register_method(:process, Support::Lazydoc::Method)
  super
end

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

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’.



173
174
175
# File 'lib/tap/task.rb', line 173

def parse(argv=ARGV, app=Tap::App.instance, &block) # :yields: help_str
  parse!(argv.dup, &block)
end

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

Same as parse, but removes switches destructively.



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
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
# File 'lib/tap/task.rb', line 178

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
  opts.separator ""
  opts.separator "options:"

  opts.on_tail("-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
  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
  
  # 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]
  end
  obj.reconfigure(path_configs).reconfigure(argv_config)
  
  [obj, (argv + use_args)]
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.



474
475
476
477
478
# File 'lib/tap/task.rb', line 474

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.



514
515
516
# File 'lib/tap/task.rb', line 514

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)



502
503
504
505
# File 'lib/tap/task.rb', line 502

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.



497
498
499
# File 'lib/tap/task.rb', line 497

def process(*inputs)
  inputs
end

#to_sObject

Returns self.name



508
509
510
# File 'lib/tap/task.rb', line 508

def to_s
  name.to_s
end