Class: Roby::TaskArguments

Inherits:
Object show all
Includes:
Enumerable, DRoby::V5::TaskArgumentsDumper
Defined in:
lib/roby/task_arguments.rb,
lib/roby/droby/enable.rb

Overview

Management of task arguments

This class essentially behaves like a Hash, but with two important caveats:

  • already set values cannot be modified

  • some special values (“delayed arguments”) allow to provide arguments whose evaluation is delayed until the value is needed.

Write-Once Values

Once set to a plain object (not a delayed argument, see below), a value cannot be changed. Some methods allow to bypass the relevant checks, but these methods must be considered internal

Delayed Arguments

Objects that respond to a #evaluate_delayed_argument method are handled differently by TaskArguments. They are considered “delayed arguments”, that is arguments that are essentially not set yet, but can be evaluated later (usually at execution time) to determine the argument value.

These objects must follow the DelayedArgument interface

These delayed arguments are handled differently than “plain” arguments in a few ways:

  • standard hash access methods such as #[] will “hide” them, that is return nil instead of the object

  • they can be overriden once set using e.g. #[]= or #merge!, unlike “plain arguments”

In addition, Roby provides a mechanism to fine-tune the way delayed arguments are merged. The #semantic_merge! call delegates the merge to the delayed arguments, allowing for instance two default argument to be merged if they have the same value.

Defined Under Namespace

Classes: StaticArgumentWrapper

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from DRoby::V5::TaskArgumentsDumper

#droby_dump

Methods included from Enumerable

#empty?

Constructor Details

#initialize(task) ⇒ TaskArguments

Returns a new instance of TaskArguments.



43
44
45
46
47
48
# File 'lib/roby/task_arguments.rb', line 43

def initialize(task)
    @task   = task
    @static = true
    @values = {}
    super()
end

Instance Attribute Details

#taskObject (readonly)

Returns the value of attribute task.



41
42
43
# File 'lib/roby/task_arguments.rb', line 41

def task
  @task
end

#valuesObject (readonly)

Returns the value of attribute values.



41
42
43
# File 'lib/roby/task_arguments.rb', line 41

def values
  @values
end

Class Method Details

.delayed_argument?(obj) ⇒ Boolean

Checks whether the given object is a delayed argument object

Returns:

  • (Boolean)

    true if the object has an evaluate_delayed_argument method



53
54
55
# File 'lib/roby/task_arguments.rb', line 53

def self.delayed_argument?(obj)
    obj.respond_to?(:evaluate_delayed_argument)
end

Instance Method Details

#==(other) ⇒ Boolean

True if the arguments are equal

Both proper values and delayed values have to be equal

Returns:

  • (Boolean)


146
147
148
# File 'lib/roby/task_arguments.rb', line 146

def ==(other)
    to_hash == other.to_hash
end

#[](key) ⇒ Object?

Return a set value for the given key

Parameters:

  • key (Symbol)

Returns:

  • (Object, nil)

    return the set value for key, or nil if the value is either a delayed argument object (e.g. a default value) or if no value is set at all



390
391
392
393
394
# File 'lib/roby/task_arguments.rb', line 390

def [](key)
    key = warn_deprecated_non_symbol_key(key)
    value = values[key]
    value unless TaskArguments.delayed_argument?(value)
end

#[]=(key, value) ⇒ Object

Assigns a value to a given argument name

The method validates that writing this argument value is allowed. Only values that have not been set, or have been set with a delayed argument, canb e updated

Raises:

  • NotMarshallable if the new values cannot be marshalled with DRoby. All task arguments must be marshallable

  • OwnershipError if we don’t own the task

  • ArgumentError if the argument is already set



352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
# File 'lib/roby/task_arguments.rb', line 352

