import os import logging import shutil from glob import glob from copy import copy from itertools import chain from wlauto.core import pluginloader from wlauto.exceptions import ConfigError from wlauto.utils.types import integer, boolean, identifier, list_of_strings from wlauto.utils.misc import isiterable, get_article from wlauto.utils.serializer import read_pod, yaml class ConfigurationPoint(object): """ This defines a gneric configuration point for workload automation. This is used to handle global settings, plugin parameters, etc. """ # Mapping for kind conversion; see docs for convert_types below kind_map = { int: integer, bool: boolean, } def __init__(self, name, kind=None, mandatory=None, default=None, override=False, allowed_values=None, description=None, constraint=None, merge=False, aliases=None, convert_types=True): """ Create a new Parameter object. :param name: The name of the parameter. This will become an instance member of the plugin object to which the parameter is applied, so it must be a valid python identifier. This is the only mandatory parameter. :param kind: The type of parameter this is. This must be a callable that takes an arbitrary object and converts it to the expected type, or raised ``ValueError`` if such conversion is not possible. Most Python standard types -- ``str``, ``int``, ``bool``, etc. -- can be used here. This defaults to ``str`` if not specified. :param mandatory: If set to ``True``, then a non-``None`` value for this parameter *must* be provided on plugin object construction, otherwise ``ConfigError`` will be raised. :param default: The default value for this parameter. If no value is specified on plugin construction, this value will be used instead. (Note: if this is specified and is not ``None``, then ``mandatory`` parameter will be ignored). :param override: A ``bool`` that specifies whether a parameter of the same name further up the hierarchy should be overridden. If this is ``False`` (the default), an exception will be raised by the ``AttributeCollection`` instead. :param allowed_values: This should be the complete list of allowed values for this parameter. Note: ``None`` value will always be allowed, even if it is not in this list. If you want to disallow ``None``, set ``mandatory`` to ``True``. :param constraint: If specified, this must be a callable that takes the parameter value as an argument and return a boolean indicating whether the constraint has been satisfied. Alternatively, can be a two-tuple with said callable as the first element and a string describing the constraint as the second. :param merge: The default behaviour when setting a value on an object that already has that attribute is to overrided with the new value. If this is set to ``True`` then the two values will be merged instead. The rules by which the values are merged will be determined by the types of the existing and new values -- see ``merge_config_values`` documentation for details. :param aliases: Alternative names for the same configuration point. These are largely for backwards compatibility. :param convert_types: If ``True`` (the default), will automatically convert ``kind`` values from native Python types to WA equivalents. This allows more ituitive interprestation of parameter values, e.g. the string ``"false"`` being interpreted as ``False`` when specifed as the value for a boolean Parameter. """ self.name = identifier(name) if kind is not None and not callable(kind): raise ValueError('Kind must be callable.') if convert_types and kind in self.kind_map: kind = self.kind_map[kind] self.kind = kind self.mandatory = mandatory self.default = default self.override = override self.allowed_values = allowed_values self.description = description if self.kind is None and not self.override: self.kind = str if constraint is not None and not callable(constraint) and not isinstance(constraint, tuple): raise ValueError('Constraint must be callable or a (callable, str) tuple.') self.constraint = constraint self.merge = merge self.aliases = aliases or [] def match(self, name): if name == self.name: return True elif name in self.aliases: return True return False def set_value(self, obj, value=None): if value is None: if self.default is not None: value = self.default elif self.mandatory: msg = 'No values specified for mandatory parameter {} in {}' raise ConfigError(msg.format(self.name, obj.name)) else: try: value = self.kind(value) except (ValueError, TypeError): typename = self.get_type_name() msg = 'Bad value "{}" for {}; must be {} {}' article = get_article(typename) raise ConfigError(msg.format(value, self.name, article, typename)) if self.merge and hasattr(obj, self.name): value = merge_config_values(getattr(obj, self.name), value) setattr(obj, self.name, value) def validate(self, obj): value = getattr(obj, self.name, None) if value is not None: if self.allowed_values: self._validate_allowed_values(obj, value) if self.constraint: self._validate_constraint(obj, value) else: if self.mandatory: msg = 'No value specified for mandatory parameter {} in {}.' raise ConfigError(msg.format(self.name, obj.name)) def get_type_name(self): typename = str(self.kind) if '\'' in typename: typename = typename.split('\'')[1] elif typename.startswith(' plugin_config. """ def __init__(self, loader=pluginloader): self.loader = loader self.config = {} def update(self, name, config): if not hasattr(config, 'get'): raise ValueError('config must be a dict-like object got: {}'.format(config)) name, alias_config = self.loader.resolve_alias(name) existing_config = self.config.get(name) if existing_config is None: existing_config = alias_config new_config = config or {} self.config[name] = merge_config_values(existing_config, new_config) def merge_config_values(base, other): """ This is used to merge two objects, typically when setting the value of a ``ConfigurationPoint``. First, both objects are categorized into c: A scalar value. Basically, most objects. These values are treated as atomic, and not mergeable. s: A sequence. Anything iterable that is not a dict or a string (strings are considered scalars). m: A key-value mapping. ``dict`` and it's derivatives. n: ``None``. o: A mergeable object; this is an object that implements both ``merge_with`` and ``merge_into`` methods. The merge rules based on the two categories are then as follows: (c1, c2) --> c2 (s1, s2) --> s1 . s2 (m1, m2) --> m1 . m2 (c, s) --> [c] . s (s, c) --> s . [c] (s, m) --> s . [m] (m, s) --> [m] . s (m, c) --> ERROR (c, m) --> ERROR (o, X) --> o.merge_with(X) (X, o) --> o.merge_into(X) (X, n) --> X (n, X) --> X where: '.' means concatenation (for maps, contcationation of (k, v) streams then converted back into a map). If the types of the two objects differ, the type of ``other`` is used for the result. 'X' means "any category" '[]' used to indicate a literal sequence (not necessarily a ``list``). when this is concatenated with an actual sequence, that sequencies type is used. notes: - When a mapping is combined with a sequence, that mapping is treated as a scalar value. - When combining two mergeable objects, they're combined using ``o1.merge_with(o2)`` (_not_ using o2.merge_into(o1)). - Combining anything with ``None`` yields that value, irrespective of the order. So a ``None`` value is eqivalent to the corresponding item being omitted. - When both values are scalars, merging is equivalent to overwriting. - There is no recursion (e.g. if map values are lists, they will not be merged; ``other`` will overwrite ``base`` values). If complicated merging semantics (such as recursion) are required, they should be implemented within custom mergeable types (i.e. those that implement ``merge_with`` and ``merge_into``). While this can be used as a generic "combine any two arbitry objects" function, the semantics have been selected specifically for merging configuration point values. """ cat_base = categorize(base) cat_other = categorize(other) if cat_base == 'n': return other elif cat_other == 'n': return base if cat_base == 'o': return base.merge_with(other) elif cat_other == 'o': return other.merge_into(base) if cat_base == 'm': if cat_other == 's': return merge_sequencies([base], other) elif cat_other == 'm': return merge_maps(base, other) else: message = 'merge error ({}, {}): "{}" and "{}"' raise ValueError(message.format(cat_base, cat_other, base, other)) elif cat_base == 's': if cat_other == 's': return merge_sequencies(base, other) else: return merge_sequencies(base, [other]) else: # cat_base == 'c' if cat_other == 's': return merge_sequencies([base], other) elif cat_other == 'm': message = 'merge error ({}, {}): "{}" and "{}"' raise ValueError(message.format(cat_base, cat_other, base, other)) else: return other def merge_sequencies(s1, s2): return type(s2)(chain(s1, s2)) def merge_maps(m1, m2): return type(m2)(chain(m1.iteritems(), m2.iteritems())) def categorize(v): if hasattr(v, 'merge_with') and hasattr(v, 'merge_into'): return 'o' elif hasattr(v, 'iteritems'): return 'm' elif isiterable(v): return 's' elif v is None: return 'n' else: return 'c' settings = WAConfiguration()