Source code for jube2.parameter

# JUBE Benchmarking Environment
# Copyright (C) 2008-2024
# Forschungszentrum Juelich GmbH, Juelich Supercomputing Centre
# http://www.fz-juelich.de/jsc/jube
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
"""Parameter related classes"""

from __future__ import (print_function,
                        unicode_literals,
                        division)

import itertools
import os
import xml.etree.ElementTree as ET
import copy
import jube2.util.util
import jube2.conf
import jube2.log
import re
import inspect

LOGGER = jube2.log.get_logger(__name__)

JUBE_MODE = "jube"
NEVER_MODE = "never"
STEP_MODE = "step"
CYCLE_MODE = "cycle"
ALWAYS_MODE = "always"
USE_MODE = "use"
UPDATE_MODES = (JUBE_MODE, NEVER_MODE, STEP_MODE,
                CYCLE_MODE, USE_MODE, ALWAYS_MODE)


[docs]class Parameterset(object): """A parameterset represent a template or a specific product space. It can be combined with other Parametersets.""" def __init__(self, name="", duplicate="replace"): self._name = name self._duplicate = duplicate self._parameters = dict()
[docs] def clear(self): """Remove all stored parameters""" self._parameters = dict()
[docs] def copy(self): """Returns a deepcopy of the Parameterset""" new_parameterset = Parameterset(self._name, self._duplicate) new_parameterset.add_parameterset(self) return new_parameterset
@property def name(self): """Return name of the Parameterset""" return self._name @property def duplicate(self): """Return the duplicate property of the Parameterset""" return self._duplicate @property def has_templates(self): """This Parameterset contains template paramters?""" for parameter in self._parameters.values(): if parameter.is_template: return True return False @property def parameter_dict(self): """Return dictionary name -> parameter""" return dict(self._parameters) @property def all_parameters(self): """Return list of all parameters""" return self._parameters.values() @property def all_parameter_names(self): """Return list of all parameter names""" return self._parameters.keys()
[docs] def add_parameterset(self, parameterset): """Add all parameters from given parameterset, existing ones will be overwritten""" for parameter in parameterset: self.add_parameter(parameter.copy()) return self
[docs] def update_parameterset(self, parameterset): """Overwrite existing parameters. Do not add new parameters""" for parameter in parameterset: if parameter.name in self: self._parameters[parameter.name] = parameter.copy()
[docs] def concat_parameter(self, parameter): """Concatenate a new parameter to a potentially existing one.""" if parameter.name in self._parameters.keys(): if self._parameters[parameter.name]._value == parameter._value: return parameter else: value=list(set(jube2.util.util.ensure_list(self._parameters[parameter.name]._value)+jube2.util.util.ensure_list(parameter._value))) value.sort() return jube2.parameter.TemplateParameter( parameter._name, value, parameter._separator, parameter._type, parameter._mode, parameter._unit, parameter._export, parameter._update_mode, parameter._idx, parameter._eval_helper, parameter._duplicate) else: return parameter
[docs] def check_parameter_options(self, parameter, only_duplicate=True): """Check whether both parameters have identical options and throw an error if this is not the case""" if only_duplicate: if parameter._duplicate != self._parameters[parameter.name]._duplicate: LOGGER.debug( "The duplicate options for the parameter {0} are stated at least twice differently leading to undefined behaviour.\n".format( parameter.name)) raise ValueError("The duplicate options for the parameter {0} are stated at least twice differently leading to undefined behaviour.".format(parameter.name)) else: if parameter._separator != self._parameters[parameter.name]._separator or \ parameter._type != self._parameters[parameter.name]._type or \ parameter._update_mode != self._parameters[parameter.name]._update_mode: LOGGER.debug( "At least one option (separator, type, update_mode) for the parameter {0} was defined at least twice differently leading to undefined behaviour.\n".format( parameter.name)) raise ValueError("At least one option (separator, type, update_mode) for the parameter {0} was defined at least twice differently leading to undefined behaviour.".format(parameter.name))
[docs] def add_parameter(self, parameter): """Add a new parameter""" if parameter.name not in self._parameters.keys(): self._parameters[parameter.name] = parameter else: # Check whether only the duplicate option of two parameters is # identical, otherwise the behaviour is undefined. self.check_parameter_options(parameter=parameter, only_duplicate=True) # check, which action to perform and prioritize the duplicate # option from the parameters over the duplicate option from # the parametersets raise_unknown_error=False if parameter._duplicate == "replace": self._parameters[parameter.name] = parameter elif parameter._duplicate == "concat": self.check_parameter_options(parameter=parameter, only_duplicate=False) self._parameters[parameter.name] = self.concat_parameter(parameter) elif parameter._duplicate == "error": if parameter.name in self._parameters.keys(): raise Exception("The parameter {0} was defined at least twice.".format(parameter.name)) else: self._parameters[parameter.name] = parameter elif parameter._duplicate == "none": if self._duplicate == "replace": self._parameters[parameter.name] = parameter elif self._duplicate == "concat": self.check_parameter_options(parameter=parameter, only_duplicate=False) self._parameters[parameter.name] = self.concat_parameter(parameter) elif self._duplicate == "error": if parameter.name in self._parameters.keys(): raise Exception("The parameter {0} was defined at least twice.".format(parameter.name)) else: self._parameters[parameter.name] = parameter else: # unknown error, this situation should never occur! raise_unknown_error=True else: # unknown error, this situation should never occur! raise_unknown_error=True if raise_unknown_error: raise Exception("The execution was aborted due to an unknown error "+ "when adding a parameter. Please contact the JUBE developers "+ "to resolve this situation.")
[docs] def delete_parameter(self, parameter): """Delete a parameter""" name = "" if isinstance(parameter, Parameter): name = parameter.name else: name = parameter if name in self._parameters: del self._parameters[name]
@property def constant_parameter_dict(self): """Return dictionary representation of all constant parameters""" return dict([(parameter.name, parameter) for parameter in self._parameters.values() if (not parameter.is_template) and (parameter.mode not in jube2.conf.ALLOWED_SCRIPTTYPES.union( jube2.conf.ALLOWED_ADVANCED_MODETYPES))]) @property def template_parameter_dict(self): """Return dictionary representation of all template parameters""" return dict([(parameter.name, parameter) for parameter in self._parameters.values() if parameter.is_template]) @property def export_parameter_dict(self): """Return dictionary representation of all export parameters""" return dict([(parameter.name, parameter) for parameter in self._parameters.values() if (not parameter.is_template) and parameter.export])
[docs] def get_updatable_parameter(self, mode, keep_index=False): """Returns a parameterset containing all updatable parameter for a specific mode, the root parameter is added""" parameterset = Parameterset() for parameter in self._parameters.values(): if ((parameter.update_mode == mode) or (parameter.update_mode == ALWAYS_MODE and mode == CYCLE_MODE) or (parameter.update_mode == STEP_MODE and mode == USE_MODE) or (parameter.update_mode == ALWAYS_MODE and mode == USE_MODE) or (parameter.update_mode == ALWAYS_MODE and mode == STEP_MODE)): root_paramter = parameter.based_on_root.copy() if keep_index: root_paramter.idx = parameter.idx parameterset.add_parameter(root_paramter) return parameterset
[docs] def is_compatible(self, parameterset, update_mode=NEVER_MODE): """Two Parametersets are compatible, if the intersection only contains equivilant parameters""" return len(self.get_incompatible_parameter( parameterset, update_mode)) == 0
[docs] def get_incompatible_parameter(self, parameterset, update_mode=NEVER_MODE): """Return a set of incompatible parameter names between the current and the given parameterset""" result = set() # Find parameternames which exists in both parametersets intersection = set(self.all_parameter_names) & \ set(parameterset.all_parameter_names) for name in intersection: if (not (self[name].update_allowed(update_mode) or # In case of the USE_MODE (in the beginning of a # new step) only the actual new parameterset and its # mode is relevant parameterset[name].update_allowed( NEVER_MODE if (update_mode == USE_MODE) else update_mode)) and not self[name].is_equivalent(parameterset[name])): result.add(name) return result
[docs] def remove_jube_parameter(self): """Remove JUBE update mode parameter from the parameterset""" remove_list = [] for parameter in self: if parameter.is_jube_parameter: remove_list.append(parameter.name) for parameter_name in remove_list: self.delete_parameter(parameter_name)
[docs] def expand_templates(self): """Expand all remaining templates in the Parameterset and returns the resulting parametersets """ parameter_list = list() # Create all possible constant parameter representations for parameter in self.template_parameter_dict.values(): expanded_parameter_list = list() for static_param in parameter.expand(): expanded_parameter_list.append(static_param) parameter_list.append(expanded_parameter_list) # Generator for parameters in itertools.product(*parameter_list): parameterset = self.copy() # Addition of the constant parameters will overwrite the templates for parameter in parameters: parameterset.add_parameter(parameter) yield parameterset
def __contains__(self, parameter): if isinstance(parameter, Parameter): if parameter.name in self._parameters: return parameter.is_equivalent( self._parameters[parameter.name]) else: return False else: return parameter in self._parameters def __getitem__(self, name): if name in self._parameters: return self._parameters[name] else: return None def __iter__(self): for parameter in self.all_parameters: yield parameter
[docs] def etree_repr(self, use_current_selection=False): """Return etree object representation""" parameterset_etree = ET.Element('parameterset') if len(self._name) > 0: parameterset_etree.attrib["name"] = self._name parameterset_etree.attrib["duplicate"] = self._duplicate for parameter in self._parameters.values(): parameterset_etree.append( parameter.etree_repr(use_current_selection)) return parameterset_etree
def __len__(self): return len(self._parameters) def __repr__(self): return "Parameterset:{0}".format( dict([[parameter.name, parameter.value] for parameter in self.all_parameters]))
[docs] def parameter_substitution(self, additional_parametersets=None, final_sub=False): """Substitute all parameter inside the parameterset. Parameters from additional_parameterset will be used for substitution but will not be added to the set. final_sub marks the last substitution process.""" set_changed = True count = 0 while set_changed and (not self.has_templates) and \ (count < jube2.conf.MAX_RECURSIVE_SUB): set_changed = False count += 1 # Create dependencies depend_dict = dict() for par in self: if not par.is_template: depend_dict[par.name] = set() for other_par in self: # search for parameter usage if par.depends_on(other_par): depend_dict[par.name].add(other_par.name) # Resolve dependencies substitution_list = [self._parameters[name] for name in jube2.util.util.resolve_depend(depend_dict)] # Do substition and evaluation if possible set_changed = self.__substitute_parameters_in_list( substitution_list, additional_parametersets) # Run forced evaluation if there were no further changes if not set_changed: set_changed = self.__substitute_parameters_in_list( substitution_list, additional_parametersets, force_evaluation=True) if final_sub: parameter = [par for par in self] for par in parameter: if par.is_template: LOGGER.debug( ("Parameter ${0} = {1} is handled as " + "a template and will not be evaluated.\n").format( par.name, par.value)) else: new_par, param_changed = \ par.substitute_and_evaluate(final_sub=True) if param_changed: self.add_parameter(new_par)
def __substitute_parameters_in_list(self, parameter_list, additional_parametersets=None, force_evaluation=False): """Substitute all parameter inside the given parameter_list. Parameters from additional_parameterset will be used for substitution but will not be added to the set. force_evaluation will force script parameter evaluation""" set_changed = False for par in parameter_list: if par.can_substitute_and_evaluate(self): parametersets = [self] if additional_parametersets is not None: parametersets += additional_parametersets new_par, param_changed = \ par.substitute_and_evaluate( parametersets, force_evaluation=force_evaluation) if param_changed: self.add_parameter(new_par) set_changed = set_changed or param_changed return set_changed
[docs]class Parameter(object): """Contains data for single Parameter. This Parameter can be a constant value, a template or a specific value out of a given template""" # This regex can be used to find variables inside parameter values parameter_regex = \ re.compile(r"(?<!\$)(?:\$\$)*\$(?!\$)(\{)?(\w+?)(?(1)\}|(?=\W|$))") def __init__(self, name, value, separator=None, parameter_type="string", parameter_mode="text", unit="", export=False, update_mode=NEVER_MODE, idx=-1, eval_helper=None, duplicate="none"): self._name = name self._value = value if separator is None: self._separator = jube2.conf.DEFAULT_SEPARATOR else: self._separator = separator self._type = parameter_type self._mode = parameter_mode self._unit = unit self._based_on = None self._export = export self._idx = idx if update_mode in UPDATE_MODES: self._update_mode = update_mode else: self._update_mode = NEVER_MODE self._eval_helper = eval_helper self._duplicate=duplicate
[docs] @staticmethod def create_parameter(name, value, separator=None, parameter_type="string", selected_value=None, parameter_mode="text", unit="", export=False, no_templates=False, update_mode=NEVER_MODE, idx=-1, eval_helper=None, fixed=False, duplicate="none"): """Parameter constructor. Return a Static- or TemplateParameter based on the given data.""" if separator is None: sep = jube2.conf.DEFAULT_SEPARATOR else: sep = separator # Unicode conversion value = "" + value # Check weather a new template should be created or not if no_templates or update_mode == JUBE_MODE: values = [value] else: values = [val.strip() for val in jube2.util.util.safe_split(value, sep)] if len(values) == 1 or \ (parameter_mode in jube2.conf.ALLOWED_SCRIPTTYPES.union( jube2.conf.ALLOWED_ADVANCED_MODETYPES)): if fixed: result = FixedParameter(name, value, separator, parameter_type, parameter_mode, unit, export, update_mode, idx, eval_helper, duplicate) else: result = StaticParameter(name, value, separator, parameter_type, parameter_mode, unit, export, update_mode, idx, eval_helper, duplicate) else: result = TemplateParameter(name, values, separator, parameter_type, parameter_mode, unit, export, update_mode, idx, eval_helper, duplicate) if selected_value is not None: tmp = result parameter_mode = "text" result = FixedParameter(name, selected_value, separator, parameter_type, parameter_mode, unit, export, update_mode, idx, eval_helper, duplicate) result.based_on = tmp return result
[docs] def copy(self): """Returns Parameter copy (flat copy)""" return copy.copy(self)
[docs] def search_method(self, propertyString, recursiveProperty=None): """ Searches, potentially recursively, for a method and returns True in case of a success """ if(inspect.ismethod(self[propertyString])): return True elif(recursiveProperty and self[recursiveProperty]): return self[recursiveProperty].search_method(propertyString, recursiveProperty) else: return False
@property def eval_helper(self): """Return evaluation helper function""" return self._eval_helper @eval_helper.setter def eval_helper(self, new_eval_helper): """Sets a new evaluation helper function""" self._eval_helper = new_eval_helper @property def name(self): """Returns the Parameter name""" return self._name @property def idx(self): """Return template idx""" return self._idx @idx.setter def idx(self, new_idx): """Sets a new parameteridx""" self._idx = new_idx @property def update_mode(self): """Returns the update mode of the parameter""" return self._update_mode
[docs] def update_allowed(self, mode): """Check wether the parameter can be updated using the given update mode""" if mode is None or mode == NEVER_MODE: return False elif self._update_mode == NEVER_MODE: return False elif (self._update_mode == ALWAYS_MODE) and (mode == CYCLE_MODE): return True elif (self._update_mode == ALWAYS_MODE) and (mode == STEP_MODE): return True elif (self._update_mode == ALWAYS_MODE) and (mode == USE_MODE): return True elif (self._update_mode == STEP_MODE) and (mode == USE_MODE): return True else: return mode == self._update_mode
@property def is_jube_parameter(self): """Parameter is handled by JUBE automatically""" return self._update_mode == JUBE_MODE @property def export(self): """Return if parameter should be exported""" return self._export @property def mode(self): """Return parameter mode""" return self._mode @property def unit(self): """Return unit""" return self._unit @property def value(self): """Return parameter value""" return self._value @property def based_on(self): """The base of the current Parameter""" return self._based_on @based_on.setter def based_on(self, parameter): """The Parameter based on another one""" self._based_on = parameter @property def based_on_mode(self): """Return the root parameter mode inside the based_on graph""" if self._based_on is None: return self._mode else: return self._based_on.based_on_mode @property def based_on_root(self): """Return the root parameter inside the based_on graph""" if self._based_on is None: return self else: return self._based_on.based_on_root @property def based_on_value(self): """Return the root value inside the based_on graph""" return self.based_on_root.value @property def is_template(self): """Return whether the parameter is a template""" return isinstance(self, TemplateParameter) @property def is_fixed(self): """Return whether the parameter is fixed""" return isinstance(self, FixedParameter) @property def parameter_type(self): """Return parametertype""" return self._type @property def duplicate(self): """Return duplicate option""" return self._duplicate
[docs] def is_equivalent(self, parameter, first_check=True): """Checks whether the given and the current Parameter based on equivalent templates and (if expanded) contain the same value and were on the same place within the original template """ result = True if ((self._based_on is not None) and (parameter.based_on is not None) and first_check): result = self.value == parameter.value and \ self.idx == parameter.idx elif (self._based_on is None) and (parameter.based_on is None): result = self.value == parameter.value if (self._based_on is not None) or (parameter.based_on is not None): if (self._based_on is not None): self_based_on = self._based_on else: self_based_on = self if (parameter.based_on is not None): other_based_on = parameter.based_on else: other_based_on = parameter result = result and self_based_on.is_equivalent(other_based_on, False) return result
[docs] def etree_repr(self, use_current_selection=False): """Return etree object representation""" parameter_etree = ET.Element('parameter') parameter_etree.attrib["name"] = self._name parameter_etree.attrib["type"] = self._type parameter_etree.attrib["separator"] = self._separator parameter_etree.attrib["duplicate"] = self._duplicate based_on = self.based_on_value if use_current_selection: content_etree = ET.SubElement(parameter_etree, "value") content_etree.text = based_on else: parameter_etree.text = based_on if self._update_mode != NEVER_MODE: parameter_etree.attrib["update_mode"] = self._update_mode if use_current_selection and (based_on != self.value): parameter_etree.attrib["mode"] = self.based_on_mode selection_etree = ET.SubElement(parameter_etree, "selection") selection_etree.text = self.value if (self._idx != -1): selection_etree.attrib["idx"] = str(self._idx) else: parameter_etree.attrib["mode"] = self._mode if self._export: parameter_etree.attrib["export"] = "true" if self._unit != "": parameter_etree.attrib["unit"] = self._unit return parameter_etree
def __repr__(self): return "Parameter({0})".format(self.__dict__) def __getitem__(self, propertyString): return getattr(self, propertyString)
[docs]class StaticParameter(Parameter): """A StaticParameter can be substituted and evaluated.""" def __init__(self, name, value, separator=None, parameter_type="string", parameter_mode="text", unit="", export=False, update_mode=NEVER_MODE, idx=-1, eval_helper=None, duplicate="none"): Parameter.__init__(self, name, value, separator, parameter_type, parameter_mode, unit, export, update_mode, idx, eval_helper, duplicate) self._depending_parameter = \ set([other_par[1] for other_par in re.findall(Parameter.parameter_regex, self._value)])
[docs] def can_substitute_and_evaluate(self, parameterset): """A parameter can be substituted and evaluated if there are no depending templates or unevaluated parameter inside""" return all([(param_name not in parameterset) or ((not parameterset[param_name].is_template) and (not parameterset[param_name].mode in jube2.conf.ALLOWED_SCRIPTTYPES.union( jube2.conf.ALLOWED_ADVANCED_MODETYPES))) for param_name in self._depending_parameter])
[docs] def depends_on(self, parameter): """Checks the parameter depends on an other parameter.""" return (parameter.name in self._depending_parameter)
[docs] def substitute_and_evaluate(self, parametersets=None, final_sub=False, no_templates=False, force_evaluation=False): """Substitute all variables inside the parameter value by using the parameters inside the given parameterset. final_sub marks the last substitution. Return the new parameter and a boolean value which represent a change of value """ value = self._value if not final_sub and "$" in value: value = jube2.util.util.expand_dollar_count(value) parameter_dict = dict() if parametersets is not None: for parameterset in parametersets: for name, param in parameterset.\ constant_parameter_dict.items(): # Avoid evaluation of fixed parameter content if param.is_fixed and "$" in param.value: parameter_dict[name] = re.sub(r"\$", "$$", param.value) else: parameter_dict[name] = param.value value = jube2.util.util.substitution(value, parameter_dict) # Run parameter evaluation, if value is fully expanded and # Parameter is a script mode = self._mode pre_script_value = value # Script evaluation is allowed if: # all parameter were already replaced OR # last substitution before workpackage creation (force run) OR # last substitution after workpackage creation (final run) # AND no jube_wp_ parameter inside the value (otherwise force run will # execute these parameternames to early) # AND parameter must be a scripting parameter if ((not re.search(Parameter.parameter_regex, value)) or force_evaluation or final_sub) and \ (not any(parname.startswith("jube_wp_") for parname in self._depending_parameter)) and \ ((self._mode in jube2.conf.ALLOWED_SCRIPTTYPES.union( jube2.conf.ALLOWED_ADVANCED_MODETYPES))): try: # Run additional substitution to remove $$ before running # script evaluation to allow usage of environment variables if not final_sub: value = jube2.util.util.substitution(value, parameter_dict) # Run script evaluation LOGGER.debug("Evaluate parameter: {0}".format(self._name)) if self._mode in jube2.conf.ALLOWED_SCRIPTTYPES: value = jube2.util.util.script_evaluation( value, self._mode) if self._mode == "env": try: value = os.environ[value] except KeyError: raise RuntimeError(("\"{0}\" isn't an available " + "environment variable").format( value)) # Insert new $$ if needed if not final_sub and "$" in value: value = re.sub(r"\$", "$$", value) # Select new parameter mode mode = "text" except Exception as exception: # Ignore the forced evaluation if there was an error if force_evaluation: value = pre_script_value else: try: raise RuntimeError(("Cannot evaluate \"{0}\" for " + "parameter \"{1}\": {2}").format( value, self.name, str(exception))) except UnicodeDecodeError: raise RuntimeError(("Cannot evaluate \"{0}\" for " + "parameter \"{1}\"").format( value, self.name)) # Run evaluation helper functions if self._eval_helper is not None: value = self._eval_helper(value) changed = (value != self._value) or (mode != self._mode) if changed: param = Parameter.create_parameter(name=self._name, value=value, separator=self._separator, parameter_type=self._type, parameter_mode=mode, unit=self._unit, export=self._export, no_templates=no_templates, update_mode=self._update_mode, idx=self._idx, eval_helper=None, fixed=final_sub, duplicate=self._duplicate) param.based_on = self else: param = self return param, changed
[docs] @staticmethod def fix_export_string(value): """Add missing quotes to jube_wp_envstr if needed""" env_str = "" for var_name, var_value in re.findall( r"^export (.+?)\s*=\s*?(.+?)?\s*?$", value, re.MULTILINE): if not var_value: # Exporting empty variables env_str += "export {0}=\"\"\n".format(var_name) elif (var_value[0] == "'" and var_value[-1] == "'") or \ (var_value[0] == "\"" and var_value[-1] == "\""): env_str += "export {0}={1}\n".format(var_name, var_value) else: env_str += "export {0}=\"{1}\"\n".format( var_name, var_value.replace("\"", "\\\"")) return env_str
[docs]class TemplateParameter(Parameter): """A TemplateParameter represent a set of possible parameter values, which can be accessed by a single name. To use the template in a specific environment, it must be expanded.""" @property def value(self): """Return Template values""" return self._separator.join(self._value)
[docs] def expand(self): """Expand Template and produce set of static parameter""" if (self._idx is None) or (self._idx == -1): indices = range(len(self._value)) else: indices = [self._idx] for index in indices: value = self._value[index] static_param = StaticParameter(name=self._name, value=value, separator=self._separator, parameter_type=self._type, unit = self._unit, export=self._export, update_mode=self._update_mode, idx=index, duplicate=self._duplicate) static_param.based_on = self yield static_param
[docs]class FixedParameter(StaticParameter): """A FixedParameter is a parameter which can not be evaluated anymore. It represents a fixed value. """ def __init__(self, name, value, separator=None, parameter_type="string", parameter_mode="text", unit="", export=False, update_mode=NEVER_MODE, idx=-1, eval_helper=None, duplicate="none"): StaticParameter.__init__(self, name, value, separator, parameter_type, parameter_mode, unit, export, update_mode, idx, eval_helper, duplicate) self._depending_parameter = set()
[docs] def substitute_and_evaluate(self, parametersets=None, final_sub=False, no_templates=False, force_evaluation=False): """No substitute""" return self, False