def []=(key, value)
    key = warn_deprecated_non_symbol_key(key)
    if writable?(key, value)
        if !value.droby_marshallable?
            raise NotMarshallable, "values used as task arguments must be "\
                "marshallable, attempting to set #{key} to #{value} of "\
                "class #{value.class}, which is not"
        elsif !task.read_write?
            raise OwnershipError, "cannot change the argument set of a task "\
                "which is not owned #{task} is owned by #{task.owners} and "\
                "#{task.plan} by #{task.plan.owners}"
        end

        if TaskArguments.delayed_argument?(value)
            @static = false
        elsif values.key?(key) && TaskArguments.delayed_argument?(values[key])
            update_static = true
        end

        values[key] = value
        task.plan.log(:task_arguments_updated, task, key, value)

        if update_static
            @static = values.all? { |k, v| !TaskArguments.delayed_argument?(v) }
        end
        value
    else
        raise ArgumentError, "cannot override task argument #{key} as it is "\
            "already set to #{values[key]}"
    end
end

#assigned?(key) ⇒ Boolean

Tests if a given argument has been assigned, that is either has a static value or has a delayed value object

Returns:

  • (Boolean)


131
132
133
# File 'lib/roby/task_arguments.rb', line 131

def assigned?(key)
    key?(key)
end

#assigned_argumentsHash

Returns the set of arguments for which a proper value has been assigned

Returns:

  • (Hash)

See Also:



168
169
170
171
172
173
174
# File 'lib/roby/task_arguments.rb', line 168

def assigned_arguments
    result = {}
    each_assigned_argument do |k, v|
        result[k] = v
    end
    result
end

#can_semantic_merge?(other_args) ⇒ Boolean

Checks whether self can be merged with other_args through #semantic_merge!

Parameters:

Returns:

  • (Boolean)

See Also:



259
260
261
# File 'lib/roby/task_arguments.rb', line 259

def can_semantic_merge?(other_args)
    semantic_merge_blockers(other_args).empty?
end

#dupObject



121
122
123
# File 'lib/roby/task_arguments.rb', line 121

def dup
    self.to_hash
end

#each(&block) ⇒ Object



201
202
203
# File 'lib/roby/task_arguments.rb', line 201

def each(&block)
    values.each(&block)
end

#each_assigned_argument {|name, arg| ... } ⇒ Object

Enumerates assigned arguments that are not delayed arguments

Yield Parameters:

  • name (Symbol)

    the argument name

  • arg (Object)

    the argument value

See Also:



181
182
183
184
185
186
187
# File 'lib/roby/task_arguments.rb', line 181

def each_assigned_argument
    return assigned_arguments unless block_given?

    each do |key, value|
        yield(key, value) unless TaskArguments.delayed_argument?(value)
    end
end

#each_delayed_argument {|name, arg| ... } ⇒ Object

Enumerates delayed arguments

Yield Parameters:

  • name (Symbol)

    the argument name

  • arg (Object)

    the argument value



193
194
195
196
197
198
199
# File 'lib/roby/task_arguments.rb', line 193

def each_delayed_argument
    return enum_for(__method__) unless block_given?

    each do |key, value|
        yield(key, value) if TaskArguments.delayed_argument?(value)
    end
end

#evaluate_delayed_argumentsHash

Returns this argument set, but with the delayed arguments evaluated

Returns:

  • (Hash)


399
400
401
402
403
404
405
406
407
408
409
410
411
# File 'lib/roby/task_arguments.rb', line 399

def evaluate_delayed_arguments
    result = {}
    values.each do |key, val|
        if TaskArguments.delayed_argument?(val)
            catch(:no_value) do
                result[key] = val.evaluate_delayed_argument(task)
            end
        else
            result[key] = val
        end
    end
    result
end

#force_merge!(hash) ⇒ Object

Merge a hash into the arguments, updating existing values

Unlike #merge!, this will update existing values. You should not do it, unless you know what you’re doing.

Raises:

  • NotMarshallable if the new values cannot be marshalled with DRoby. All task arguments must be marshallable



420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
# File 'lib/roby/task_arguments.rb', line 420

def force_merge!(hash)
    hash.each do |key, value|
        unless value.droby_marshallable?
            raise NotMarshallable, "values used as task arguments must "\
                "be marshallable, attempting to set #{key} to #{value}, "\
                "which is not"
        end
    end

    if task.plan&.executable?
        values.merge!(hash) do |k, _, v|
            task.plan.log(:task_arguments_updated, task, k, v)
            v
        end
    else
        values.merge!(hash)
    end
    @static = values.all? { |k, v| !TaskArguments.delayed_argument?(v) }
