def process_attribute_default_enum(self, target: Class, attr: Attr): """ Convert string literal default value for enum fields. Loop through all attributes types and search for enum sources. If an enum source exist map the default string literal value to a qualified name. If the source class in inner promote it to root classes. """ source_found = False for attr_type in attr.types: source = self.find_enum(target, attr_type) if not source: continue source_found = True source_attr = next( (x for x in source.attrs if x.default == attr.default), None) if source_attr: if attr_type.forward: self.promote_inner_class(target, source) attr.default = f"@enum@{source.name}::{source_attr.name}" return if source_found: logger.warning( "No enumeration member matched %s.%s default value `%s`", target.name, attr.local_name, attr.default, ) attr.default = None
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 ClassUtils.copy_inner_class(source, target, attr, clone_type) restrictions = source_attr.restrictions.clone() restrictions.merge(attr.restrictions) attr.restrictions = restrictions attr.help = attr.help or source_attr.help if source.nillable: restrictions.nillable = True
def process_attribute_restrictions(cls, attr: Attr): """Sanitize attribute required flag by comparing the min/max occurrences restrictions.""" restrictions = attr.restrictions min_occurs = restrictions.min_occurs or 0 max_occurs = restrictions.max_occurs or 0 if attr.is_attribute: restrictions.min_occurs = None restrictions.max_occurs = None elif attr.is_tokens: restrictions.required = None if max_occurs <= 1: restrictions.min_occurs = None restrictions.max_occurs = None elif attr.xml_type is None or min_occurs == max_occurs == 1: restrictions.required = True restrictions.min_occurs = None restrictions.max_occurs = None elif min_occurs == 0 and max_occurs < 2: restrictions.required = None restrictions.min_occurs = None restrictions.max_occurs = None attr.default = None attr.fixed = False else: # max_occurs > 1 restrictions.min_occurs = min_occurs restrictions.required = None attr.fixed = False if attr.default or attr.fixed or attr.restrictions.nillable: restrictions.required = None
def create_default_attribute(cls, item: Class, extension: Extension): """Add a default value field to the given class based on the extension type.""" if extension.type.native_code == DataType.ANY_TYPE.code: attr = Attr( name="any_element", local_name="any_element", index=0, default=list if extension.restrictions.is_list else None, types=[extension.type.clone()], tag=Tag.ANY, namespace=NamespaceType.ANY.value, restrictions=extension.restrictions.clone(), ) else: attr = Attr( name="value", local_name="value", index=0, default=None, types=[extension.type.clone()], tag=Tag.EXTENSION, restrictions=extension.restrictions.clone(), ) item.attrs.insert(0, attr) item.extensions.remove(extension)
def process(cls, target: Class): """Add or update an existing an xs:anyType derived attribute if the target class supports mixed content.""" if not target.mixed: return wildcard = first(attr for attr in target.attrs if attr.tag == Tag.ANY) if wildcard: wildcard.mixed = True if not wildcard.is_list: wildcard.restrictions.min_occurs = 0 wildcard.restrictions.max_occurs = sys.maxsize else: attr = Attr( name="content", types=[AttrType(qname=str(DataType.ANY_TYPE), native=True)], tag=Tag.ANY, mixed=True, namespace=NamespaceType.ANY_NS, restrictions=Restrictions(min_occurs=0, max_occurs=sys.maxsize), ) target.attrs.insert(0, attr)
def group_fields(self, target: Class, attrs: List[Attr]): """Group attributes into a new compound field.""" pos = target.attrs.index(attrs[0]) names = [] choices = [] min_occurs = [] max_occurs = [] for attr in attrs: target.attrs.remove(attr) names.append(attr.local_name) min_occurs.append(attr.restrictions.min_occurs) max_occurs.append(attr.restrictions.max_occurs) choices.append(self.build_attr_choice(attr)) name = "choice" if len(names) > 3 else "_Or_".join(names) target.attrs.insert( pos, Attr( name=name, index=0, types=[AttrType(qname=str(DataType.ANY_TYPE), native=True)], tag=Tag.CHOICE, restrictions=Restrictions( min_occurs=min((x for x in min_occurs if x is not None), default=0), max_occurs=max((x for x in max_occurs if x is not None), default=0), ), choices=choices, ), )
def process_simple_dependency(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 """ if source.is_enumeration: return total = len(source.attrs) if total == 0: cls.reset_attribute_type(attr_type) elif total == 1: 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 ClassUtils.copy_inner_classes(source, target) else: raise AnalyzerValueError( f"{source.type.__name__} with more than one attribute: `{source.name}`" )
def build_class_attribute( cls, target: Class, obj: ElementBase, parent_restrictions: Restrictions ): """Generate and append an attribute field to the target class.""" target.ns_map.update(obj.ns_map) types = cls.build_class_attribute_types(target, obj) restrictions = Restrictions.from_element(obj) if obj.class_name in (Tag.ELEMENT, Tag.ANY): restrictions.merge(parent_restrictions) if restrictions.prohibited: return name = obj.real_name target.attrs.append( Attr( index=obj.index, name=name, local_name=name, default=obj.default_value, fixed=obj.is_fixed, types=types, tag=obj.class_name, help=obj.display_help, namespace=cls.element_namespace(obj, target.qname.namespace), restrictions=restrictions, ) )
def create( cls, name: Optional[str] = None, index: Optional[int] = None, types: Optional[List[AttrType]] = None, choices: Optional[List[Attr]] = None, tag: Optional[str] = None, namespace: Optional[str] = None, default: Any = None, fixed: bool = False, mixed: bool = False, restrictions: Optional[Restrictions] = None, **kwargs: Any, ) -> Attr: name = name or f"attr_{cls.next_letter()}" return Attr( name=name, index=cls.counter if index is None else index, types=types or [AttrTypeFactory.native(DataType.STRING)], choices=choices or [], tag=tag or random.choice(cls.tags), namespace=namespace or None, default=default or None, fixed=fixed, mixed=mixed, restrictions=restrictions or Restrictions(), **kwargs, )
def process_attribute_default(self, target: Class, attr: Attr): """ Sanitize attribute default value. Cases: 1. List fields can not have a fixed value. 2. Optional fields or xsi:type can not have a default or fixed value. 3. Convert string literal default value for enum fields. """ if attr.is_list: attr.fixed = False if attr.is_optional or attr.is_xsi_type: attr.fixed = False attr.default = None if attr.default and not attr.is_enumeration: self.process_attribute_default_enum(target, attr)
def create_substitution(cls, source: Class) -> Attr: """Create an attribute with type that refers to the given source class and namespaced qualified name.""" return Attr( name=source.name, types=[AttrType(qname=source.qname)], tag=source.type.__name__, namespace=source.namespace, )
def get_or_create_attribute(cls, target: Class, name: str, tag: str) -> Attr: """Find or create for the given parameters an attribute in the target class.""" for attr in target.attrs: if attr.name == attr.local_name == name and attr.tag == tag: return attr attr = Attr(name=name, tag=tag) target.attrs.insert(0, attr) return attr
def get_or_create_attribute(cls, target: Class, name: str, tag: str) -> Attr: """Find or create for the given parameters an attribute in the target class.""" for attr in target.attrs: if attr.name == name: return attr attr = Attr(name=name, tag=tag) attr.restrictions.min_occurs = 1 attr.restrictions.max_occurs = 1 target.attrs.insert(0, attr) return attr
def process_attribute_name(cls, attr: Attr): """ Sanitize attribute name in preparation for duplicate attribute names handler. Steps: 1. Remove non alpha numerical values 2. Handle Enum negative numerical values 3. Remove namespaces prefixes 4. Ensure name not empty 5. Ensure name starts with a letter """ if attr.is_enumeration: attr.name = attr.default if re.match(r"^-\d*\.?\d+$", attr.name): attr.name = f"value_minus_{attr.name}" else: attr.name = re.sub("[^0-9a-zA-Z]", " ", attr.name).strip() else: attr.name = re.sub("[^0-9a-zA-Z]", " ", attr.name).strip() if not attr.name: attr.name = "value" elif not attr.name[0].isalpha(): attr.name = f"value_{attr.name}"
def rename_attr_dependencies(self, attr: Attr, reference: int, replace: str): """Search and replace the old qualified attribute type name with the new one in the attr types, choices and default value.""" for attr_type in attr.types: if attr_type.reference == reference: attr_type.qname = replace if isinstance(attr.default, str) and attr.default.startswith("@enum@"): member = text.suffix(attr.default, "::") attr.default = f"@enum@{replace}::{member}" for choice in attr.choices: self.rename_attr_dependencies(choice, reference, replace)
def rename_attr_dependencies(self, attr: Attr, search: str, replace: str): """Search and replace the old qualified attribute type name with the new one in the attr types, choices and default value.""" for attr_type in attr.types: if attr_type.qname == search: attr_type.qname = replace if isinstance(attr.default, str) and attr.default.startswith("@enum@"): attr.default = attr.default.replace(search, replace) for choice in attr.choices: self.rename_attr_dependencies(choice, search, replace)
def process(cls, target: Class): """Add an xs:anyType attribute to the given class if it supports mixed content and doesn't have a wildcard attribute yet.""" if not target.mixed or target.has_wild_attr: return attr = Attr( name="content", local_name="content", index=0, types=[AttrType(name=DataType.ANY_TYPE.code, native=True)], tag=Tag.ANY, namespace=NamespaceType.ANY.value, ) target.attrs.insert(0, attr)
def create_substitution(cls, source: Class, qname: QName) -> Attr: """Create an attribute with type that refers to the given source class and namespaced qualified name.""" prefix = None if qname.namespace != source.source_namespace: prefix = source.source_prefix reference = f"{prefix}:{source.name}" if prefix else source.name return Attr( name=source.name, local_name=source.name, index=0, default=None, types=[AttrType(name=reference)], tag=source.type.__name__, namespace=source.namespace, )
def build_attr( cls, name: str, qname: str, native: bool = False, forward: bool = False, namespace: Optional[str] = None, default: Optional[str] = None, ) -> Attr: """Builder method for attributes.""" return Attr( tag=Tag.ELEMENT, name=name, namespace=namespace, default=default, types=[AttrType(qname=qname, forward=forward, native=native)], )
def build_attribute( cls, target: Class, qname: str, attr_type: AttrType, target_namespace: Optional[str] = None, tag: str = Tag.ELEMENT, sequential: bool = False, ): namespace, name = split_qname(qname) namespace = cls.select_namespace(namespace, target_namespace, tag) index = len(target.attrs) attr = Attr(index=index, name=name, tag=tag, namespace=namespace) attr.types.append(attr_type) attr.restrictions.sequential = sequential or None cls.add_attribute(target, attr)
def clone_attribute(cls, attr: Attr, restrictions: Restrictions, prefix: Optional[str] = None) -> Attr: """ Clone the given attribute and merge its restrictions with the given instance. Prepend the given namespace prefix to the attribute name if available. """ clone = attr.clone() clone.restrictions.merge(restrictions) if prefix: for attr_type in clone.types: if not attr_type.native and attr_type.name.find(":") == -1: attr_type.name = f"{prefix}:{attr_type.name}" return clone
def build_attr( cls, name: str, qname: str, native: bool = False, forward: bool = False, namespace: Optional[str] = None, default: Optional[str] = None, ) -> Attr: """Builder method for attributes.""" occurs = 1 if default is not None else None return Attr( tag=Tag.ELEMENT, name=name, namespace=namespace, default=default, types=[AttrType(qname=qname, forward=forward, native=native)], restrictions=Restrictions(min_occurs=occurs, max_occurs=occurs), )
def build_attr_choice(cls, attr: Attr) -> Attr: """ Converts the given attr to a choice. The most important part is the reset of certain restrictions that don't make sense as choice metadata like occurrences. """ restrictions = attr.restrictions.clone() restrictions.min_occurs = None restrictions.max_occurs = None restrictions.sequential = None return Attr( name=attr.local_name, namespace=attr.namespace, default=attr.default, types=attr.types, tag=attr.tag, help=attr.help, restrictions=restrictions, )
def group_fields(self, target: Class, attrs: List[Attr]): """Group attributes into a new compound field.""" pos = target.attrs.index(attrs[0]) choice = attrs[0].restrictions.choice sum_occurs = choice and choice.startswith("effective_") names = [] choices = [] min_occurs = [] max_occurs = [] for attr in attrs: target.attrs.remove(attr) names.append(attr.local_name) min_occurs.append(attr.restrictions.min_occurs or 0) max_occurs.append(attr.restrictions.max_occurs or 0) choices.append(self.build_attr_choice(attr)) if len(names) > 3 or len(names) != len(set(names)): name = "choice" else: name = "_Or_".join(names) target.attrs.insert( pos, Attr( name=name, index=0, types=[AttrType(qname=str(DataType.ANY_TYPE), native=True)], tag=Tag.CHOICE, restrictions=Restrictions( min_occurs=sum(min_occurs) if sum_occurs else min(min_occurs), max_occurs=sum(max_occurs) if sum_occurs else max(max_occurs), ), choices=choices, ), )
def clone_attribute(cls, attr: Attr, restrictions: Restrictions) -> Attr: """Clone the given attribute and merge its restrictions with the given instance.""" clone = attr.clone() clone.restrictions.merge(restrictions) return clone