import logging import re import typing from lxml import etree from zeep.exceptions import XMLParseError from zeep.loader import absolute_location, load_external, normalize_location from zeep.utils import as_qname, qname_attr from zeep.xsd import elements as xsd_elements from zeep.xsd import types as xsd_types from zeep.xsd.const import AUTO_IMPORT_NAMESPACES, xsd_ns from zeep.xsd.types.unresolved import UnresolvedCustomType, UnresolvedType logger = logging.getLogger(__name__) class tags: schema = xsd_ns("schema") import_ = xsd_ns("import") include = xsd_ns("include") annotation = xsd_ns("annotation") element = xsd_ns("element") simpleType = xsd_ns("simpleType") complexType = xsd_ns("complexType") simpleContent = xsd_ns("simpleContent") complexContent = xsd_ns("complexContent") sequence = xsd_ns("sequence") group = xsd_ns("group") choice = xsd_ns("choice") all = xsd_ns("all") list = xsd_ns("list") union = xsd_ns("union") attribute = xsd_ns("attribute") any = xsd_ns("any") anyAttribute = xsd_ns("anyAttribute") attributeGroup = xsd_ns("attributeGroup") restriction = xsd_ns("restriction") extension = xsd_ns("extension") notation = xsd_ns("notation") class SchemaVisitor: """Visitor which processes XSD files and registers global elements and types in the given schema. Notes: TODO: include and import statements can reference other nodes. We need to load these first. Always global. :param schema: :type schema: zeep.xsd.schema.Schema :param document: :type document: zeep.xsd.schema.SchemaDocument """ def __init__(self, schema, document): self.document = document self.schema = schema self._includes = set() def register_element(self, qname: etree.QName, instance: xsd_elements.Element): self.document.register_element(qname, instance) def register_attribute( self, name: etree.QName, instance: xsd_elements.Attribute ) -> None: self.document.register_attribute(name, instance) def register_type(self, qname: etree.QName, instance) -> None: self.document.register_type(qname, instance) def register_group(self, qname: etree.QName, instance: xsd_elements.Group): self.document.register_group(qname, instance) def register_attribute_group( self, qname: etree.QName, instance: xsd_elements.AttributeGroup ) -> None: self.document.register_attribute_group(qname, instance) def register_import(self, namespace, document): self.document.register_import(namespace, document) def process(self, node, parent): visit_func = self.visitors.get(node.tag) if not visit_func: raise ValueError("No visitor defined for %r" % node.tag) result = visit_func(self, node, parent) return result def process_ref_attribute(self, node, array_type=None): ref = qname_attr(node, "ref") if ref: ref = self._create_qname(ref) # Some wsdl's reference to xs:schema, we ignore that for now. It # might be better in the future to process the actual schema file # so that it is handled correctly if ref.namespace == "http://www.w3.org/2001/XMLSchema": return return xsd_elements.RefAttribute( node.tag, ref, self.schema, array_type=array_type ) def process_reference(self, node, **kwargs): ref = qname_attr(node, "ref") if not ref: return ref = self._create_qname(ref) if node.tag == tags.element: cls = xsd_elements.RefElement elif node.tag == tags.attribute: cls = xsd_elements.RefAttribute elif node.tag == tags.group: cls = xsd_elements.RefGroup elif node.tag == tags.attributeGroup: cls = xsd_elements.RefAttributeGroup return cls(node.tag, ref, self.schema, **kwargs) def visit_schema(self, node): """Visit the xsd:schema element and process all the child elements Definition:: Content: ( (include | import | redefine | annotation)*, (((simpleType | complexType | group | attributeGroup) | element | attribute | notation), annotation*)*) :param node: The XML node :type node: lxml.etree._Element """ assert node is not None # A schema should always have a targetNamespace attribute, otherwise # it is called a chameleon schema. In that case the schema will inherit # the namespace of the enclosing schema/node. tns = node.get("targetNamespace") if tns: self.document._target_namespace = tns self.document._element_form = node.get("elementFormDefault", "unqualified") self.document._attribute_form = node.get("attributeFormDefault", "unqualified") for child in node: self.process(child, parent=node) def visit_import(self, node, parent): """ Definition:: Content: (annotation?) :param node: The XML node :type node: lxml.etree._Element :param parent: The parent XML node :type parent: lxml.etree._Element """ schema_node = None namespace = node.get("namespace") location = node.get("schemaLocation") if location: location = normalize_location( self.schema.settings, location, self.document._base_url ) if not namespace and not self.document._target_namespace: raise XMLParseError( "The attribute 'namespace' must be existent if the " "importing schema has no target namespace.", filename=self.document.location, sourceline=node.sourceline, ) # We found an empty statement, this needs to trigger 4.1.2 # from https://www.w3.org/TR/2012/REC-xmlschema11-1-20120405/#src-resolve # for QName resolving. # In essence this means we will resolve QNames without a namespace to no # namespace instead of the target namespace. # The following code snippet works because imports have to occur before we # visit elements. if not namespace and not location: self.document._has_empty_import = True # Check if the schema is already imported before based on the # namespace. Schema's without namespace are registered as 'None' document = self.schema.documents.get_by_namespace_and_location( namespace, location ) if document: logger.debug("Returning existing schema: %r", location) self.register_import(namespace, document) return document # Hardcode the mapping between the xml namespace and the xsd for now. # This seems to fix issues with exchange wsdl's, see #220 if not location and namespace == "http://www.w3.org/XML/1998/namespace": location = "https://www.w3.org/2001/xml.xsd" # Silently ignore import statements which we can't resolve via the # namespace and doesn't have a schemaLocation attribute. if not location: logger.debug( "Ignoring import statement for namespace %r " + "(missing schemaLocation)", namespace, ) return # Load the XML schema_node = self._retrieve_data(location, base_url=self.document._location) # Check if the xsd:import namespace matches the targetNamespace. If # the xsd:import statement didn't specify a namespace then make sure # that the targetNamespace wasn't declared by another schema yet. schema_tns = schema_node.get("targetNamespace") if namespace and schema_tns and namespace != schema_tns: raise XMLParseError( ( "The namespace defined on the xsd:import doesn't match the " "imported targetNamespace located at %r " ) % (location), filename=self.document._location, sourceline=node.sourceline, ) # If the imported schema doesn't define a target namespace and the # node doesn't specify it either then inherit the existing target # namespace. elif not schema_tns and not namespace: namespace = self.document._target_namespace schema = self.schema.create_new_document( schema_node, location, target_namespace=namespace ) self.register_import(namespace, schema) return schema def visit_include(self, node, parent): """ Definition:: Content: (annotation?) :param node: The XML node :type node: lxml.etree._Element :param parent: The parent XML node :type parent: lxml.etree._Element """ if not node.get("schemaLocation"): raise NotImplementedError("schemaLocation is required") location = node.get("schemaLocation") if location in self._includes: return schema_node = self._retrieve_data(location, base_url=self.document._base_url) self._includes.add(location) # When the included document has no default namespace defined but the # parent document does have this then we should (atleast for #360) # transfer the default namespace to the included schema. We can't # update the nsmap of elements in lxml so we create a new schema with # the correct nsmap and move all the content there. # Included schemas must have targetNamespace equal to parent schema (the including) or None. # If included schema doesn't have default ns, then it should be set to parent's targetNs. # See Chameleon Inclusion https://www.w3.org/TR/xmlschema11-1/#chameleon-xslt if not schema_node.nsmap.get(None) and ( node.nsmap.get(None) or parent.attrib.get("targetNamespace") ): nsmap = {None: node.nsmap.get(None) or parent.attrib["targetNamespace"]} nsmap.update(schema_node.nsmap) new = etree.Element(schema_node.tag, nsmap=nsmap) for child in schema_node: new.append(child) for key, value in schema_node.attrib.items(): new.set(key, value) if not new.attrib.get("targetNamespace"): new.attrib["targetNamespace"] = parent.attrib["targetNamespace"] schema_node = new # Use the element/attribute form defaults from the schema while # processing the nodes. element_form_default = self.document._element_form attribute_form_default = self.document._attribute_form base_url = self.document._base_url self.document._element_form = schema_node.get( "elementFormDefault", "unqualified" ) self.document._attribute_form = schema_node.get( "attributeFormDefault", "unqualified" ) self.document._base_url = absolute_location(location, self.document._base_url) # Iterate directly over the children. for child in schema_node: self.process(child, parent=schema_node) self.document._element_form = element_form_default self.document._attribute_form = attribute_form_default self.document._base_url = base_url def visit_element(self, node, parent): """ Definition:: Content: (annotation?, ( (simpleType | complexType)?, (unique | key | keyref)*)) :param node: The XML node :type node: lxml.etree._Element :param parent: The parent XML node :type parent: lxml.etree._Element """ is_global = parent.tag == tags.schema # minOccurs / maxOccurs are not allowed on global elements if not is_global: min_occurs, max_occurs = _process_occurs_attrs(node) else: max_occurs = 1 min_occurs = 1 # If the element has a ref attribute then all other attributes cannot # be present. Short circuit that here. # Ref is prohibited on global elements (parent = schema) if not is_global: # Naive workaround to mark fields which are part of a choice element # as optional if parent.tag == tags.choice: min_occurs = 0 result = self.process_reference( node, min_occurs=min_occurs, max_occurs=max_occurs ) if result: return result element_form = node.get("form", self.document._element_form) if element_form == "qualified" or is_global: qname = qname_attr(node, "name", self.document._target_namespace) else: qname = etree.QName(node.get("name").strip()) children = list(node) xsd_type = None if children: value = None for child in children: if child.tag == tags.annotation: continue elif child.tag in (tags.simpleType, tags.complexType): assert not value xsd_type = self.process(child, node) if not xsd_type: node_type = qname_attr(node, "type") if node_type: xsd_type = self._get_type(node_type.text) else: xsd_type = xsd_types.AnyType() nillable = node.get("nillable") == "true" default = node.get("default") element = xsd_elements.Element( name=qname, type_=xsd_type, min_occurs=min_occurs, max_occurs=max_occurs, nillable=nillable, default=default, is_global=is_global, ) # Only register global elements if is_global: self.register_element(qname, element) return element def visit_attribute( self, node: etree._Element, parent: etree._Element ) -> typing.Union[xsd_elements.Attribute, xsd_elements.RefAttribute]: """Declares an attribute. Definition:: Content: (annotation?, (simpleType?)) :param node: The XML node :type node: lxml.etree._Element :param parent: The parent XML node :type parent: lxml.etree._Element """ is_global = parent.tag == tags.schema # Check of wsdl:arayType array_type = node.get("{http://schemas.xmlsoap.org/wsdl/}arrayType") if array_type: match = re.match(r"([^\[]+)", array_type) if match: array_type = match.groups()[0] qname = as_qname(array_type, node.nsmap) array_type = UnresolvedType(qname, self.schema) # If the elment has a ref attribute then all other attributes cannot # be present. Short circuit that here. # Ref is prohibited on global elements (parent = schema) if not is_global: result = self.process_ref_attribute(node, array_type=array_type) if result: return result attribute_form = node.get("form", self.document._attribute_form) if attribute_form == "qualified" or is_global: name = qname_attr(node, "name", self.document._target_namespace) else: name = etree.QName(node.get("name")) annotation, items = self._pop_annotation(list(node)) if items: xsd_type = self.visit_simple_type(items[0], node) else: node_type = qname_attr(node, "type") if node_type: xsd_type = self._get_type(node_type) else: xsd_type = xsd_types.AnyType() # TODO: We ignore 'prohobited' for now required = node.get("use") == "required" default = node.get("default") attr = xsd_elements.Attribute( name, type_=xsd_type, default=default, required=required ) # Only register global elements if is_global: assert name is not None self.register_attribute(name, attr) return attr def visit_simple_type(self, node, parent): """ Definition:: Content: (annotation?, (restriction | list | union)) :param node: The XML node :type node: lxml.etree._Element :param parent: The parent XML node :type parent: lxml.etree._Element """ if parent.tag == tags.schema: name = node.get("name") is_global = True else: name = parent.get("name", "Anonymous") is_global = False base_type = "{http://www.w3.org/2001/XMLSchema}string" qname = as_qname(name, node.nsmap, self.document._target_namespace) annotation, items = self._pop_annotation(list(node)) child = items[0] if child.tag == tags.restriction: base_type = self.visit_restriction_simple_type(child, node) xsd_type = UnresolvedCustomType(qname, base_type, self.schema) elif child.tag == tags.list: xsd_type = self.visit_list(child, node) elif child.tag == tags.union: xsd_type = self.visit_union(child, node) else: raise AssertionError("Unexpected child: %r" % child.tag) assert xsd_type is not None if is_global: self.register_type(qname, xsd_type) return xsd_type def visit_complex_type(self, node, parent): """ Definition:: Content: (annotation?, (simpleContent | complexContent | ((group | all | choice | sequence)?, ((attribute | attributeGroup)*, anyAttribute?)))) :param node: The XML node :type node: lxml.etree._Element :param parent: The parent XML node :type parent: lxml.etree._Element """ children = [] base_type = "{http://www.w3.org/2001/XMLSchema}anyType" # If the complexType's parent is an element then this type is # anonymous and should have no name defined. Otherwise it's global if parent.tag == tags.schema: name = node.get("name") is_global = True else: name = parent.get("name") is_global = False qname = as_qname(name, node.nsmap, self.document._target_namespace) cls_attributes = {"__module__": "zeep.xsd.dynamic_types", "_xsd_name": qname} xsd_cls = type(name, (xsd_types.ComplexType,), cls_attributes) xsd_type = None # Process content annotation, children = self._pop_annotation(list(node)) first_tag = children[0].tag if children else None if first_tag == tags.simpleContent: base_type, attributes = self.visit_simple_content(children[0], node) xsd_type = xsd_cls( attributes=attributes, extension=base_type, qname=qname, is_global=is_global, ) elif first_tag == tags.complexContent: kwargs = self.visit_complex_content(children[0], node) xsd_type = xsd_cls(qname=qname, is_global=is_global, **kwargs) elif first_tag: element = None if first_tag in (tags.group, tags.all, tags.choice, tags.sequence): child = children.pop(0) element = self.process(child, node) attributes = self._process_attributes(node, children) xsd_type = xsd_cls( element=element, attributes=attributes, qname=qname, is_global=is_global ) else: xsd_type = xsd_cls(qname=qname, is_global=is_global) if is_global: self.register_type(qname, xsd_type) return xsd_type def visit_complex_content(self, node, parent): """The complexContent element defines extensions or restrictions on a complex type that contains mixed content or elements only. Definition:: Content: (annotation?, (restriction | extension)) :param node: The XML node :type node: lxml.etree._Element :param parent: The parent XML node :type parent: lxml.etree._Element """ children = list(node) child = children[-1] if child.tag == tags.restriction: base, element, attributes = self.visit_restriction_complex_content( child, node ) return {"attributes": attributes, "element": element, "restriction": base} elif child.tag == tags.extension: base, element, attributes = self.visit_extension_complex_content( child, node ) return {"attributes": attributes, "element": element, "extension": base} def visit_simple_content(self, node, parent): """Contains extensions or restrictions on a complexType element with character data or a simpleType element as content and contains no elements. Definition:: Content: (annotation?, (restriction | extension)) :param node: The XML node :type node: lxml.etree._Element :param parent: The parent XML node :type parent: lxml.etree._Element """ children = list(node) child = children[-1] if child.tag == tags.restriction: return self.visit_restriction_simple_content(child, node) elif child.tag == tags.extension: return self.visit_extension_simple_content(child, node) raise AssertionError("Expected restriction or extension") def visit_restriction_simple_type(self, node, parent): """ Definition:: Content: (annotation?, (simpleType?, ( minExclusive | minInclusive | maxExclusive | maxInclusive | totalDigits |fractionDigits | length | minLength | maxLength | enumeration | whiteSpace | pattern)*)) :param node: The XML node :type node: lxml.etree._Element :param parent: The parent XML node :type parent: lxml.etree._Element """ base_name = qname_attr(node, "base") if base_name: return self._get_type(base_name) annotation, children = self._pop_annotation(list(node)) if children[0].tag == tags.simpleType: return self.visit_simple_type(children[0], node) def visit_restriction_simple_content(self, node, parent): """ Definition:: Content: (annotation?, (simpleType?, ( minExclusive | minInclusive | maxExclusive | maxInclusive | totalDigits |fractionDigits | length | minLength | maxLength | enumeration | whiteSpace | pattern)* )?, ((attribute | attributeGroup)*, anyAttribute?)) :param node: The XML node :type node: lxml.etree._Element :param parent: The parent XML node :type parent: lxml.etree._Element """ base_name = qname_attr(node, "base") base_type = self._get_type(base_name) return base_type, [] def visit_restriction_complex_content(self, node, parent): """ Definition:: Content: (annotation?, (group | all | choice | sequence)?, ((attribute | attributeGroup)*, anyAttribute?)) :param node: The XML node :type node: lxml.etree._Element :param parent: The parent XML node :type parent: lxml.etree._Element """ base_name = qname_attr(node, "base") base_type = self._get_type(base_name) annotation, children = self._pop_annotation(list(node)) element = None attributes = [] if children: child = children[0] if child.tag in (tags.group, tags.all, tags.choice, tags.sequence): children.pop(0) element = self.process(child, node) attributes = self._process_attributes(node, children) return base_type, element, attributes def visit_extension_complex_content(self, node, parent): """ Definition:: Content: (annotation?, ( (group | all | choice | sequence)?, ((attribute | attributeGroup)*, anyAttribute?))) :param node: The XML node :type node: lxml.etree._Element :param parent: The parent XML node :type parent: lxml.etree._Element """ base_name = qname_attr(node, "base") base_type = self._get_type(base_name) annotation, children = self._pop_annotation(list(node)) element = None attributes = [] if children: child = children[0] if child.tag in (tags.group, tags.all, tags.choice, tags.sequence): children.pop(0) element = self.process(child, node) attributes = self._process_attributes(node, children) return base_type, element, attributes def visit_extension_simple_content(self, node, parent): """ Definition:: Content: (annotation?, ((attribute | attributeGroup)*, anyAttribute?)) """ base_name = qname_attr(node, "base") base_type = self._get_type(base_name) annotation, children = self._pop_annotation(list(node)) attributes = self._process_attributes(node, children) return base_type, attributes def visit_annotation(self, node, parent): """Defines an annotation. Definition:: Content: (appinfo | documentation)* :param node: The XML node :type node: lxml.etree._Element :param parent: The parent XML node :type parent: lxml.etree._Element """ return def visit_any(self, node, parent): """ Definition:: Content: (annotation?, (element | group | choice | sequence | any)*) :param node: The XML node :type node: lxml.etree._Element :param parent: The parent XML node :type parent: lxml.etree._Element """ sub_types = [ tags.annotation, tags.any, tags.choice, tags.element, tags.group, tags.sequence, ] min_occurs, max_occurs = _process_occurs_attrs(node) result = xsd_elements.Sequence(min_occurs=min_occurs, max_occurs=max_occurs) annotation, children = self._pop_annotation(list(node)) for child in children: if child.tag not in sub_types: raise self._create_error( "Unexpected element %s in xsd:sequence" % child.tag, child ) item = self.process(child, node) assert item is not None result.append(item) assert None not in result return result def visit_all(self, node, parent): """Allows the elements in the group to appear (or not appear) in any order in the containing element. Definition:: Content: (annotation?, element*) :param node: The XML node :type node: lxml.etree._Element :param parent: The parent XML node :type parent: lxml.etree._Element """ sub_types = [tags.annotation, tags.element] result = xsd_elements.All() annotation, children = self._pop_annotation(list(node)) for child in children: assert child.tag in sub_types, child item = self.process(child, node) result.append(item) assert None not in result return result def visit_group(self, node, parent): """Groups a set of element declarations so that they can be incorporated as a group into complex type definitions. Definition:: Content: (annotation?, (all | choice | sequence)) :param node: The XML node :type node: lxml.etree._Element :param parent: The parent XML node :type parent: lxml.etree._Element """ min_occurs, max_occurs = _process_occurs_attrs(node) result = self.process_reference( node, min_occurs=min_occurs, max_occurs=max_occurs ) if result: return result qname = qname_attr(node, "name", self.document._target_namespace) # There should be only max nodes, first node (annotation) is irrelevant annotation, children = self._pop_annotation(list(node)) child = children[0] item = self.process(child, parent) elm = xsd_elements.Group(name=qname, child=item) if parent.tag == tags.schema: self.register_group(qname, elm) return elm def visit_list(self, node, parent): """ Definition:: Content: (annotation?, (simpleType?)) The use of the simpleType element child and the itemType attribute is mutually exclusive. :param node: The XML node :type node: lxml.etree._Element :param parent: The parent XML node :type parent: lxml.etree._Element """ item_type = qname_attr(node, "itemType") if item_type: sub_type = self._get_type(item_type.text) else: subnodes = list(node) child = subnodes[-1] # skip annotation sub_type = self.visit_simple_type(child, node) return xsd_types.ListType(sub_type) def visit_choice(self, node, parent): """ Definition:: Content: (annotation?, (element | group | choice | sequence | any)*) """ min_occurs, max_occurs = _process_occurs_attrs(node) annotation, children = self._pop_annotation(list(node)) choices = [] for child in children: elm = self.process(child, node) choices.append(elm) return xsd_elements.Choice( choices, min_occurs=min_occurs, max_occurs=max_occurs ) def visit_union(self, node, parent): """Defines a collection of multiple simpleType definitions. Definition:: Content: (annotation?, (simpleType*)) :param node: The XML node :type node: lxml.etree._Element :param parent: The parent XML node :type parent: lxml.etree._Element """ # TODO members = node.get("memberTypes") types = [] if members: for member in members.split(): qname = as_qname(member, node.nsmap) xsd_type = self._get_type(qname) types.append(xsd_type) else: annotation, types = self._pop_annotation(list(node)) types = [self.visit_simple_type(t, node) for t in types] return xsd_types.UnionType(types) def visit_unique(self, node, parent): """Specifies that an attribute or element value (or a combination of attribute or element values) must be unique within the specified scope. The value must be unique or nil. Definition:: Content: (annotation?, (selector, field+)) :param node: The XML node :type node: lxml.etree._Element :param parent: The parent XML node :type parent: lxml.etree._Element """ # TODO pass def visit_attribute_group(self, node, parent): """ Definition:: Content: (annotation?), ((attribute | attributeGroup)*, anyAttribute?)) :param node: The XML node :type node: lxml.etree._Element :param parent: The parent XML node :type parent: lxml.etree._Element """ ref = self.process_reference(node) if ref: return ref qname = qname_attr(node, "name", self.document._target_namespace) annotation, children = self._pop_annotation(list(node)) attributes = self._process_attributes(node, children) attribute_group = xsd_elements.AttributeGroup(qname, attributes) self.register_attribute_group(qname, attribute_group) def visit_any_attribute(self, node, parent): """ Definition:: Content: (annotation?) :param node: The XML node :type node: lxml.etree._Element :param parent: The parent XML node :type parent: lxml.etree._Element """ process_contents = node.get("processContents", "strict") return xsd_elements.AnyAttribute(process_contents=process_contents) def visit_notation(self, node, parent): """Contains the definition of a notation to describe the format of non-XML data within an XML document. An XML Schema notation declaration is a reconstruction of XML 1.0 NOTATION declarations. Definition:: Content: (annotation?) :param node: The XML node :type node: lxml.etree._Element :param parent: The parent XML node :type parent: lxml.etree._Element """ pass def _retrieve_data(self, url: typing.IO, base_url=None): return load_external( url, self.schema._transport, base_url, settings=self.schema.settings ) def _get_type(self, name): assert name is not None name = self._create_qname(name) return UnresolvedType(name, self.schema) def _create_qname(self, name): if not isinstance(name, etree.QName): name = etree.QName(name) # Handle reserved namespace if name.namespace == "xml": name = etree.QName("http://www.w3.org/XML/1998/namespace", name.localname) # Various xsd builders assume that some schema's are available by # default (actually this is mostly just the soap-enc ns). So live with # that fact and handle it by auto-importing the schema if it is # referenced. if name.namespace in AUTO_IMPORT_NAMESPACES and not self.document.is_imported( name.namespace ): logger.debug("Auto importing missing known schema: %s", name.namespace) import_node = etree.Element( tags.import_, namespace=name.namespace, schemaLocation=name.namespace ) self.visit_import(import_node, None) if ( not name.namespace and self.document._element_form == "qualified" and self.document._target_namespace and not self.document._has_empty_import ): name = etree.QName(self.document._target_namespace, name.localname) return name def _pop_annotation(self, items): if not len(items): return None, [] if items[0].tag == tags.annotation: annotation = self.visit_annotation(items[0], None) return annotation, items[1:] return None, items def _process_attributes(self, node, items): attributes = [] for child in items: if child.tag in (tags.attribute, tags.attributeGroup, tags.anyAttribute): attribute = self.process(child, node) attributes.append(attribute) else: raise self._create_error("Unexpected tag `%s`" % (child.tag), node) return attributes def _create_error(self, message, node): return XMLParseError( message, filename=self.document._location, sourceline=node.sourceline ) visitors = { tags.any: visit_any, tags.element: visit_element, tags.choice: visit_choice, tags.simpleType: visit_simple_type, tags.anyAttribute: visit_any_attribute, tags.complexType: visit_complex_type, tags.simpleContent: None, tags.complexContent: None, tags.sequence: visit_sequence, tags.all: visit_all, tags.group: visit_group, tags.attribute: visit_attribute, tags.import_: visit_import, tags.include: visit_include, tags.annotation: visit_annotation, tags.attributeGroup: visit_attribute_group, tags.notation: visit_notation, } def _process_occurs_attrs(node): """Process the min/max occurrence indicators""" max_occurs = node.get("maxOccurs", "1") min_occurs = int(node.get("minOccurs", "1")) if max_occurs == "unbounded": max_occurs = "unbounded" else: max_occurs = int(max_occurs) return min_occurs, max_occurs