end

#has_key?(key) ⇒ Boolean

Deprecated.

use #key? instead

Returns:

  • (Boolean)


79
80
81
# File 'lib/roby/task_arguments.rb', line 79

def has_key?(key)
    values.key?(key)
end

#key?(key) ⇒ Boolean

True if an argument with that name is assigned, be it a proper value or a delayed value object. This is an alias to #assigned?

This is implemented to be consistent with the Hash API. However, because of the semantics of delayed value objects, prefer #set? and #assigned?

Returns:

  • (Boolean)


89
90
91
# File 'lib/roby/task_arguments.rb', line 89

def key?(key)
    values.key?(key)
end

#keysObject

The set of argument names that have been assigned so far, either with a proper object or a delayed value object



95
96
97
# File 'lib/roby/task_arguments.rb', line 95

def keys
    values.keys
end

#merge!(hash) ⇒ Object

Merge a hash into the arguments

Only arguments that are unset or are currently delayed arguments (such as default arguments) can be updated. If the caller tries ot update other arguments, the method will raise

Raises:

  • NotMarshallable if the new values cannot be marshalled with DRoby. All task arguments must be marshallable

  • ArgumentError if the merge would modify an existing value



449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
# File 'lib/roby/task_arguments.rb', line 449

def merge!(hash)
    hash.each do |key, value|
        unless value.droby_marshallable?
            raise NotMarshallable, "values used as task arguments must "\
                "be marshallable, attempting to set #{key} to #{value}, "\
                "which is not"
        end
    end

    values.merge!(hash) do |key, old, new|
        if old == new then old
        elsif writable?(key, new)
            task.plan.log(:task_arguments_updated, task, key, new)
            new
        else
            raise ArgumentError, "cannot override task argument #{key}: "\
                "trying to replace #{old} by #{new}"
        end
    end
    @static = values.all? { |k, v| !TaskArguments.delayed_argument?(v) }
    self
end

#pretty_print(pp) ⇒ Object

Pretty-prints this argument set



151
152
153
154
155
156
157
# File 'lib/roby/task_arguments.rb', line 151

def pretty_print(pp)
    pp.seplist(values) do |keyvalue|
        key, value = *keyvalue
        pp.text "#{key}: "
        value.pretty_print(pp)
    end
end

#raw_get(key) ⇒ Object

Return the value stored for the given key as-is

Unlike #[], it does not filter out delayed arguments



74
75
76
# File 'lib/roby/task_arguments.rb', line 74

def raw_get(key)
    values[key]
end

#semantic_merge!(other_args) ⇒ Object

Merging method that takes delayed arguments into account

Unlike #merge, this method will let delayed arguments “merge themselves”, by delegating to their #merge method. It allows to e.g. propagating default arguments in the merge chain if they are the same.



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
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
# File 'lib/roby/task_arguments.rb', line 271

def semantic_merge!(other_args)
    current_values = values.dup
    other_task = other_args.task
    values.merge!(other_args.values) do |name, arg, other_arg|
        self_delayed  = TaskArguments.delayed_argument?(arg)
        other_delayed = TaskArguments.delayed_argument?(other_arg)

        if self_delayed && other_delayed
            if !(arg.strong? ^ other_arg.strong?)
                arg.merge(task, other_task, other_arg)
            elsif arg.strong?
                arg
            else
                other_arg
            end
        elsif self_delayed
            if arg.strong?
                arg.merge(task, other_task,
                          StaticArgumentWrapper.new(other_arg))
            else
                other_arg
            end
        elsif other_delayed
            if other_arg.strong?
                other_arg.merge(other_task, task,
                                StaticArgumentWrapper.new(arg))
            else
                arg
            end
        else
            arg
        end
    end
    current_values.each do |k, v|
        if (new_value = values[k]) != v
            task.plan.log(:task_arguments_updated, task, k, new_value)
        end
    end
    (values.keys - current_values.keys).each do |new_k|
        task.plan.log(:task_arguments_updated, task, new_k, values[new_k])
    end
    @static = values.each_value.none? { |v| TaskArguments.delayed_argument?(v) }
    self
