def test_get_accessed_parameters(self): self.assertEqual(get_accessed_parameters("some text"), frozenset()) self.assertEqual(get_accessed_parameters("some {parametric} text"), frozenset(['parametric'])) self.assertEqual(get_accessed_parameters("some {} text"), frozenset([''])) self.assertEqual(get_accessed_parameters("some {1} {2} {3} text"), frozenset(['1', '2', '3']))
def check(self, parent, unit, field): # Non-parametric units are always valid if unit.is_parametric: value = unit._data.get(field) # No value? No problem! if value is None: return if unit.template_engine == 'jinja2': param_set = get_accessed_parameters(value, template_engine='jinja2') else: param_set = get_accessed_parameters(value) # Invariant fields cannot depend on any parameters if len(param_set) != 0: yield parent.error(unit, field, Problem.variable, self.message)
def get_accessed_parameters(self, *, force=False): """ Get a set of attributes accessed from each template attribute :param force (keyword-only): If specified then it will operate despite being invoked on a non-parametric unit. This is only intended to be called by TemplateUnit to inspect what the generated unit looks like in the early validation code. :returns: A dictionary of sets with names of attributes accessed by each template field. Note that for non-parametric Units the return value is always a dictionary of empty sets, regardless of how they actual parameter values look like. This function computes a dictionary of sets mapping from each template field (except from fields starting with the string 'template-') to a set of all the resource object attributes accessed by that element. """ if force or self.is_parametric: return { key: get_accessed_parameters(value) for key, value in self._data.items() } else: return {key: frozenset() for key in self._data}
def check(self, parent, unit, field): # Non-parametric units are always valid if unit.is_parametric: value = unit._data.get(field) # No value? No problem! if value is not None: if unit.template_engine == 'jinja2': param_set = get_accessed_parameters( value, template_engine='jinja2') else: param_set = get_accessed_parameters(value) # Variant fields must depend on some parameters if len(param_set) == 0: yield parent.error(unit, field, Problem.constant, self.message) # Each parameter must be present in the unit for param_name in param_set: if param_name not in unit.parameters: message = _("reference to unknown parameter {!r}" ).format(param_name) yield parent.error(unit, field, Problem.unknown_param, message)
def instantiate_one(self, resource, unit_cls_hint=None, index=0): """ Instantiate a single job out of a resource and this template. :param resource: A Resource object to provide template data :param unit_cls_hint: A unit class to instantiate :param index: An integer parameter representing the current loop index :returns: A new JobDefinition created out of the template and resource data. :raises AttributeError: If the template referenced a value not defined by the resource object. Fields starting with the string 'template-' are discarded. All other fields are interpolated by attributes from the resource object. References to missing resource attributes cause the process to fail. """ # Look up the unit we're instantiating if unit_cls_hint is not None: unit_cls = unit_cls_hint else: unit_cls = self.get_target_unit_cls() assert unit_cls is not None # Filter out template- data fields as they are not relevant to the # target unit. data = { key: value for key, value in self._data.items() if not key.startswith('template-') } raw_data = { key: value for key, value in self._raw_data.items() if not key.startswith('template-') } # Override the value of the 'unit' field from 'template-unit' field data['unit'] = raw_data['unit'] = self.template_unit # XXX: extract raw dictionary from the resource object, there is no # normal API for that due to the way resource objects work. parameters = dict(object.__getattribute__(resource, '_data')) accessed_parameters = set( itertools.chain( *{get_accessed_parameters(value) for value in data.values()})) # Recreate the parameters with only the subset that will actually be # used by the template. Doing this filter can prevent exceptions like # DependencyDuplicateError where an unused resource property can differ # when resuming and bootstrapping sessions, causing job checksums # mismatches. # See https://bugs.launchpad.net/bugs/1561821 parameters = { k: v for k, v in parameters.items() if k in accessed_parameters } # Add the special __index__ to the resource namespace variables parameters['__index__'] = index # Instantiate the class using the instantiation API return unit_cls.instantiate_template(data, raw_data, self.origin, self.provider, parameters, self.field_offset_map)