#!/usr/bin/env python3
# Author(s): Toni Sissala
# Copyright 2019 Finnish Social Science Data Archive FSD / University of Tampere
# Licensed under the EUPL. See LICENSE.txt for full license.
"""Properties and actions for field types supported by records
defined in :mod:`kuha_common.document_store.records`
Provides field types to be used not only for the construction of new records
and updating exising records, but also to provide a format for fields
of records that is interchangeable in a way that a receiver does not need to know the
specifics of a field beforehand, but may use the field to gain knowledge
of the properties of the field.
This module also provides factories which are used to fabricate the fieldtypes.
The instantiated factories hold knowledge of the fields even thought the fields themselves
are not yet instantiated. This knowledge is used for querying records, but also
to dynamically fabricate fieldtypes for records in
:mod:`kuha_common.document_store.records`
"""
from .constants import REC_FIELDNAME_LANGUAGE
[docs]class FieldTypeException(Exception):
"""Exception to raise on field type errors.
Used for programming errors.
"""
[docs]class Value:
"""Value is the most simple type of field.
Field type with name and single value.
Serves also as a baseclass for other field types.
:param name: Name of the field.
:type name: str
:param value: Optional value for the field.
"""
def __init__(self, name, value=None):
self.name = name
self.value = None
if value is not None:
self.set_value(value)
[docs] def set_value(self, value):
"""Set initial value.
:param value: Value to set.
"""
self.value = value
return self
[docs] def add_value(self, value):
"""Add value for the field.
:note: Overrides existing value.
:param value: The value to set.
"""
self.set_value(value)
return self
[docs] def get_value(self):
"""Get the value of the field.
:returns: Value.
"""
return self.value
[docs] def get_name(self):
"""Get name of the field.
:returns: The name of the field.
:rtype: str
"""
return self.name
[docs] def export_dict(self):
"""Exports Value as dictinary.
:returns: {name:value}
:rtype: dict
"""
if self.value is None:
return {}
return {self.get_name(): self.get_value()}
[docs] def import_records(self, record):
"""Import record to field.
:param record: record to import.
"""
self.set_value(record)
[docs] def updates(self, secondary_values):
"""Update value.
:param secondary_values: Value to update to.
:type secondary_values: str
"""
self.set_value(secondary_values)
[docs]class Set(Value):
"""Set is a field type with name and list of unique values.
Derived from :class:`Value`
Implements methods that differ from parent class.
:param name: Name of the field.
:type name: str
:param value: Optional value for the field.
:type value: list or None
"""
def __init__(self, name, value=None):
if value is None:
value = []
super().__init__(name, value)
[docs] def set_value(self, value):
"""Sets value.
:param value: Value for field.
:type value: list
:raises: :exc:`FieldTypeException` if submitted value is not a list.
"""
if not isinstance(value, list):
raise FieldTypeException("Unsupported value {}".format(value))
self.value = value
[docs] def add_value(self, value):
"""Add value to field.
Appends a value to the list of values already set.
Makes sure that the list holds no duplicates by silently
discarding them.
:param value: value or values to be appended.
If value is None, empties the list.
:type value: list or str or None
"""
if self.value is None:
self.value = []
if isinstance(value, list):
self.value.extend(value)
# Remove duplicates
self.value = list(set(self.value))
else:
if value not in self.value:
self.value.append(value)
[docs] def import_records(self, record):
"""Import records by adding the submitted records
to contained values.
:param record: Hold the values to be imported.
:type record: list or str or None
"""
self.add_value(record)
[docs] def updates(self, secondary_values):
"""Updates old values with values contained in this Set.
Looks for combination of secondary_values and values in this set.
Discards duplicates and stores the updated values to ``value``.
:param secondary_values: list of old values to be updated with new ones.
:type secondary_values: list
"""
_value = list(set(secondary_values).union(set(self.value)))
self.set_value(_value)
[docs]class Element(Value):
"""Element is a field type with name, value and attributes.
Derived from :class:`Value`.
Element is used to store fields that contain attributes in
addition to a value. Each attribute in itself
is an instance of :class:`Value` and is dynamically stored in
instance variable ``attr_<name>``. When instantiated and populated with
values and attributes, the element-instance can be used to get it's value,
value's name, but also to get the attributes and their names, even thought
the caller does not know the attribute names a priori.
Example of constructing an element (the source)::
>>> from kuha_common.document_store.field_types import Element
>>> animal = Element('animal', ['color', 'weight', 'height'])
>>> animal.add_value('cat', color='yellow', weight=10, height=5)
Example of reading from an unknown element (the receiver)::
>>> unknown_element.get_name()
'animal'
>>> unknown_element.get_value()
'cat'
>>> for att in unknown_element.iterate_attributes():
... att.get_name() + ' : ' + str(att.get_value())
...
'height : 5'
'color : yellow'
'weight : 10'
>>> unknown_element.attr_color.get_value()
'yellow'
This is especially useful when using as an interchange format. The receiver
does not need to know the attribute names beforehand. Instead the receiver can
iterate throught every attribute to get their name-value pairs or if
the receiver is interested in a single attribute, it may be called by the dynamically
constructed instance-variable prefixed with `attr_`.
:param name: Name of the field.
:type name: str
:param attribute_names: Optional parameter for attribute names.
:type attribute_names: list
:raises: :exc:`FieldTypeException` if `attribute_names` has duplicates.
"""
def __init__(self,
name,
attribute_names=None):
super().__init__(name)
if attribute_names:
if len(attribute_names) > len(set(attribute_names)):
# Has duplicates
raise FieldTypeException(
"Element cannot have attributes with duplicate names: {}"
.format(', '.join(attribute_names))
)
self.attribute_names = attribute_names
for att in attribute_names:
setattr(self, 'attr_{}'.format(att), Value(att))
else:
self.attribute_names = []
self.attributes = []
self.pending_values = True
[docs] def is_pending(self):
"""Is the element pending for values.
:returns: True if pending, False if not.
:rtype: bool
"""
return self.pending_values
def _set_pending_values(self, pending):
self.pending_values = bool(pending)
[docs] def new(self):
"""Create a new element-instance with same name and attributes
but without values.
Instantiates a new instance of itself.
The new instance is pending for values.
Example::
>>> animal = Element('animal', ['color', 'weight', 'height'])
>>> animal.add_value('cat', color='yellow', weight=10, height=5)
>>> another_animal = animal.new()
>>> another_animal.add_value('dog', color='white', weight=30, height=15)
:returns: new element.
:rtype: :class:`Element`
"""
instance = self.__class__(self.name, self.attribute_names)
return instance
[docs] def add_value(self, value=None, **attributes):
r"""Add value with attributes as keyword arguments.
:note: This may only be called once for each instance.
Example::
>>> from kuha_common.document_store.field_types import Element
>>> animal = Element('animal', ['color', 'weight', 'height'])
>>> animal.add_value('cat', color='yellow', weight=10, height=5)
:param value: Value for the element.
:type value: str or int or None
:param \*\*attributes: keyword arguments for attributes of the element.
:raises: :exc:`FieldTypeException` if the element already has values
or if submitted value is None and no attributes are given.
"""
if not self.is_pending():
raise FieldTypeException("Element %s already has values" % (self.name,))
if value is None and all(val is None for val in attributes.values()):
raise FieldTypeException("Give value or attributes for Element %s" % (self.name,))
self.set_value(value)
self._add_attributes(**attributes)
self._set_pending_values(False)
[docs] def iterate_attributes(self):
"""Generator function. Iterates element attributes.
:returns: a generator object for iterating attributes.
"""
for att in self.attributes:
yield att
[docs] def get_attribute(self, name):
"""Get attribute by attribute name.
:param name: Name of the attribute to look for.
:type name: str
:returns: attribute of the element or None if not found.
:rtype: :class:`Value` or None
"""
for att in self.iterate_attributes():
if att.get_name() == name:
return att
return None
def _add_attributes(self, **kwargs):
if self.attributes:
raise FieldTypeException("Attributes already set")
for _k, _v in kwargs.items():
self.set_attribute(_k, _v)
[docs] def set_attribute(self, name, value):
"""Sets new value for attribute.
:note: The element must have an attribute with the `name`.
:param name: attribute name.
:type name: str
:param value: new value.
:type value: str or int or None
:raises: :exc:`FieldTypeException` if element does not have an attribute with
submitted `name`.
"""
if name not in self.attribute_names:
raise FieldTypeException(
'{} not a supported attribute for {}'.format(name, self.name)
)
_att = self.get_attribute(name)
if _att:
_att.set_value(value)
else:
self.attributes.append(getattr(self, 'attr_{}'.format(name)).set_value(value))
[docs] def export_attributes_as_dict(self):
"""Export element's attributes as a dictionary.
:returns: dictinary representing the attributes
:rtype: dict
"""
result = {}
for att in self.iterate_attributes():
result.update(att.export_dict())
return result
[docs] def export_dict(self):
"""Export the element as a dictionary.
Returns a dictionary with key-value pairs given wrapped
inside a another dictionary with the elements key as name.
Example::
>>> from kuha_common.document_store.field_types import Element
>>> animal = Element('animal', ['color', 'weight', 'height'])
>>> animal.add_value('cat', color='yellow', weight=10, height=5)
>>> animal.export_dict()
{'animal': {'color': 'yellow', 'weight': 10, 'height': 5, 'animal': 'cat'}}
:returns: dictinary representing the :class:`Element`
:rtype: dict
"""
result = self.export_attributes_as_dict()
result.update(super().export_dict())
return {self.get_name(): result}
[docs] def import_records(self, record):
"""This object does not support importing records.
:raises: :exc:`FieldTypeException`
"""
raise FieldTypeException(
"Element does not support importing of records: {}".format(self.name)
)
[docs] def updates(self, secondary_values):
"""Updates attributes not found in this element with the ones
found from `secondary_values`.
:param secondary_values: Attributes from old element.
:type secondary_values: dict
"""
if self.attributes:
for old_attr_key, old_attr_val in secondary_values.items():
if not self.get_attribute(old_attr_key):
self.set_attribute(old_attr_key, old_attr_val)
else:
self._add_attributes(**secondary_values)
[docs]class LocalizableElement(Element):
"""LocalizableElement is a field type with name, value, language and attributes.
Derived from :class:`Element`. Has an additional attribute for language.
The language is special attribute that is used when updating elements.
:seealso: :class:`Element`
:param name: Name of the element.
:type name: str
:param attribute_names: Optional list of attribute names.
:type attribute_names: list
:raises: :exc:`FieldTypeException` if `attribute_names` contain a name that is reserved for language.
"""
def __init__(self, name, attribute_names=None):
super().__init__(name, attribute_names)
if REC_FIELDNAME_LANGUAGE in self.attribute_names:
raise FieldTypeException(
"LocalizableElement ({}) cannot have an attribute named {}. It is reserved for language."
.format(self.name, REC_FIELDNAME_LANGUAGE)
)
self.language = None
[docs] def set_language(self, language):
"""Set language for element.
:param language: language to set.
:type language: str
:raises: :exc:`FieldTypeException` if language already set.
"""
if self.language:
raise FieldTypeException("Language already set")
self.language = language
[docs] def get_language(self):
"""Get language of element.
:returns: language
:rtype: str or None
"""
return self.language
[docs] def add_value(self, value=None, language=None, **attributes):
r"""Add values for element.
:note: This may only be called once for each instance.
:seealso: :meth:`Element.add_value`
:param value: value to set.
:type value: str or int
:param language: language of the element.
:type language: str
:param \*\*attributes: keyword arguments for attributes of the element.
:raises: :exc:`TypeError` if language is not given or is None.
"""
if language is None:
raise TypeError("LocalizableElement requires language")
super().add_value(value, **attributes)
self.set_language(language)
[docs] def export_dict(self):
"""Export the element as a dictionary.
:seealso: :meth:`Element.export_dict`
:returns: dictinary representation of the element.
:rtype: dict
"""
result = super().export_dict()
result[self.name].update({REC_FIELDNAME_LANGUAGE: self.get_language()})
return result
[docs]class ElementContainer(Value):
"""ElementContainer contains a list of single type of Element/LocalizableElement field types.
Receives mandatory parameters for `name` and `sub_element`. The `sub_element` describes
the element types that this container can store. Every new element that a container
can create will be an instance created from this `sub_element`.
Example::
>>> from kuha_common.document_store.field_types import ElementContainer, LocalizableElement
>>> animal = LocalizableElement('animal', ['color', 'width', 'height'])
>>> animals = ElementContainer('animals', animal)
>>> animals.add_value('cat', 'en', color='yellow', width=10, height=5)
>>> animals.add_value('kissa', 'fi', color='keltainen', width=10, height=5)
>>> animals.export_dict() # result formatted for better readability
{'animals': [
{'width': 10,
'language': 'en',
'color': 'yellow',
'height': 5,
'animal': 'cat'},
{'width': 10,
'language': 'fi',
'color': 'keltainen',
'height': 5,
'animal': 'kissa'}]
}
Elements can be iterated::
>>> for animal in animals:
... animal.attr_color.get_value() + " for language: " + animal.get_language()
...
'yellow for language: en'
'keltainen for language: fi'
And updated with containers sharing name and attribute names::
>>> another_animal = LocalizableElement('animal', ['color', 'width', 'height'])
>>> more_animals = ElementContainer('animals', another_animal)
>>> more_animals.add_value('dog', 'en', color='white', width=20, height=10)
>>> more_animals.add_value('koira', 'fi', color='valkoinen', width=20, height=10)
>>> animals.updates(more_animals)
>>> animals.export_dict() # result formatted for better readability
{'animals': [
{'language': 'en',
'height': 5,
'color': 'yellow',
'animal': 'cat',
'width': 10},
{'language': 'fi',
'height': 5,
'color': 'keltainen',
'animal': 'kissa',
'width': 10},
{'language': 'en',
'height': 10,
'color': 'white',
'animal': 'dog',
'width': 20},
{'language': 'fi',
'height': 10,
'color': 'valkoinen',
'animal': 'koira',
'width': 20}]
}
:param name: name of the container.
:type name: str
:param sub_element: element to contain.
:type sub_element: :class:`LocalizableElement` or
:class:`Element`
:raises: :exc:`FieldTypeException` for invalid sub_element.
"""
def __init__(self,
name,
sub_element):
super().__init__(name)
if not hasattr(sub_element, 'get_name'):
raise FieldTypeException(
"Invalid sub_element for %s. sub_element must be a "
"descendant of Value or implement required interfaces." % (name,)
)
if not hasattr(sub_element, 'new'):
raise FieldTypeException(
"Invalid sub_element for %s : "
"sub_element must implement new() interface" % (name,)
)
self.sub_element = sub_element
self.value = []
self._localizable = hasattr(sub_element, 'get_language')
def __iter__(self):
for val in self.value:
yield val
[docs] def import_records(self, record):
"""Imports records from a list of dictionaries.
:note: The dictionaries will lose information.
:param record: list of dictionaries with records to import.
:type record: list
"""
for _dict in record:
_value = _dict.pop(self.sub_element.get_name(), None)
_lang = _dict.pop(REC_FIELDNAME_LANGUAGE, None)
self.add_value(_value, language=_lang, **_dict)
[docs] def add_value(self, value=None, language=None, **kwargs):
r"""Add new element to list of elements
:param value: value for the new element.
:type value: str or int or None
:param language: language for the new element.
:type language: str or None
:param \*\*kwargs: key-value pairs for attributes of the new element.
:raises: :exc:`FieldTypeException` for invalid language parameter
depending on whether the :attr:`sub_element` is localizable.
"""
new_element = self.sub_element.new()
if language is None:
if self._localizable is True:
raise FieldTypeException(
"Language must be provided for localizable sub_element %s" % (self.get_name(),))
new_element.add_value(value, **kwargs)
else:
if self._localizable is False:
raise FieldTypeException("Language not supported by sub_element for %s" % (self.get_name(),))
new_element.add_value(value, language, **kwargs)
self.value.append(new_element)
[docs] def export_dict(self):
"""Export container as dictionary.
:returns: dictionary representing the container.
:rtype: dict
"""
_results = []
_sub_element_name = self.sub_element.get_name()
for val in self:
_results.append(val.export_dict()[_sub_element_name])
return {self.get_name(): _results}
[docs] def iterate_values_for_language(self, language):
"""Generator for iterating contained elements by language.
:param language: language which is used to filter yielded results.
:type language: str
:returns: a generator object for iterating elements
"""
if not self._localizable:
return
for val in self:
if val.get_language() == language:
yield val
[docs] def get_available_languages(self):
"""Get list of languages for this container.
:returns: list of distinct languages.
:rtype: list
"""
if not self._localizable:
return []
langs = set()
for val in self:
langs.add(val.get_language())
return list(langs)
[docs] def updates(self, secondary_values):
"""Updates contained values with `secondary_values`.
Looks for values that are not currently contained, and
appends them as contained values. Also appends different language
versions. If a language version has the same value, looks for
differences in attributes. If new value has not the same attributes
as the old one, adds these attributes to the new value. If old value
has same attribute name, it will be discarded.
:note: Document Store uses MongoDB as a backend.
MongoDB deals with JSON-like objects, which in turn
are best represented in Python as dictionaries.
The purpose of :mod:`kuha_common.document_store.records`
is to be used as a global (in Kuha context) interchange format
and so it will be best to support both dictionaries and
ElementContainers for this operation.
Therefore there is some flexibility in the type of
parameter that this method accepts.
:note: There is a logical difference in which type of
parameter is submitted to this method. When using
other types than ElementContainers, the parameter's content
will be changed.
:param secondary_values: Old values known to have the same container (must
have the same name). If secondary_values is a list,
it is assumed that the caller has explicitly checked that
the parameter represents old values for this container.
Otherwise the name of the container will be checked here
and KeyError exceptions will be raised.
:type secondary_values: instance of :class:`ElementContainer` or dict or list
"""
if hasattr(secondary_values, 'export_dict'):
# Assume ElementContainer. Convert to list.
secondary_values = secondary_values.export_dict()[self.get_name()]
elif hasattr(secondary_values, 'keys'):
# Assume dict. Convert to list.
secondary_values = secondary_values[self.get_name()]
self._updates(secondary_values)
def _iter_ext_val_lang_and_attrs(self, external_values):
"""Extract value, language and attributes from external_values list.
Returns a tuple containing three items (value, language, attributes).
Value and language may be None if they are not specified. attributes is
a dictionary, and may be an empty dict.
:param external_values: values to extract and yield.
:type external_values: list or iterable
:returns: Tuple of three items (value, language, attributes)
:rtype: tuple
"""
for values_dict in external_values:
value = values_dict.pop(self.sub_element.get_name(), None)
language = values_dict.pop(REC_FIELDNAME_LANGUAGE, None)
# from this point on, values_dict contains only attributes.
yield value, language, values_dict
def _iter_value_with_value_or_attributes(self, value, language=None, attributes=None):
"""Yield contained values that match with value, language and attributes.
If contained element is localizable, the parameter for language must not be None.
If contained element is not localizabe, the parameter for language must be None.
If parameter attributes is None, this will match based on value and language only.
:param value: Value to match
:type value: str or None
:param language: Language to match
:type language: str or None
:param attributes: attributes to match
:type attributes: dict or None
:returns: matched contained value
:rtype: :obj:`Element` or :obj:`LocalizableElement`
"""
if self._localizable and language is None:
raise TypeError("Must provide language for localizable element: %s" % (self.get_name(),))
if not self._localizable and language is not None:
raise TypeError("Cannot provide language for non localizable element: %s" % (self.get_name(),))
_iterator = self.iterate_values_for_language(language) if self._localizable else self
def compare_value_and_attrs(contained_value):
return contained_value.get_value() == value and contained_value.export_attributes_as_dict() == attributes
def compare_value(contained_value):
return contained_value.get_value() == value
compare_func = compare_value_and_attrs if attributes is not None else compare_value
for contained_value in _iterator:
if compare_func(contained_value):
yield contained_value
def _updates(self, secondary_values):
"""Updates contained values with secondary_values. If cannot find updatable contained value
creates a new contained value based on a secondary value.
:param secondary_values: values to update contained values with.
:returns: None
"""
new_values_from_secondary = []
for sec_value, sec_language, sec_attrs in self._iter_ext_val_lang_and_attrs(secondary_values):
# Loop through primary values. Append to new_values_from_secondary if there is
# need to add new values based on secondary values. Add secondary values as a
# last step to avoid comparing secondary values to secondary values.
if sec_value is None:
matching_values = list(self._iter_value_with_value_or_attributes(sec_value, sec_language, sec_attrs))
if matching_values == []:
# Didn't find matching attributes with None-value
new_values_from_secondary.append((sec_value, sec_language, sec_attrs))
continue
found_matching_value = False
for matching_value in self._iter_value_with_value_or_attributes(sec_value, sec_language):
found_matching_value = True
matching_value.updates(sec_attrs)
if found_matching_value is False:
new_values_from_secondary.append((sec_value, sec_language, sec_attrs))
for sec_value, sec_language, sec_attrs in new_values_from_secondary:
# Add secondary values that didn't match with primary values.
self.add_value(sec_value, language=sec_language, **sec_attrs)
[docs]class FieldAttribute:
"""Common attributes for each field type.
Stores fields name, parent fields name and constructs
a path for the field. This path can be used when
building queries against Document Store. The name can
be used to lookup values from objects returned from Document
Store.
Used by :class:`FieldTypeFactory` to store information
of fields that can be used before the field has been
fabricated.
:param name: name of the field.
:type name: str
:param parent: optional parameter parent. Used for sub-elements.
:type parent: str
"""
def __init__(self, name, parent=None):
self.name = name
self.parent = parent
if self.parent:
self.path = "{}.{}".format(self.parent, self.name)
else:
self.path = name
[docs] def value_from_dict(self, _dict):
"""Get value or values corresponding to :attr:`path` from parameter.
:note: Returned values cannot be separated by language afterwards.
:param _dict: dictionary to lookup for :attr:`path`.
:type _dict: dict
:returns: value or values stored in path of the _dict.
:rtype: str or list or None
"""
tmp = _dict.copy()
keys = self.path.split('.')
last_key = keys.pop()
for key in keys:
# Recurse as deep as possible with keys
if key not in tmp:
return None
tmp = tmp[key]
if hasattr(tmp, 'get'):
# tmp is a dict
return tmp.get(last_key)
# Finally handle a list of dictionaries
result = [item[last_key] for item in tmp if last_key in item]
if result == []:
return None
return result
[docs]class FieldTypeFactory:
"""Factory for field types.
Stores information for each field, that can be used
before the field actually has been initiated. This is useful
for building queries against Document Store, because the caller
needs to know the names and paths of the fields about to be queried.
The attributes stored here are also used to fabricate each field type.
This means that each of the field types supported by
:mod:`kuha_common.document_store.records` are to be initiated
throught this factory.
:seealso: :class:`ElementContainer`
Example::
>>> from kuha_common.document_store.field_types import FieldTypeFactory
>>> animals_factory = FieldTypeFactory('animals', 'animal', ['color', 'width', 'height'])
>>> animals_factory.attr_color.name
'color'
>>> animals_factory.attr_color.path
'animals.color'
>>> animals = animals_factory.fabricate()
>>> animals.add_value('cat', 'en', color='yellow', height=10, width=5)
>>> animals.export_dict()
{'animals': [{'color': 'yellow', 'animal': 'cat', 'height': 10, 'width': 5, 'language': 'en'}]}
:param name: name of the field.
:type name: str
:param sub_name: name of the sub field, if any.
:type sub_name: str
:param attrs: field attributes, if any. Multiple attributes in list.
:type attrs: list or str
:param localizable: is the field localizable.
:type localizable: bool
:param single_value: The fabricated field can contain only a single value.
:type single_value: bool
:raises: :exc:`ValueError` if attribute has same name as the element or sub_element.
:raises: :exc:`FieldTypeException` for parameter combinations that are not supported.
"""
def __init__(self, name, sub_name=None, attrs=None, localizable=True, single_value=False):
self.name = FieldAttribute(name)
self.path = name
self.sub_name = FieldAttribute(
sub_name, self.path) if sub_name else None
self.attrs = []
if attrs:
if isinstance(attrs, list):
for att in attrs:
self.__add_attr(att)
else:
self.__add_attr(attrs)
if localizable not in [True, False]:
raise TypeError("Invalid value for localizable")
self.localizable = localizable
if single_value not in [True, False]:
raise TypeError("Invalid value for single_value")
self.single_value = single_value
if self.localizable:
self.__add_attr(REC_FIELDNAME_LANGUAGE, False)
self._validate_params()
def __add_attr(self, attr_name, from_params=True):
_comp = self.sub_name.name if self.sub_name else self.name.name
if attr_name == _comp:
raise ValueError(
"Cannot have attribute of the same name as element: {}".format(_comp)
)
if attr_name == REC_FIELDNAME_LANGUAGE and from_params:
raise ValueError(
"Cannot set attribute name that is reserved for localization."
)
setattr(self,
'attr_{}'.format(attr_name),
FieldAttribute(attr_name, self.path))
if from_params:
self.attrs.append(attr_name)
def _validate_params(self):
_valid = True
_msgs = []
if self.single_value:
if self.attrs:
_valid = False
_msgs.append("Type cannot support attributes and have only single value")
if self.localizable:
_valid = False
_msgs.append("Type cannot be localizable and have only single value")
if self.sub_name:
_valid = False
_msgs.append("Type cannot be have sub-element and have only single value")
elif self.sub_name is None and self.attrs == [] and self.localizable:
_valid = False
_msgs.append("Set element cannot not be localizable")
if not _valid:
raise FieldTypeException("{}: {}".format(', '.join(_msgs), self.path))
[docs] def fabricate(self):
"""Fabricate field type by factory attributes.
Returns the correct type of field type based on attributes given to the
factory at initialization time.
:returns: Instance of one of the fields types.
"""
if self.sub_name:
if self.localizable:
return ElementContainer(self.path, LocalizableElement(self.sub_name.name, self.attrs))
return ElementContainer(self.path, Element(self.sub_name.name, self.attrs))
if self.attrs:
if self.localizable:
return LocalizableElement(self.path, self.attrs)
return Element(self.path, self.attrs)
if self.single_value:
return Value(self.path)
return Set(self.path)