Source code for xsdata.codegen.handlers.attribute_type

from dataclasses import dataclass
from dataclasses import field
from typing import Dict
from typing import List
from typing import Optional
from typing import Set
from typing import Tuple

from xsdata.codegen.mixins import ContainerInterface
from xsdata.codegen.mixins import HandlerInterface
from xsdata.codegen.models import Attr
from xsdata.codegen.models import AttrType
from xsdata.codegen.models import Class
from xsdata.codegen.models import Status
from xsdata.codegen.utils import ClassUtils
from xsdata.logger import logger
from xsdata.models.enums import DataType
from xsdata.utils import collections


[docs]@dataclass class AttributeTypeHandler(HandlerInterface): """Minimize class attributes complexity by filtering and flattening types.""" container: ContainerInterface dependencies: Dict = field(default_factory=dict)
[docs] def process(self, target: Class): """ Process the given class attributes and their types. Ensure all types are unique. """ for attr in list(target.attrs): for attr_type in list(attr.types): self.process_type(target, attr, attr_type) attr.types = self.filter_types(attr.types)
[docs] def process_type(self, target: Class, attr: Attr, attr_type: AttrType): """ Process attribute type, split process for xml schema and user defined types. Ignore forward references to inner classes. """ if attr_type.native: self.process_native_type(attr, attr_type) elif attr_type.forward: self.process_inner_type(target, attr, attr_type) else: self.process_dependency_type(target, attr, attr_type)
[docs] @classmethod def process_native_type(cls, attr: Attr, attr_type: AttrType): """Reset attribute type if the attribute has a pattern restriction as they are not yet supported.""" if attr.restrictions.pattern: cls.reset_attribute_type(attr_type)
[docs] def find_dependency(self, attr_type: AttrType) -> Optional[Class]: """ Find dependency for the given attribute. Avoid conflicts by search in order: 1. Non element/complexType 2. Non abstract 3. anything """ conditions = ( lambda obj: not obj.is_complex, lambda x: not x.abstract, lambda x: True, ) for condition in conditions: result = self.container.find(attr_type.qname, condition=condition) if result: return result return None
[docs] def process_inner_type(self, target: Class, attr: Attr, attr_type: AttrType): """ Process an attributes type that depends on an inner type. If there is a matching simple type inner class copy the inner type attribute properties. """ inner = self.container.find_inner( target, attr_type.name, lambda x: x.is_simple_type ) if inner: self.copy_attribute_properties(inner, target, attr, attr_type) target.inner.remove(inner)
[docs] def process_dependency_type(self, target: Class, attr: Attr, attr_type: AttrType): """ Process an attributes type that depends on any global type. Steps: 1. Reset absent or dummy attribute types with a warning. 2. Copy attribute properties from a simple type. 3. Set circular flag for the rest non enumerations. """ source = self.find_dependency(attr_type) if not source or (not source.attrs and not source.extensions): logger.warning("Reset dummy type: %s", attr_type.name) self.reset_attribute_type(attr_type) elif source.is_simple_type: self.copy_attribute_properties(source, target, attr, attr_type) elif not source.is_enumeration: self.set_circular_flag(source, target, attr_type)
[docs] @classmethod def copy_attribute_properties( cls, source: Class, target: Class, attr: Attr, attr_type: AttrType ): """ Replace the given attribute type with the types of the single field source class. Ignore enumerations and gracefully handle dump types with no attributes. :raises: AnalyzerValueError if the source class has more than one attributes """ source_attr = source.attrs[0] index = attr.types.index(attr_type) attr.types.pop(index) for source_attr_type in source_attr.types: clone_type = source_attr_type.clone() attr.types.insert(index, clone_type) index += 1 restrictions = source_attr.restrictions.clone() restrictions.merge(attr.restrictions) attr.restrictions = restrictions if source.nillable: restrictions.nillable = True ClassUtils.copy_inner_classes(source, target)
[docs] def set_circular_flag(self, source: Class, target: Class, attr_type: AttrType): """Update circular reference flag.""" attr_type.circular = self.is_circular_dependency(source, target, set())
[docs] def is_circular_dependency(self, source: Class, target: Class, seen: Set) -> bool: """Check if any source dependencies recursively match the target class.""" if source is target or source.status == Status.PROCESSING: return True for qname in self.cached_dependencies(source): if qname not in seen: seen.add(qname) check = self.container.find(qname) if check and self.is_circular_dependency(check, target, seen): return True return False
[docs] def cached_dependencies(self, source: Class) -> Tuple[str]: """Returns from cache the source class dependencies as a collection of qualified names.""" cache_key = id(source) if cache_key not in self.dependencies: self.dependencies[cache_key] = tuple(source.dependencies()) return self.dependencies[cache_key]
[docs] @classmethod def reset_attribute_type(cls, attr_type: AttrType): """Reset the attribute type to native string.""" attr_type.qname = DataType.STRING.qname attr_type.native = True attr_type.circular = False attr_type.forward = False
[docs] @classmethod def filter_types(cls, types: List[AttrType]) -> List[AttrType]: """ Remove duplicate and invalid types. Invalid: 1. xs:error 2. xs:anyType and xs:anySimpleType when there are other types present """ xs_error = DataType.ERROR.code types = collections.unique_sequence(types, key="name") types = collections.remove(types, lambda x: x.native_code == xs_error) if len(types) > 1: types = collections.remove(types, lambda x: x.native_type is object) if not types: types.append(AttrType(qname=DataType.STRING.qname, native=True)) return types