end

#semantic_merge_blockers(other_args) ⇒ {Symbol => [Object, Object]}

Return the set of arguments that won’t be merged by #semantic_merge!

Parameters:

Returns:

  • ({Symbol => [Object, Object]})

    the arguments that can’t be merged



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
# File 'lib/roby/task_arguments.rb', line 217

def semantic_merge_blockers(other_args)
    blockers = values.find_all do |name, arg|
        next(false) unless other_args.key?(name)

        other_arg = other_args.values[name]

        self_delayed  = TaskArguments.delayed_argument?(arg)
        other_delayed = TaskArguments.delayed_argument?(other_arg)
        next(arg != other_arg) unless self_delayed || other_delayed

        if self_delayed && other_delayed
            if !(arg.strong? ^ other_arg.strong?)
                !arg.can_merge?(task, other_args.task,
                                other_arg)
            else
                false
            end
        elsif self_delayed
            if arg.strong?
                !arg.can_merge?(task, other_args.task,
                                StaticArgumentWrapper.new(other_arg))
            else
                false
            end
        elsif other_delayed
            if other_arg.strong?
                !other_arg.can_merge?(other_args.task, task,
                                      StaticArgumentWrapper.new(arg))
            else
                false
            end
        end
    end
    blockers.each_with_object({}) do |(name, self_obj), h|
        h[name] = [self_obj, other_args[name]]
    end
end

#set?(key) ⇒ Boolean

Tests if a given argument has been set with a proper value (not a delayed value object)

Returns:

  • (Boolean)


137
138
139
# File 'lib/roby/task_arguments.rb', line 137

def set?(key)
    key?(key) && !TaskArguments.delayed_argument?(values.fetch(key))
end

#slice(*args) ⇒ Object

Returns the listed set of arguments

Delayed arguments are evaluated before it is sliced

Parameters:

  • args (Array<Symbol>)

    the argument names



117
118
119
# File 'lib/roby/task_arguments.rb', line 117

def slice(*args)
    evaluate_delayed_arguments.slice(*args)
end

#static?Boolean

True if all the set arguments are plain (not delayed) arguments

Returns:

  • (Boolean)


58
59
60
# File 'lib/roby/task_arguments.rb', line 58

def static?
    @static
end

#to_hashObject



125
126
127
# File 'lib/roby/task_arguments.rb', line 125

def to_hash
    values.dup
end

#to_sObject



159
160
161
# File 'lib/roby/task_arguments.rb', line 159

def to_s
    values.sort_by(&:first).map { |k, v| "#{k}: #{v}" }.join(", ")
end

#update!(key, value) ⇒ Object

Updates the given argument, regardless of whether it is allowed or not

Parameters:

  • key (Symbol)

    the argument name

  • value (Object)

    the new argument value

Returns:

See Also:



322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
# File 'lib/roby/task_arguments.rb', line 322

def update!(key, value)
    if values.key?(key)
        current_value = values[key]
        is_updated    = (current_value != value)
        update_static = TaskArguments.delayed_argument?(current_value)
    else
        is_updated = true
    end

    values[key] = value
    if is_updated
        task.plan.log(:task_arguments_updated, task, key, value)
    end
    if TaskArguments.delayed_argument?(value)
        @static = false
    elsif update_static
        @static = values.all? { |k, v| !TaskArguments.delayed_argument?(v) }
    end
end

#writable?(key, value) ⇒ Boolean

True if it is possible to write the given value to the given argument

Parameters:

  • key (Symbol)

    the argument name

  • value (Object)

    the new argument value

Returns:

  • (Boolean)


103
104
105
106
107
108
109
110
# File 'lib/roby/task_arguments.rb', line 103

def writable?(key, value)
    if key?(key)
        !task.model.arguments.include?(key) ||
            TaskArguments.delayed_argument?(values[key])
    else
        true
    end
end