# Copyright 2013-2015 ARM Limited # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # # pylint: disable=E1101 import os import logging import inspect from copy import copy from collections import OrderedDict from wlauto.core.bootstrap import settings from wlauto.exceptions import ValidationError, ConfigError from wlauto.utils.misc import isiterable, ensure_directory_exists as _d, get_article from wlauto.utils.types import identifier, integer, boolean class AttributeCollection(object): """ Accumulator for extension attribute objects (such as Parameters or Artifacts). This will replace any class member list accumulating such attributes through the magic of metaprogramming\ [*]_. .. [*] which is totally safe and not going backfire in any way... """ @property def values(self): return self._attrs.values() def __init__(self, attrcls): self._attrcls = attrcls self._attrs = OrderedDict() def add(self, p): p = self._to_attrcls(p) if p.name in self._attrs: if p.override: newp = copy(self._attrs[p.name]) for a, v in p.__dict__.iteritems(): if v is not None: setattr(newp, a, v) self._attrs[p.name] = newp else: # Duplicate attribute condition is check elsewhere. pass else: self._attrs[p.name] = p append = add def __str__(self): return 'AC({})'.format(map(str, self._attrs.values())) __repr__ = __str__ def _to_attrcls(self, p): if isinstance(p, basestring): p = self._attrcls(p) elif isinstance(p, tuple) or isinstance(p, list): p = self._attrcls(*p) elif isinstance(p, dict): p = self._attrcls(**p) elif not isinstance(p, self._attrcls): raise ValueError('Invalid parameter value: {}'.format(p)) if (p.name in self._attrs and not p.override and p.name != 'modules'): # TODO: HACK due to "diamond dependecy" in workloads... raise ValueError('Attribute {} has already been defined.'.format(p.name)) return p def __iadd__(self, other): for p in other: self.add(p) return self def __iter__(self): return iter(self.values) def __contains__(self, p): return p in self._attrs def __getitem__(self, i): return self._attrs[i] def __len__(self): return len(self._attrs) class AliasCollection(AttributeCollection): def __init__(self): super(AliasCollection, self).__init__(Alias) def _to_attrcls(self, p): if isinstance(p, tuple) or isinstance(p, list): # must be in the form (name, {param: value, ...}) p = self._attrcls(p[1], **p[1]) elif not isinstance(p, self._attrcls): raise ValueError('Invalid parameter value: {}'.format(p)) if p.name in self._attrs: raise ValueError('Attribute {} has already been defined.'.format(p.name)) return p class ListCollection(list): def __init__(self, attrcls): # pylint: disable=unused-argument super(ListCollection, self).__init__() class Param(object): """ This is a generic parameter for an extension. Extensions instantiate this to declare which parameters are supported. """ # 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, global_alias=None, convert_types=True): """ Create a new Parameter object. :param name: The name of the parameter. This will become an instance member of the extension 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 extension object construction, otherwise ``ConfigError`` will be raised. :param default: The default value for this parameter. If no value is specified on extension 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 global_alias: This is an alternative alias for this parameter, unlike the name, this alias will not be namespaced under the owning extension's name (hence the global part). This is introduced primarily for backward compatibility -- so that old extension settings names still work. This should not be used for new parameters. :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.global_alias = global_alias 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)) current_value = getattr(obj, self.name, None) if current_value is None: setattr(obj, self.name, value) elif not isiterable(current_value): setattr(obj, self.name, value) else: new_value = current_value + [value] setattr(obj, self.name, new_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('