Source code for kuha_common.document_store.field_types

#!/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,
    SEPARATOR_PATH
)


[docs]class FieldTypeException(Exception): """Exception to raise on field type errors. Used for programming errors. """
def _attrname(name): return 'attr_{}'.format(name)
[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 """ 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 and 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 or [] for attname in self._attribute_names: attobj = Value(attname) setattr(self, _attrname(attname), attobj) 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 attname in self._attribute_names: yield getattr(self, _attrname(attname))
[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): for attname in self._attribute_names: if attname not in kwargs: continue attval = kwargs.pop(attname) _att = self.get_attribute(attname) _att.set_value(attval) if kwargs != {}: raise FieldTypeException( "Unknown attributes %s for element '%s'" % (', '.join("'%s'" % (key,) for key in kwargs), self.get_name()))
[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): """Import records. Import records by adding the submitted records to value and attributes. :note: the record parameter is emptied by this method. :param dict record: Record to import. """ main_value = record.pop(self.name, None) kwargs = {} for attname in self._attribute_names: kwargs[attname] = record.pop(attname, None) if record != {}: raise FieldTypeException( "Element '%s' does not support attributes: %s" % (self.name, ', '.join("'%s'" % (key,) for key in record.keys())) ) self.add_value(main_value, **kwargs)
[docs] def updates(self, secondary_values): """Updates attributes not found in this element with the ones found from `secondary_values`. Not found in this context means values that are None. :note: The parameter is iterated destructively. :param dict secondary_values: Attributes from old element. """ for att in self.iterate_attributes(): attname = att.get_name() sec_val = secondary_values.pop(attname, None) if att.get_value() is None and sec_val is not None: att.set_value(sec_val) if secondary_values != {}: raise FieldTypeException( "Element '%s' does not support attributes: %s" % (self.name, ', '.join("'%s'" % (key,) for key in secondary_values.keys())))
[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 def __len__(self): return len(self.value) def __bool__(self): """Make the object always truthy. Since there is a __len__, an empty container evaluates as falsy. Make the object always truthy. """ return True def __getitem__(self, index): return self.value[index]
[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, SEPARATOR_PATH, 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(SEPARATOR_PATH) 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] return result or None
[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, _attrname(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: field_type = ElementContainer(self.path, LocalizableElement(self.sub_name.name, self.attrs)) else: field_type = ElementContainer(self.path, Element(self.sub_name.name, self.attrs)) elif self.attrs: if self.localizable: field_type = LocalizableElement(self.path, self.attrs) else: field_type = Element(self.path, self.attrs) elif self.single_value: field_type = Value(self.path) else: field_type = Set(self.path) return field_type