http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/8ee1470e/aria/parser/presentation/fields.py ---------------------------------------------------------------------- diff --git a/aria/parser/presentation/fields.py b/aria/parser/presentation/fields.py new file mode 100644 index 0000000..4669fee --- /dev/null +++ b/aria/parser/presentation/fields.py @@ -0,0 +1,754 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. + +import threading +from functools import wraps +from types import MethodType +from collections import OrderedDict + +from ..exceptions import InvalidValueError, AriaException +from ..utils import (FrozenList, FrozenDict, print_exception, deepcopy_with_locators, merge, + cachedmethod, puts, as_raw, full_type_name, safe_repr) +from .null import NULL +from .utils import validate_primitive + +# +# Class decorators +# + +# pylint: disable=unused-argument + +def has_fields(cls): + """ + Class decorator for validated field support. + + 1. Adds a :code:`FIELDS` class property that is a dict of all the fields. + Will inherit and merge :code:`FIELDS` properties from base classes if + they have them. + + 2. Generates automatic :code:`@property` implementations for the fields + with the help of a set of special function decorators. + + The class also works with the Python dict protocol, so that + fields can be accessed via dict semantics. The functionality is + identical to that of using attribute access. + + The class will also gain two utility methods, :code:`_iter_field_names` + and :code:`_iter_fields`. + """ + + # Make sure we have FIELDS + if 'FIELDS' not in cls.__dict__: + setattr(cls, 'FIELDS', OrderedDict()) + + # Inherit FIELDS from base classes + for base in cls.__bases__: + if hasattr(base, 'FIELDS'): + cls.FIELDS.update(base.FIELDS) + + # We could do this: + # + # for name, field in cls.__dict__.iteritems(): + # + # But dir() is better because it has a deterministic order (alphabetical) + + for name in dir(cls): + field = getattr(cls, name) + + if isinstance(field, Field): + # Accumulate + cls.FIELDS[name] = field + + field.name = name + field.container_cls = cls + + # This function is here just to create an enclosed scope for "field" + def closure(field): + + # By convention, we have the getter wrap the original function. + # (It is, for example, where the Python help() function will look for + # docstrings when encountering a property.) + @cachedmethod + @wraps(field.func) + def getter(self): + return field.get(self, None) + + def setter(self, value): + field.set(self, None, value) + + # Convert to Python property + return property(fget=getter, fset=setter) + + setattr(cls, name, closure(field)) + + # Bind methods + setattr(cls, '_iter_field_names', MethodType(has_fields_iter_field_names, None, cls)) + setattr(cls, '_iter_fields', MethodType(has_fields_iter_fields, None, cls)) + + # Behave like a dict + setattr(cls, '__len__', MethodType(has_fields_len, None, cls)) + setattr(cls, '__getitem__', MethodType(has_fields_getitem, None, cls)) + setattr(cls, '__setitem__', MethodType(has_fields_setitem, None, cls)) + setattr(cls, '__delitem__', MethodType(has_fields_delitem, None, cls)) + setattr(cls, '__iter__', MethodType(has_fields_iter, None, cls)) + setattr(cls, '__contains__', MethodType(has_fields_contains, None, cls)) + + return cls + + +def short_form_field(name): + """ + Class decorator for specifying the short form field. + + The class must be decorated with :func:`has_fields`. + """ + + def decorator(cls): + if hasattr(cls, name) and hasattr(cls, 'FIELDS') and (name in cls.FIELDS): + setattr(cls, 'SHORT_FORM_FIELD', name) + return cls + else: + raise AttributeError('@short_form_field must be used with ' + 'a Field name in @has_fields class') + return decorator + + +def allow_unknown_fields(cls): + """ + Class decorator specifying that the class allows unknown fields. + + The class must be decorated with :func:`has_fields`. + """ + + if hasattr(cls, 'FIELDS'): + setattr(cls, 'ALLOW_UNKNOWN_FIELDS', True) + return cls + else: + raise AttributeError('@allow_unknown_fields must be used with a @has_fields class') + +# +# Method decorators +# + + +def primitive_field(cls=None, default=None, allowed=None, required=False): + """ + Method decorator for primitive fields. + + The function must be a method in a class decorated with :func:`has_fields`. + """ + + def decorator(func): + return Field(field_variant='primitive', func=func, cls=cls, default=default, + allowed=allowed, required=required) + return decorator + + +def primitive_list_field(cls=None, default=None, allowed=None, required=False): + """ + Method decorator for list of primitive fields. + + The function must be a method in a class decorated with :func:`has_fields`. + """ + + def decorator(func): + return Field(field_variant='primitive_list', func=func, cls=cls, default=default, + allowed=allowed, required=required) + return decorator + + +def primitive_dict_field(cls=None, default=None, allowed=None, required=False): + """ + Method decorator for dict of primitive fields. + + The function must be a method in a class decorated with :func:`has_fields`. + """ + def decorator(func): + return Field(field_variant='primitive_dict', func=func, cls=cls, default=default, + allowed=allowed, required=required) + return decorator + + +def primitive_dict_unknown_fields(cls=None, default=None, allowed=None, required=False): + """ + Method decorator for dict of primitive fields, for all the fields that are + not already decorated. + + The function must be a method in a class decorated with :func:`has_fields`. + """ + + def decorator(func): + return Field(field_variant='primitive_dict_unknown_fields', func=func, cls=cls, + default=default, allowed=allowed, required=required) + return decorator + + +def object_field(cls, default=None, allowed=None, required=False): + """ + Method decorator for object fields. + + The function must be a method in a class decorated with :func:`has_fields`. + """ + def decorator(func): + return Field(field_variant='object', func=func, cls=cls, default=default, allowed=allowed, + required=required) + return decorator + + +def object_list_field(cls, default=None, allowed=None, required=False): + """ + Method decorator for list of object fields. + + The function must be a method in a class decorated with :func:`has_fields`. + """ + + def decorator(func): + return Field(field_variant='object_list', func=func, cls=cls, default=default, + allowed=allowed, required=required) + return decorator + + +def object_dict_field(cls, default=None, allowed=None, required=False): + """ + Method decorator for dict of object fields. + + The function must be a method in a class decorated with :func:`has_fields`. + """ + + def decorator(func): + return Field(field_variant='object_dict', func=func, cls=cls, default=default, + allowed=allowed, required=required) + return decorator + + +def object_sequenced_list_field(cls, default=None, allowed=None, required=False): + """ + Method decorator for sequenced list of object fields. + + The function must be a method in a class decorated with :func:`has_fields`. + """ + + def decorator(func): + return Field(field_variant='sequenced_object_list', func=func, cls=cls, default=default, + allowed=allowed, required=required) + return decorator + + +def object_dict_unknown_fields(cls, default=None, allowed=None, required=False): + """ + Method decorator for dict of object fields, for all the fields that are not already decorated. + + The function must be a method in a class decorated with :func:`has_fields`. + """ + def decorator(func): + return Field(field_variant='object_dict_unknown_fields', func=func, cls=cls, + default=default, allowed=allowed, required=required) + return decorator + + +def field_getter(getter_func): + """ + Method decorator for overriding the getter function of a field. + + The signature of the getter function must be: :code:`f(field, presentation, context)`. + The default getter can be accessed as :code:`field.default_get(presentation, context)`. + + The function must already be decorated with a field decorator. + """ + + def decorator(field): + if isinstance(field, Field): + field.get = MethodType(getter_func, field, Field) + return field + else: + raise AttributeError('@field_getter must be used with a Field') + return decorator + + +def field_setter(setter_func): + """ + Method decorator for overriding the setter function of a field. + + The signature of the setter function must be: :code:`f(field, presentation, context, value)`. + The default setter can be accessed as :code:`field.default_set(presentation, context, value)`. + + The function must already be decorated with a field decorator. + """ + + def decorator(field): + if isinstance(field, Field): + field.set = MethodType(setter_func, field, Field) + return field + else: + raise AttributeError('@field_setter must be used with a Field') + return decorator + + +def field_validator(validator_fn): + """ + Method decorator for overriding the validator function of a field. + + The signature of the validator function must be: :code:f(field, presentation, context)`. + The default validator can be accessed as :code:`field.default_validate(presentation, context)`. + + The function must already be decorated with a field decorator. + """ + + def decorator(field): + if isinstance(field, Field): + field.validate = MethodType(validator_fn, field, Field) + return field + else: + raise AttributeError('@field_validator must be used with a Field') + return decorator + +# +# Utils +# + + +def has_fields_iter_field_names(self): + for name in self.__class__.FIELDS: + yield name + + +def has_fields_iter_fields(self): + return self.FIELDS.iteritems() + + +def has_fields_len(self): + return len(self.__class__.FIELDS) + + +def has_fields_getitem(self, key): + if not isinstance(key, basestring): + raise TypeError('key must be a string') + if key not in self.__class__.FIELDS: + raise KeyError('no \'%s\' property' % key) + return getattr(self, key) + + +def has_fields_setitem(self, key, value): + if not isinstance(key, basestring): + raise TypeError('key must be a string') + if key not in self.__class__.FIELDS: + raise KeyError('no \'%s\' property' % key) + return setattr(self, key, value) + + +def has_fields_delitem(self, key): + if not isinstance(key, basestring): + raise TypeError('key must be a string') + if key not in self.__class__.FIELDS: + raise KeyError('no \'%s\' property' % key) + return setattr(self, key, None) + + +def has_fields_iter(self): + return self.__class__.FIELDS.iterkeys() + + +def has_fields_contains(self, key): + if not isinstance(key, basestring): + raise TypeError('key must be a string') + return key in self.__class__.FIELDS + + +class Field(object): + """ + Field handler used by :code:`@has_fields` decorator. + """ + + def __init__(self, field_variant, func, cls=None, default=None, allowed=None, required=False): + if cls == str: + # Use "unicode" instead of "str" + cls = unicode + + self.container_cls = None + self.name = None + self.field_variant = field_variant + self.func = func + self.cls = cls + self.default = default + self.allowed = allowed + self.required = required + + @property + def full_name(self): + return 'field "%s" in "%s"' % (self.name, full_type_name(self.container_cls)) + + @property + def full_cls_name(self): + name = full_type_name(self.cls) + if name == 'unicode': + # For simplicity, display "unicode" as "str" + name = 'str' + return name + + def get(self, presentation, context): + return self.default_get(presentation, context) + + def set(self, presentation, context, value): + return self.default_set(presentation, context, value) + + def validate(self, presentation, context): + self.default_validate(presentation, context) + + def get_locator(self, raw): + if hasattr(raw, '_locator'): + locator = raw._locator + if locator is not None: + return locator.get_child(self.name) + return None + + def dump(self, presentation, context): + value = getattr(presentation, self.name) + if value is None: + return + + dumper = getattr(self, '_dump_%s' % self.field_variant) + dumper(context, value) + + def default_get(self, presentation, context): + # Handle raw + + default_raw = (presentation._get_default_raw() + if hasattr(presentation, '_get_default_raw') + else None) + + if default_raw is None: + raw = presentation._raw + else: + # Handle default raw value + raw = deepcopy_with_locators(default_raw) + merge(raw, presentation._raw) + + # Handle unknown fields + + if self.field_variant == 'primitive_dict_unknown_fields': + return self._get_primitive_dict_unknown_fields(presentation, raw, context) + elif self.field_variant == 'object_dict_unknown_fields': + return self._get_object_dict_unknown_fields(presentation, raw, context) + + is_short_form_field = (self.container_cls.SHORT_FORM_FIELD == self.name + if hasattr(self.container_cls, 'SHORT_FORM_FIELD') + else False) + is_dict = isinstance(raw, dict) + + # Find value + + value = self._find_value(is_short_form_field, is_dict, raw) + + # Handle required + + if value is None: + if self.required: + raise InvalidValueError('required %s does not have a value' % self.full_name, + locator=self.get_locator(raw)) + else: + return None + + # Handle allowed values + + if self.allowed is not None: + if value not in self.allowed: + raise InvalidValueError('%s is not %s' + % (self.full_name, ' or '.join([safe_repr(v) + for v in self.allowed])), + locator=self.get_locator(raw)) + + # Handle get according to variant + + getter = getattr(self, '_get_%s' % self.field_variant, None) + + if getter is None: + locator = self.get_locator(raw) + location = (' @%s' % locator) if locator is not None else '' + raise AttributeError('%s has unsupported field variant: "%s"%s' + % (self.full_name, self.field_variant, location)) + + return getter(presentation, raw, value, context) + + def _find_value(self, is_short_form_field, is_dict, raw): + value = None + if is_short_form_field and not is_dict: + # Handle short form + value = raw + elif is_dict: + if self.name in raw: + value = raw[self.name] + if value is None: + # An explicit null + value = NULL + else: + value = self.default + return value + + def default_set(self, presentation, context, value): + raw = presentation._raw + old = self.get(presentation, context) + raw[self.name] = value + try: + self.validate(presentation, context) + except Exception as e: + raw[self.name] = old + raise e + return old + + def default_validate(self, presentation, context): + value = None + + try: + value = self.get(presentation, context) + except AriaException as e: + if e.issue: + context.validation.report(issue=e.issue) + except Exception as e: + context.validation.report(exception=e) + print_exception(e) + + self.validate_value(value, context) + + def validate_value(self, value, context): + if isinstance(value, list): + if self.field_variant == 'object_list': + for element in value: + if hasattr(element, '_validate'): + element._validate(context) + elif self.field_variant == 'sequenced_object_list': + for _, element in value: + if hasattr(element, '_validate'): + element._validate(context) + elif isinstance(value, dict): + if self.field_variant in ('object_dict', 'object_dict_unknown_fields'): + for inner_value in value.itervalues(): + if hasattr(inner_value, '_validate'): + inner_value._validate(context) + + if hasattr(value, '_validate'): + value._validate(context) + + @staticmethod + def _get_context(): + thread_locals = threading.local() + return getattr(thread_locals, 'aria_consumption_context', None) + + def _coerce_primitive(self, value, context): + if context is None: + context = Field._get_context() + allow_primitive_coercion = (context.validation.allow_primitive_coersion + if context is not None + else True) + return validate_primitive(value, self.cls, allow_primitive_coercion) + + # primitive + + def _get_primitive(self, presentation, raw, value, context): + if (self.cls is not None and not isinstance(value, self.cls) + and value is not None and value is not NULL): + try: + return self._coerce_primitive(value, context) + except ValueError as e: + raise InvalidValueError('%s is not a valid "%s": %s' % + (self.full_name, self.full_cls_name, safe_repr(value)), + locator=self.get_locator(raw), cause=e) + return value + + def _dump_primitive(self, context, value): + if hasattr(value, 'as_raw'): + value = as_raw(value) + puts('%s: %s' % (self.name, context.style.literal(value))) + + # primitive list + + def _get_primitive_list(self, presentation, raw, value, context): + if not isinstance(value, list): + raise InvalidValueError('%s is not a list: %s' % (self.full_name, safe_repr(value)), + locator=self.get_locator(raw)) + primitive_list = value + if self.cls is not None: + if context is None: + context = Field._get_context() + primitive_list = [] + for i, _ in enumerate(value): + primitive = value[i] + try: + primitive = self._coerce_primitive(primitive, context) + except ValueError as e: + raise InvalidValueError('%s is not a list of "%s": element %d is %s' + % (self.full_name, + self.full_cls_name, + i, + safe_repr(primitive)), + locator=self.get_locator(raw), cause=e) + if primitive in primitive_list: + raise InvalidValueError('%s has a duplicate "%s": %s' + % (self.full_name, + self.full_cls_name, + safe_repr(primitive)), + locator=self.get_locator(raw)) + primitive_list.append(primitive) + return FrozenList(primitive_list) + + def _dump_primitive_list(self, context, value): + puts('%s:' % self.name) + with context.style.indent: + for primitive in value: + if hasattr(primitive, 'as_raw'): + primitive = as_raw(primitive) + puts(context.style.literal(primitive)) + + # primitive dict + + def _get_primitive_dict(self, presentation, raw, value, context): + if not isinstance(value, dict): + raise InvalidValueError('%s is not a dict: %s' % (self.full_name, safe_repr(value)), + locator=self.get_locator(raw)) + primitive_dict = value + if self.cls is not None: + if context is None: + context = Field._get_context() + primitive_dict = OrderedDict() + for k, v in value.iteritems(): + try: + primitive_dict[k] = self._coerce_primitive(v, context) + except ValueError as e: + raise InvalidValueError('%s is not a dict of "%s" values: entry "%d" is %s' + % (self.full_name, self.full_cls_name, k, safe_repr(v)), + locator=self.get_locator(raw), + cause=e) + return FrozenDict(primitive_dict) + + def _dump_primitive_dict(self, context, value): + puts('%s:' % self.name) + with context.style.indent: + for v in value.itervalues(): + if hasattr(v, 'as_raw'): + v = as_raw(v) + puts(context.style.literal(v)) + + # object + + def _get_object(self, presentation, raw, value, context): + try: + return self.cls(name=self.name, raw=value, container=presentation) + except TypeError as e: + raise InvalidValueError('%s cannot not be initialized to an instance of "%s": %s' + % (self.full_name, self.full_cls_name, safe_repr(value)), + cause=e, + locator=self.get_locator(raw)) + + def _dump_object(self, context, value): + puts('%s:' % self.name) + with context.style.indent: + if hasattr(value, '_dump'): + value._dump(context) + + # object list + + def _get_object_list(self, presentation, raw, value, context): + if not isinstance(value, list): + raise InvalidValueError('%s is not a list: %s' + % (self.full_name, safe_repr(value)), + locator=self.get_locator(raw)) + return FrozenList((self.cls(name=self.name, raw=v, container=presentation) for v in value)) + + def _dump_object_list(self, context, value): + puts('%s:' % self.name) + with context.style.indent: + for v in value: + if hasattr(v, '_dump'): + v._dump(context) + + # object dict + + def _get_object_dict(self, presentation, raw, value, context): + if not isinstance(value, dict): + raise InvalidValueError('%s is not a dict: %s' % (self.full_name, safe_repr(value)), + locator=self.get_locator(raw)) + return FrozenDict(((k, self.cls(name=k, raw=v, container=presentation)) + for k, v in value.iteritems())) + + def _dump_object_dict(self, context, value): + puts('%s:' % self.name) + with context.style.indent: + for v in value.itervalues(): + if hasattr(v, '_dump'): + v._dump(context) + + # sequenced object list + + def _get_sequenced_object_list(self, presentation, raw, value, context): + if not isinstance(value, list): + raise InvalidValueError('%s is not a sequenced list (a list of dicts, ' + 'each with exactly one key): %s' + % (self.full_name, safe_repr(value)), + locator=self.get_locator(raw)) + sequence = [] + for v in value: + if not isinstance(v, dict): + raise InvalidValueError('%s list elements are not all dicts with ' + 'exactly one key: %s' % (self.full_name, safe_repr(value)), + locator=self.get_locator(raw)) + if len(v) != 1: + raise InvalidValueError('%s list elements do not all have exactly one key: %s' + % (self.full_name, safe_repr(value)), + locator=self.get_locator(raw)) + key, value = v.items()[0] + sequence.append((key, self.cls(name=key, raw=value, container=presentation))) + return FrozenList(sequence) + + def _dump_sequenced_object_list(self, context, value): + puts('%s:' % self.name) + for _, v in value: + if hasattr(v, '_dump'): + v._dump(context) + + # primitive dict for unknown fields + + def _get_primitive_dict_unknown_fields(self, presentation, raw, context): + if isinstance(raw, dict): + primitive_dict = raw + if self.cls is not None: + if context is None: + context = Field._get_context() + primitive_dict = OrderedDict() + for k, v in raw.iteritems(): + if k not in presentation.FIELDS: + try: + primitive_dict[k] = self._coerce_primitive(v, context) + except ValueError as e: + raise InvalidValueError('%s is not a dict of "%s" values:' + ' entry "%d" is %s' + % (self.full_name, self.full_cls_name, + k, safe_repr(v)), + locator=self.get_locator(raw), + cause=e) + return FrozenDict(primitive_dict) + return None + + def _dump_primitive_dict_unknown_fields(self, context, value): + self._dump_primitive_dict(context, value) + + # object dict for unknown fields + + def _get_object_dict_unknown_fields(self, presentation, raw, context): + if isinstance(raw, dict): + return FrozenDict(((k, self.cls(name=k, raw=v, container=presentation)) + for k, v in raw.iteritems() if k not in presentation.FIELDS)) + return None + + def _dump_object_dict_unknown_fields(self, context, value): + self._dump_object_dict(context, value)
http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/8ee1470e/aria/parser/presentation/null.py ---------------------------------------------------------------------- diff --git a/aria/parser/presentation/null.py b/aria/parser/presentation/null.py new file mode 100644 index 0000000..a69134e --- /dev/null +++ b/aria/parser/presentation/null.py @@ -0,0 +1,67 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. + +from ..utils import deepcopy_with_locators + + +class Null(object): + """ + Represents an explicit null value provided by the user, which is different from + not supplying a value at all. + + It is a singleton. + """ + + @property + def as_raw(self): + return None + +NULL = Null() + + +def none_to_null(value): + """ + Convert :code:`None` to :code:`NULL`, recursively. + """ + + if value is None: + return NULL + if isinstance(value, list): + value = deepcopy_with_locators(value) + for i, _ in enumerate(value): + value[i] = none_to_null(value[i]) + elif isinstance(value, dict): + value = deepcopy_with_locators(value) + for k, v in value.iteritems(): + value[k] = none_to_null(v) + return value + + +def null_to_none(value): + """ + Convert :code:`NULL` to :code:`None`, recursively. + """ + + if value is NULL: + return None + if isinstance(value, list): + value = deepcopy_with_locators(value) + for i, _ in enumerate(value): + value[i] = none_to_null(value[i]) + elif isinstance(value, dict): + value = deepcopy_with_locators(value) + for k, v in value.iteritems(): + value[k] = none_to_null(v) + return value http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/8ee1470e/aria/parser/presentation/presentation.py ---------------------------------------------------------------------- diff --git a/aria/parser/presentation/presentation.py b/aria/parser/presentation/presentation.py new file mode 100644 index 0000000..15cb5ed --- /dev/null +++ b/aria/parser/presentation/presentation.py @@ -0,0 +1,235 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. + +from ..validation import Issue +from ..utils import HasCachedMethods, full_type_name, deepcopy_with_locators, puts, safe_repr +from .null import none_to_null +from .utils import (get_locator, validate_no_short_form, validate_no_unknown_fields, + validate_known_fields, validate_primitive) + + +class Value(object): + """ + Encapsulates a typed value assignment. + """ + + def __init__(self, type_name, value, description): + self.type = deepcopy_with_locators(type_name) + self.value = deepcopy_with_locators(value) + self.description = deepcopy_with_locators(description) + + +class PresentationBase(HasCachedMethods): + """ + Base class for ARIA presentation classes. + """ + + def __init__(self, name=None, raw=None, container=None): + self._name = name + self._raw = raw + self._container = container + super(PresentationBase, self).__init__() + + @property + def as_raw(self): + return self._raw + + def _validate(self, context): + """ + Validates the presentation while reporting errors in the validation context but + *not* raising exceptions. + + The base class does not thing, but subclasses may override this for specialized + validation. + """ + + @property + def _fullname(self): + """ + Always returns a usable full name of the presentation, whether it itself is named, + or recursing to its container, and finally defaulting to the class name. + """ + + if self._name is not None: + return self._name + elif self._container is not None: + return self._container._fullname + return full_type_name(self) + + @property + def _locator(self): + """ + Attempts to return the most relevant locator, whether we have one, or recursing + to our container. + + :rtype: :class:`aria.reading.Locator` + """ + + return get_locator(self._raw, self._container) + + def _get(self, *names): + """ + Gets attributes recursively. + """ + + obj = self + if (obj is not None) and names: + for name in names: + obj = getattr(obj, name, None) + if obj is None: + break + return obj + + def _get_from_dict(self, *names): + """ + Gets attributes recursively, except for the last name which is used + to get a value from the last dict. + """ + + if names: + obj = self._get(*names[:-1]) + if isinstance(obj, dict): + return obj.get(names[-1]) # pylint: disable=no-member + return None + + def _get_child_locator(self, *names): + """ + Attempts to return the locator of one our children. Will default to our locator + if not found. + + :rtype: :class:`aria.reading.Locator` + """ + + if hasattr(self._raw, '_locator'): + locator = self._raw._locator + if locator is not None: + return locator.get_child(*names) + return self._locator + + def _dump(self, context): + """ + Emits a colorized representation. + + The base class will emit a sensible default representation of the fields, + (by calling :code:`_dump_content`), but subclasses may override this for specialized + dumping. + """ + + if self._name: + puts(context.style.node(self._name)) + with context.style.indent: + self._dump_content(context) + else: + self._dump_content(context) + + def _dump_content(self, context, field_names=None): + """ + Emits a colorized representation of the contents. + + The base class will call :code:`_dump_field` on all the fields, but subclasses may + override this for specialized dumping. + """ + + if field_names: + for field_name in field_names: + self._dump_field(context, field_name) + elif hasattr(self, '_iter_field_names'): + for field_name in self._iter_field_names(): # pylint: disable=no-member + self._dump_field(context, field_name) + else: + puts(context.style.literal(self._raw)) + + def _dump_field(self, context, field_name): + """ + Emits a colorized representation of the field. + + According to the field type, this may trigger nested recursion. The nested + types will delegate to their :code:`_dump` methods. + """ + + field = self.FIELDS[field_name] # pylint: disable=no-member + field.dump(self, context) + + def _clone(self, container=None): + """ + Creates a clone of this presentation, optionally allowing for a new container. + """ + + raw = deepcopy_with_locators(self._raw) + if container is None: + container = self._container + return self.__class__(name=self._name, raw=raw, container=container) + + +class Presentation(PresentationBase): + """ + Base class for ARIA presentations. A presentation is a Pythonic wrapper around + agnostic raw data, adding the ability to read and modify the data with proper + validation. + + ARIA presentation classes will often be decorated with @has_fields, as that + mechanism automates a lot of field-specific validation. However, that is not a + requirement. + + Make sure that your utility property and method names begin with a "_", because + those names without a "_" prefix are normally reserved for fields. + """ + + def _validate(self, context): + validate_no_short_form(context, self) + validate_no_unknown_fields(context, self) + validate_known_fields(context, self) + + +class AsIsPresentation(PresentationBase): + """ + Base class for trivial ARIA presentations that provide the raw value as is. + """ + + def __init__(self, name=None, raw=None, container=None, cls=None): + super(AsIsPresentation, self).__init__(name, raw, container) + self.cls = cls + + @property + def value(self): + return none_to_null(self._raw) + + @value.setter + def value(self, value): + self._raw = value + + @property + def _full_cls_name(self): + name = full_type_name(self.cls) if self.cls is not None else None + if name == 'unicode': + # For simplicity, display "unicode" as "str" + name = 'str' + return name + + def _validate(self, context): + try: + validate_primitive(self._raw, self.cls, context.validation.allow_primitive_coersion) + except ValueError as e: + context.validation.report('"%s" is not a valid "%s": %s' + % (self._fullname, self._full_cls_name, safe_repr(self._raw)), + locator=self._locator, + level=Issue.FIELD, + exception=e) + + def _dump(self, context): + if hasattr(self._raw, '_dump'): + self._raw._dump(context) + else: + super(AsIsPresentation, self)._dump(context) http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/8ee1470e/aria/parser/presentation/presenter.py ---------------------------------------------------------------------- diff --git a/aria/parser/presentation/presenter.py b/aria/parser/presentation/presenter.py new file mode 100644 index 0000000..4cade3a --- /dev/null +++ b/aria/parser/presentation/presenter.py @@ -0,0 +1,69 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. + +from ..validation import Issue +from ..utils import merge, safe_repr +from .presentation import Presentation + + +class Presenter(Presentation): + """ + Base class for ARIA presenters. + + Presenters provide a robust API over agnostic raw data. + """ + + DSL_VERSIONS = None + ALLOWED_IMPORTED_DSL_VERSIONS = None + + @classmethod + def can_present(cls, raw): + dsl = raw.get('tosca_definitions_version') + assert cls.DSL_VERSIONS + return dsl in cls.DSL_VERSIONS + + def _validate_import(self, context, presentation): + tosca_definitions_version = presentation.service_template.tosca_definitions_version + assert self.ALLOWED_IMPORTED_DSL_VERSIONS + if tosca_definitions_version is not None \ + and tosca_definitions_version not in self.__class__.ALLOWED_IMPORTED_DSL_VERSIONS: + context.validation.report( + 'import "tosca_definitions_version" is not one of %s: %s' + % (' or '.join([safe_repr(v) + for v in self.__class__.ALLOWED_IMPORTED_DSL_VERSIONS]), + presentation.service_template.tosca_definitions_version), + locator=presentation._get_child_locator('inputs'), + level=Issue.BETWEEN_TYPES) + return False + return True + + def _merge_import(self, presentation): + merge(self._raw, presentation._raw) + if hasattr(self._raw, '_locator') and hasattr(presentation._raw, '_locator'): + self._raw._locator.merge(presentation._raw._locator) + + def _link_locators(self): + if hasattr(self._raw, '_locator'): + locator = self._raw._locator + delattr(self._raw, '_locator') + locator.link(self._raw) + + @staticmethod + def _get_import_locations(context): + raise NotImplementedError + + @staticmethod + def _get_deployment_template(context): + raise NotImplementedError http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/8ee1470e/aria/parser/presentation/source.py ---------------------------------------------------------------------- diff --git a/aria/parser/presentation/source.py b/aria/parser/presentation/source.py new file mode 100644 index 0000000..6f195d0 --- /dev/null +++ b/aria/parser/presentation/source.py @@ -0,0 +1,47 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. + +from .exceptions import PresenterNotFoundError + +PRESENTER_CLASSES = [] + + +class PresenterSource(object): + """ + Base class for ARIA presenter sources. + + Presenter sources provide appropriate :class:`Presenter` classes for agnostic raw data. + """ + + def get_presenter(self, raw): # pylint: disable=unused-argument,no-self-use + raise PresenterNotFoundError('presenter not found') + + +class DefaultPresenterSource(PresenterSource): + """ + The default ARIA presenter source supports TOSCA Simple Profile. + """ + + def __init__(self, classes=None): + if classes is None: + classes = PRESENTER_CLASSES + self.classes = classes + + def get_presenter(self, raw): + for cls in self.classes: + if cls.can_present(raw): + return cls + + return super(DefaultPresenterSource, self).get_presenter(raw) http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/8ee1470e/aria/parser/presentation/utils.py ---------------------------------------------------------------------- diff --git a/aria/parser/presentation/utils.py b/aria/parser/presentation/utils.py new file mode 100644 index 0000000..2e4d873 --- /dev/null +++ b/aria/parser/presentation/utils.py @@ -0,0 +1,186 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. + +from types import FunctionType + +from ..validation import Issue +from ..utils import full_type_name, safe_repr +from .null import NULL + + +def get_locator(*values): + """ + Gets the first available locator. + + :rtype: :class:`aria.reading.Locator` + """ + + for v in values: + if hasattr(v, '_locator'): + locator = v._locator + if locator is not None: + return locator + return None + + +def parse_types_dict_names(types_dict_names): + """ + If the first element in the array is a function, extracts it out. + """ + + convert = None + if isinstance(types_dict_names[0], FunctionType): + convert = types_dict_names[0] + types_dict_names = types_dict_names[1:] + return types_dict_names, convert + + +def validate_primitive(value, cls, coerce=False): + """ + Checks if the value is of the primitive type, optionally attempting to coerce it + if it is not. + + Raises a :code:`ValueError` if it isn't or if coercion failed. + """ + + if (cls is not None) and (value is not None) and (value is not NULL): + if (cls is unicode) or (cls is str): # These two types are interchangeable + valid = isinstance(value, basestring) + elif cls is int: + # In Python, a bool is an int + valid = isinstance(value, int) and not isinstance(value, bool) + else: + valid = isinstance(value, cls) + if not valid: + if coerce: + value = cls(value) + else: + raise ValueError('not a "%s": %s' % (full_type_name(cls), safe_repr(value))) + return value + + +def validate_no_short_form(context, presentation): + """ + Makes sure that we can use short form definitions only if we allowed it. + """ + + if not hasattr(presentation, 'SHORT_FORM_FIELD') and not isinstance(presentation._raw, dict): + context.validation.report('short form not allowed for field "%s"' % presentation._fullname, + locator=presentation._locator, + level=Issue.BETWEEN_FIELDS) + + +def validate_no_unknown_fields(context, presentation): + """ + Make sure that we can use unknown fields only if we allowed it. + """ + + if not getattr(presentation, 'ALLOW_UNKNOWN_FIELDS', False) \ + and not context.validation.allow_unknown_fields \ + and isinstance(presentation._raw, dict) \ + and hasattr(presentation, 'FIELDS'): + for k in presentation._raw: + if k not in presentation.FIELDS: + context.validation.report('field "%s" is not supported in "%s"' + % (k, presentation._fullname), + locator=presentation._get_child_locator(k), + level=Issue.BETWEEN_FIELDS) + + +def validate_known_fields(context, presentation): + """ + Validates all known fields. + """ + + if hasattr(presentation, '_iter_fields'): + for _, field in presentation._iter_fields(): + field.validate(presentation, context) + + +def get_parent_presentation(context, presentation, *types_dict_names): + """ + Returns the parent presentation according to the :code:`derived_from` field, or None if invalid. + + Checks that we do not derive from ourselves and that we do not cause a circular hierarchy. + + The arguments from the third onwards are used to locate a nested field under + :code:`service_template` under the root presenter. The first of these can optionally + be a function, in which case it will be called to convert type names. This can be used + to support shorthand type names, aliases, etc. + """ + + type_name = presentation.derived_from + + if type_name is None: + return None + + types_dict_names, convert = parse_types_dict_names(types_dict_names) + types_dict = context.presentation.get('service_template', *types_dict_names) or {} + + if convert: + type_name = convert(context, type_name, types_dict) + + # Make sure not derived from self + if type_name == presentation._name: + return None + # Make sure derived from type exists + elif type_name not in types_dict: + return None + else: + # Make sure derivation hierarchy is not circular + hierarchy = [presentation._name] + presentation_copy = presentation + while presentation_copy.derived_from is not None: + derived_from = presentation_copy.derived_from + if convert: + derived_from = convert(context, derived_from, types_dict) + + if derived_from == presentation_copy._name or derived_from not in types_dict: + return None + presentation_copy = types_dict[derived_from] + if presentation_copy._name in hierarchy: + return None + hierarchy.append(presentation_copy._name) + + return types_dict[type_name] + + +def report_issue_for_unknown_type(context, presentation, type_name, field_name, value=None): + if value is None: + value = getattr(presentation, field_name) + context.validation.report('"%s" refers to an unknown %s in "%s": %s' + % (field_name, type_name, presentation._fullname, safe_repr(value)), + locator=presentation._get_child_locator(field_name), + level=Issue.BETWEEN_TYPES) + + +def report_issue_for_parent_is_self(context, presentation, field_name): + context.validation.report('parent type of "%s" is self' % presentation._fullname, + locator=presentation._get_child_locator(field_name), + level=Issue.BETWEEN_TYPES) + + +def report_issue_for_unknown_parent_type(context, presentation, field_name): + context.validation.report('unknown parent type "%s" in "%s"' + % (getattr(presentation, field_name), presentation._fullname), + locator=presentation._get_child_locator(field_name), + level=Issue.BETWEEN_TYPES) + + +def report_issue_for_circular_type_hierarchy(context, presentation, field_name): + context.validation.report('"%s" of "%s" creates a circular type hierarchy' + % (getattr(presentation, field_name), presentation._fullname), + locator=presentation._get_child_locator(field_name), + level=Issue.BETWEEN_TYPES) http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/8ee1470e/aria/parser/reading/__init__.py ---------------------------------------------------------------------- diff --git a/aria/parser/reading/__init__.py b/aria/parser/reading/__init__.py new file mode 100644 index 0000000..32aa5b5 --- /dev/null +++ b/aria/parser/reading/__init__.py @@ -0,0 +1,39 @@ +# 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. + +from .raw import RawReader +from .reader import Reader +from .yaml import YamlReader +from .locator import Locator +from .json import JsonReader +from .jinja import JinjaReader +from .context import ReadingContext +from .source import ReaderSource, DefaultReaderSource +from .exceptions import (ReaderException, + ReaderNotFoundError, + ReaderSyntaxError, + AlreadyReadException) + +__all__ = ( + 'ReaderException', + 'ReaderNotFoundError', + 'ReaderSyntaxError', + 'AlreadyReadException', + 'Reader', + 'ReaderSource', + 'DefaultReaderSource', + 'ReadingContext', + 'RawReader', + 'Locator', + 'YamlReader', + 'JsonReader', + 'JinjaReader') http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/8ee1470e/aria/parser/reading/context.py ---------------------------------------------------------------------- diff --git a/aria/parser/reading/context.py b/aria/parser/reading/context.py new file mode 100644 index 0000000..81135dc --- /dev/null +++ b/aria/parser/reading/context.py @@ -0,0 +1,29 @@ +# 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. + +from .source import DefaultReaderSource +from ..utils import LockedList + + +class ReadingContext(object): + """ + Properties: + + * :code:`reader_source`: For finding reader instances + * :code:`reader`: Overrides :code:`reader_source` with a specific class + """ + + def __init__(self): + self.reader_source = DefaultReaderSource() + self.reader = None + + self._locations = LockedList() # for keeping track of locations already read http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/8ee1470e/aria/parser/reading/exceptions.py ---------------------------------------------------------------------- diff --git a/aria/parser/reading/exceptions.py b/aria/parser/reading/exceptions.py new file mode 100644 index 0000000..bc2d2d5 --- /dev/null +++ b/aria/parser/reading/exceptions.py @@ -0,0 +1,44 @@ +# 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. + +from .. import AriaException +from ..validation import Issue + + +class ReaderException(AriaException): + """ + ARIA reader exception. + """ + + +class ReaderNotFoundError(ReaderException): + """ + ARIA reader error: reader not found for source. + """ + + +class ReaderSyntaxError(ReaderException): + """ + ARIA read format error. + """ + + def __init__(self, message, cause=None, cause_tb=None, location=None, line=None, + column=None, locator=None, snippet=None, level=Issue.SYNTAX): + super(ReaderSyntaxError, self).__init__(message, cause, cause_tb) + self.issue = Issue(message, location=location, line=line, column=column, + locator=locator, snippet=snippet, level=level) + + +class AlreadyReadException(ReaderException): + """ + ARIA reader exception: already read. + """ http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/8ee1470e/aria/parser/reading/jinja.py ---------------------------------------------------------------------- diff --git a/aria/parser/reading/jinja.py b/aria/parser/reading/jinja.py new file mode 100644 index 0000000..e15e54e --- /dev/null +++ b/aria/parser/reading/jinja.py @@ -0,0 +1,55 @@ +# 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. + +import os + +from jinja2 import Template + +from .. import VERSION +from ..loading import LiteralLocation, LiteralLoader +from .reader import Reader +from .exceptions import ReaderSyntaxError + + +# TODO: we could put a lot of other useful stuff here. +CONTEXT = { + 'ARIA_VERSION': VERSION, + 'ENV': os.environ} + + +class JinjaReader(Reader): + """ + ARIA Jinja reader. + + Forwards the rendered result to a new reader in the reader source. + """ + + def read(self): + data = self.load() + try: + data = str(data) + template = Template(data) + literal = template.render(CONTEXT) + # TODO: might be useful to write the literal result to a file for debugging + location = self.location + if isinstance(location, basestring) and location.endswith('.jinja'): + # Use reader based on the location with the ".jinja" prefix stripped off + location = location[:-6] + next_reader = self.context.reading.reader_source.get_reader( + self.context, LiteralLocation(literal, name=location), LiteralLoader(literal)) + else: + # Use reader for literal loader + next_reader = self.context.reading.reader_source.get_reader( + self.context, LiteralLocation(literal), LiteralLoader(literal)) + return next_reader.read() + except Exception as e: + raise ReaderSyntaxError('Jinja: %s' % e, cause=e) http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/8ee1470e/aria/parser/reading/json.py ---------------------------------------------------------------------- diff --git a/aria/parser/reading/json.py b/aria/parser/reading/json.py new file mode 100644 index 0000000..4747651 --- /dev/null +++ b/aria/parser/reading/json.py @@ -0,0 +1,33 @@ +# 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. + +from __future__ import absolute_import # so we can import standard 'json' + +import json +from collections import OrderedDict + +from .reader import Reader +from .exceptions import ReaderSyntaxError + + +class JsonReader(Reader): + """ + ARIA JSON reader. + """ + + def read(self): + data = self.load() + try: + data = unicode(data) + return json.loads(data, object_pairs_hook=OrderedDict) + except Exception as e: + raise ReaderSyntaxError('JSON: %s' % e, cause=e) http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/8ee1470e/aria/parser/reading/locator.py ---------------------------------------------------------------------- diff --git a/aria/parser/reading/locator.py b/aria/parser/reading/locator.py new file mode 100644 index 0000000..90b9e73 --- /dev/null +++ b/aria/parser/reading/locator.py @@ -0,0 +1,119 @@ +# 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. + +from ..utils import puts, Colored, indent + +# We are inheriting the primitive types in order to add the ability to set +# an attribute (_locator) on them. + + +class LocatableString(unicode): + pass + + +class LocatableInt(int): + pass + + +class LocatableFloat(float): + pass + + +def wrap(value): + if isinstance(value, basestring): + return True, LocatableString(value) + elif isinstance(value, int) and \ + not isinstance(value, bool): # Note: bool counts as int in Python! + return True, LocatableInt(value) + elif isinstance(value, float): + return True, LocatableFloat(value) + return False, value + + +class Locator(object): + """ + Stores location information (line and column numbers) for agnostic raw data. + """ + def __init__(self, location, line, column, children=None): + self.location = location + self.line = line + self.column = column + self.children = children + + def get_child(self, *names): + if (not names) or (not isinstance(self.children, dict)): + return self + name = names[0] + if name not in self.children: + return self + child = self.children[name] + return child.get_child(names[1:]) + + def link(self, raw, path=None): + if hasattr(raw, '_locator'): + # This can happen when we use anchors + return + + try: + setattr(raw, '_locator', self) + except AttributeError: + return + + if isinstance(raw, list): + for i, raw_element in enumerate(raw): + wrapped, raw_element = wrap(raw_element) + if wrapped: + raw[i] = raw_element + child_path = '%s.%d' % (path, i) if path else str(i) + try: + self.children[i].link(raw_element, child_path) + except KeyError: + raise ValueError('location map does not match agnostic raw data: %s' % + child_path) + elif isinstance(raw, dict): + for k, raw_element in raw.iteritems(): + wrapped, raw_element = wrap(raw_element) + if wrapped: + raw[k] = raw_element + child_path = '%s.%s' % (path, k) if path else k + try: + self.children[k].link(raw_element, child_path) + except KeyError: + raise ValueError('location map does not match agnostic raw data: %s' % + child_path) + + def merge(self, locator): + if isinstance(self.children, dict) and isinstance(locator.children, dict): + for k, loc in locator.children.iteritems(): + if k in self.children: + self.children[k].merge(loc) + else: + self.children[k] = loc + + def dump(self, key=None): + if key: + puts('%s "%s":%d:%d' % + (Colored.red(key), Colored.blue(self.location), self.line, self.column)) + else: + puts('"%s":%d:%d' % (Colored.blue(self.location), self.line, self.column)) + if isinstance(self.children, list): + with indent(2): + for loc in self.children: + loc.dump() + elif isinstance(self.children, dict): + with indent(2): + for k, loc in self.children.iteritems(): + loc.dump(k) + + def __str__(self): + # Should be in same format as Issue.locator_as_str + return '"%s":%d:%d' % (self.location, self.line, self.column) http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/8ee1470e/aria/parser/reading/raw.py ---------------------------------------------------------------------- diff --git a/aria/parser/reading/raw.py b/aria/parser/reading/raw.py new file mode 100644 index 0000000..ed980ac --- /dev/null +++ b/aria/parser/reading/raw.py @@ -0,0 +1,24 @@ +# 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. + +from .reader import Reader + + +class RawReader(Reader): + """ + ARIA raw reader. + + Expects to receive agnostic raw data from the loader, and so does nothing to it. + """ + + def read(self): + return self.load() http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/8ee1470e/aria/parser/reading/reader.py ---------------------------------------------------------------------- diff --git a/aria/parser/reading/reader.py b/aria/parser/reading/reader.py new file mode 100644 index 0000000..3a50739 --- /dev/null +++ b/aria/parser/reading/reader.py @@ -0,0 +1,44 @@ +# 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. + +from ..utils import OpenClose +from .exceptions import ReaderException, AlreadyReadException + + +class Reader(object): + """ + Base class for ARIA readers. + + Readers provide agnostic raw data by consuming :class:`aria.loading.Loader` instances. + """ + + def __init__(self, context, location, loader): + self.context = context + self.location = location + self.loader = loader + + def load(self): + with OpenClose(self.loader) as loader: + if self.context is not None: + with self.context._locations: + for location in self.context._locations: + if location.is_equivalent(loader.location): + raise AlreadyReadException('already read: %s' % loader.location) + self.context._locations.append(loader.location) + + data = loader.load() + if data is None: + raise ReaderException('loader did not provide data: %s' % loader) + return data + + def read(self): + raise NotImplementedError http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/8ee1470e/aria/parser/reading/source.py ---------------------------------------------------------------------- diff --git a/aria/parser/reading/source.py b/aria/parser/reading/source.py new file mode 100644 index 0000000..6fff2f6 --- /dev/null +++ b/aria/parser/reading/source.py @@ -0,0 +1,59 @@ +# 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. + +from ..loading import LiteralLocation, UriLocation +from .yaml import YamlReader +from .json import JsonReader +from .jinja import JinjaReader +from .exceptions import ReaderNotFoundError + + +EXTENSIONS = { + '.yaml': YamlReader, + '.json': JsonReader, + '.jinja': JinjaReader} + + +class ReaderSource(object): + """ + Base class for ARIA reader sources. + + Reader sources provide appropriate :class:`Reader` instances for locations. + """ + + @staticmethod + def get_reader(context, location, loader): # pylint: disable=unused-argument + raise ReaderNotFoundError('location: %s' % location) + + +class DefaultReaderSource(ReaderSource): + """ + The default ARIA reader source will generate a :class:`YamlReader` for + locations that end in ".yaml", a :class:`JsonReader` for locations that + end in ".json", and a :class:`JinjaReader` for locations that end in + ".jinja". + """ + + def __init__(self, literal_reader_class=YamlReader): + super(DefaultReaderSource, self).__init__() + self.literal_reader_class = literal_reader_class + + def get_reader(self, context, location, loader): + if isinstance(location, LiteralLocation): + return self.literal_reader_class(context, location, loader) + + elif isinstance(location, UriLocation): + for extension, reader_class in EXTENSIONS.iteritems(): + if location.uri.endswith(extension): + return reader_class(context, location, loader) + + return super(DefaultReaderSource, self).get_reader(context, location, loader) http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/8ee1470e/aria/parser/reading/yaml.py ---------------------------------------------------------------------- diff --git a/aria/parser/reading/yaml.py b/aria/parser/reading/yaml.py new file mode 100644 index 0000000..55bcbd6 --- /dev/null +++ b/aria/parser/reading/yaml.py @@ -0,0 +1,114 @@ +# 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. + +from collections import OrderedDict + +from ruamel import yaml # @UnresolvedImport + +from .reader import Reader +from .locator import Locator +from .exceptions import ReaderSyntaxError +from .locator import LocatableString, LocatableInt, LocatableFloat + +# Add our types to ruamel.yaml +yaml.representer.RoundTripRepresenter.add_representer( + LocatableString, yaml.representer.RoundTripRepresenter.represent_unicode) +yaml.representer.RoundTripRepresenter.add_representer( + LocatableInt, yaml.representer.RoundTripRepresenter.represent_int) +yaml.representer.RoundTripRepresenter.add_representer( + LocatableFloat, yaml.representer.RoundTripRepresenter.represent_float) + +MERGE_TAG = u'tag:yaml.org,2002:merge' +MAP_TAG = u'tag:yaml.org,2002:map' + + +class YamlLocator(Locator): + """ + Map for agnostic raw data read from YAML. + """ + + def add_children(self, node): + if isinstance(node, yaml.SequenceNode): + self.children = [] + for child_node in node.value: + self.add_child(child_node) + elif isinstance(node, yaml.MappingNode): + self.children = {} + for k, child_node in node.value: + self.add_child(child_node, k) + + def add_child(self, node, key=None): + locator = YamlLocator(self.location, node.start_mark.line + 1, node.start_mark.column + 1) + if key is not None: + # Dict + if key.tag == MERGE_TAG: + for merge_key, merge_node in node.value: + self.add_child(merge_node, merge_key) + else: + self.children[key.value] = locator + else: + # List + self.children.append(locator) + locator.add_children(node) + + +def construct_yaml_map(self, node): + data = OrderedDict() + yield data + value = self.construct_mapping(node) + data.update(value) + + +yaml.constructor.SafeConstructor.add_constructor(MAP_TAG, construct_yaml_map) + + +class YamlReader(Reader): + """ + ARIA YAML reader. + """ + + def read(self): + data = self.load() + try: + data = unicode(data) + # see issue here: + # https://bitbucket.org/ruamel/yaml/issues/61/roundtriploader-causes-exceptions-with + #yaml_loader = yaml.RoundTripLoader(data) + yaml_loader = yaml.SafeLoader(data) + try: + node = yaml_loader.get_single_node() + locator = YamlLocator(self.loader.location, 0, 0) + if node is not None: + locator.add_children(node) + raw = yaml_loader.construct_document(node) + else: + raw = OrderedDict() + #locator.dump() + setattr(raw, '_locator', locator) + return raw + finally: + yaml_loader.dispose() + except yaml.parser.MarkedYAMLError as e: + context = e.context or 'while parsing' + problem = e.problem + line = e.problem_mark.line + column = e.problem_mark.column + snippet = e.problem_mark.get_snippet() + raise ReaderSyntaxError('YAML %s: %s %s' % + (e.__class__.__name__, problem, context), + location=self.loader.location, + line=line, + column=column, + snippet=snippet, + cause=e) + except Exception as e: + raise ReaderSyntaxError('YAML: %s' % e, cause=e) http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/8ee1470e/aria/parser/specification.py ---------------------------------------------------------------------- diff --git a/aria/parser/specification.py b/aria/parser/specification.py new file mode 100644 index 0000000..7a269f5 --- /dev/null +++ b/aria/parser/specification.py @@ -0,0 +1,79 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. + +from collections import OrderedDict + +from .utils import full_type_name + +DSL_SPECIFICATION = {} +DSL_SPECIFICATION_PACKAGES = [] + +URL = { + 'tosca-simple-profile-1.0': 'http://docs.oasis-open.org/tosca/TOSCA-Simple-Profile-YAML/v1.0/' + 'csprd02/TOSCA-Simple-Profile-YAML-v1.0-csprd02.html', + 'tosca-simple-nfv-1.0': 'http://docs.oasis-open.org/tosca/tosca-nfv/v1.0/tosca-nfv-v1.0.html'} + + +def dsl_specification(section, spec): + """ + Decorator for TOSCA specification. + + Used for documentation and standards compliance. + """ + + def decorator(obj): + specification = DSL_SPECIFICATION.get(spec) + if specification is None: + specification = {} + DSL_SPECIFICATION[spec] = specification + if section in specification: + raise Exception('you cannot specify the same @dsl_specification twice, consider adding' + ' \'-1\', \'-2\', etc.: %s, %s' % (spec, section)) + + url = URL.get(spec) + if url: + doc = obj.__doc__ + if doc is not None: + url_start = doc.find(url) + if url_start != -1: + url_end = doc.find('>', url_start + len(url)) + if url_end != -1: + url = doc[url_start:url_end] + + specification[section] = OrderedDict(( + ('code', full_type_name(obj)), + ('url', url))) + try: + setattr(obj, DSL_SPECIFICATION, {section: section, spec: spec}) + except BaseException: + pass + return obj + return decorator + + +def iter_spec(spec): + sections = DSL_SPECIFICATION[spec] + keys = sections.keys() + def key(value): + try: + parts = value.split('-', 1) + first = (int(v) for v in parts[0].split('.')) + second = parts[1] if len(parts) > 1 else None + return (first, second) + except ValueError: + return value + keys.sort(key=key) + for key in keys: + yield key, sections[key] http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/8ee1470e/aria/parser/tools/__init__.py ---------------------------------------------------------------------- diff --git a/aria/parser/tools/__init__.py b/aria/parser/tools/__init__.py new file mode 100644 index 0000000..ae1e83e --- /dev/null +++ b/aria/parser/tools/__init__.py @@ -0,0 +1,14 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/8ee1470e/aria/parser/tools/cli.py ---------------------------------------------------------------------- diff --git a/aria/parser/tools/cli.py b/aria/parser/tools/cli.py new file mode 100644 index 0000000..a274e49 --- /dev/null +++ b/aria/parser/tools/cli.py @@ -0,0 +1,69 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. + +from aria.parser import install_aria_extensions +from aria.parser.utils import (print_exception, import_fullname) +from aria.parser.tools.utils import (CommonArgumentParser, create_context_from_namespace) +from aria.parser.consumption import (ConsumerChain, Read, Validate, Model, Types, Inputs, Instance) + +class ArgumentParser(CommonArgumentParser): + def __init__(self): + super(ArgumentParser, self).__init__(description='CLI', prog='aria') + self.add_argument('uri', help='URI or file path to profile') + self.add_argument('consumer', + nargs='?', + default='instance', + help='consumer class name (full class path or short name)') + +def main(): + try: + + args, unknown_args = ArgumentParser().parse_known_args() + + install_aria_extensions() + + context = create_context_from_namespace(args) + context.args = unknown_args + + consumer = ConsumerChain(context, (Read, Validate)) + + consumer_class_name = args.consumer + dumper = None + if consumer_class_name == 'presentation': + dumper = consumer.consumers[0] + elif consumer_class_name == 'model': + consumer.append(Model) + elif consumer_class_name == 'types': + consumer.append(Model, Types) + elif consumer_class_name == 'instance': + consumer.append(Model, Inputs, Instance) + else: + consumer.append(Model, Inputs, Instance) + consumer.append(import_fullname(consumer_class_name)) + + if dumper is None: + # Default to last consumer + dumper = consumer.consumers[-1] + + consumer.consume() + + if not context.validation.dump_issues(): + dumper.dump() + + except Exception as e: + print_exception(e) + +if __name__ == '__main__': + main()