blob: 7292562e1ae5c8efc555fe90490547833062b79c [file] [log] [blame]
# 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.caching import HasCachedMethods
from ...utils.collections import deepcopy_with_locators
from ...utils.formatting import safe_repr
from ...utils.type import full_type_name
from ...utils.console import puts
from ..validation import Issue
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.parser.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.parser.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
``_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 ``_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 ``_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 :func:`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)