import sys
from dataclasses import dataclass
from dataclasses import field
from pathlib import Path
from typing import Any
from typing import Callable
from typing import List
from typing import Optional
from typing import Type
from typing import TypeVar
from lxml import etree
from xsdata.formats.dataclass.parsers.xml import QueueItem
from xsdata.formats.dataclass.parsers.xml import XmlParser
from xsdata.models import elements as xsd
from xsdata.models.enums import FormType
from xsdata.models.enums import Mode
from xsdata.models.enums import Namespace
from xsdata.utils import text
T = TypeVar("T")
[docs]@dataclass
class SchemaParser(XmlParser):
"""
A simple parser to convert an xsd schema to an easy to handle data
structure based on dataclasses.
The parser is a dummy as possible but it will try to normalize
certain things like apply parent properties to children.
"""
element_form: Optional[FormType] = field(init=False, default=None)
attribute_form: Optional[FormType] = field(init=False, default=None)
target_namespace: Optional[str] = field(default=None)
default_attributes: Optional[str] = field(default=None)
default_open_content: Optional[xsd.DefaultOpenContent] = field(default=None)
schema_location: Optional[Path] = field(default=None)
name_generator: Callable = field(default=text.camel_case)
[docs] def from_xsd_string(self, source: str) -> xsd.Schema:
return super().from_string(source, xsd.Schema)
[docs] def from_xsd_path(self, path: Path) -> xsd.Schema:
self.schema_location = path
return super().from_path(path, xsd.Schema)
[docs] def dequeue_node(self, element: etree.Element) -> Optional[T]:
"""Override parent method to skip empty elements and to set the object
index."""
item = self.queue[-1]
obj = super(SchemaParser, self).dequeue_node(element)
# Make sure queue item is not part of mixed content
if obj is None or item is None:
return None
obj.index = item.index
self.set_namespace_map(element, obj)
return obj
[docs] def start_schema(self, element: etree.Element, item: QueueItem):
"""Collect the schema's default form for attributes and elements for
later usage."""
self.element_form = element.attrib.get("elementFormDefault", None)
self.attribute_form = element.attrib.get("attributeFormDefault", None)
self.default_attributes = element.attrib.get("defaultAttributes", None)
[docs] def set_schema_namespaces(self, obj: xsd.Schema, element: etree.Element):
"""Set the given schema's target namespace and add the default
namespaces if the are missing xsi, xlink, xml, xs."""
obj.target_namespace = obj.target_namespace or self.target_namespace
self.set_namespace_map(element, obj)
[docs] @staticmethod
def set_namespace_map(element, obj):
obj.nsmap = element.nsmap
namespaces = obj.nsmap.values()
for namespace in Namespace:
if namespace.uri not in namespaces:
obj.nsmap[namespace.prefix] = namespace.uri
[docs] @staticmethod
def add_default_imports(obj: xsd.Schema):
"""Add missing imports to the standard schemas if the namespace is
declared and."""
imp_namespaces = [imp.namespace for imp in obj.imports]
xsi_ns = Namespace.XSI.value
if xsi_ns in obj.nsmap.values() and xsi_ns not in imp_namespaces:
obj.imports.insert(0, xsd.Import.create(namespace=xsi_ns))
[docs] def resolve_schemas_locations(self, obj: xsd.Schema):
"""Resolve the locations of the schema overrides, redefines, includes
and imports relatively to the schema location."""
if not self.schema_location:
return
obj.location = self.schema_location
for over in obj.overrides:
over.location = self.resolve_path(over.schema_location)
for red in obj.redefines:
red.location = self.resolve_path(red.schema_location)
for inc in obj.includes:
inc.location = self.resolve_path(inc.schema_location)
for imp in obj.imports:
imp.location = self.resolve_local_path(imp.schema_location, imp.namespace)
[docs] def resolve_path(self, location: Optional[str]) -> Optional[Path]:
"""Resolve the given location string relatively the schema location
path."""
path = None
if self.schema_location and location:
path = self.schema_location.parent.joinpath(location).resolve()
return path if path and path.exists() else None
[docs] def resolve_local_path(self, location, namespace):
"""Resolve the given namespace to one of the local standard schemas or
fallback to the external file path."""
if not location or location.startswith("http"):
ns = Namespace.get_enum(namespace)
return ns.location if ns else None
else:
return self.resolve_path(location)
[docs] def end_attribute(self, obj: T, element: etree.Element):
"""Assign the schema's default form for attributes if the given
attribute form is None."""
if isinstance(obj, xsd.Attribute) and obj.form is None and self.attribute_form:
obj.form = FormType(self.attribute_form)
[docs] def end_complex_type(self, obj: T, element: etree.Element):
"""Prepend an attribute group reference when default attributes
apply."""
if not isinstance(obj, xsd.ComplexType):
return
if obj.default_attributes_apply and self.default_attributes:
attribute_group = xsd.AttributeGroup.create(ref=self.default_attributes)
obj.attribute_groups.insert(0, attribute_group)
if not obj.open_content:
obj.open_content = self.default_open_content
[docs] def end_default_open_content(self, obj: T, element: etree.Element):
if isinstance(obj, xsd.DefaultOpenContent):
if obj.any and obj.mode == Mode.SUFFIX:
obj.any.index = sys.maxsize
self.default_open_content = obj
[docs] def end_element(self, obj: T, element: etree.Element):
"""Assign the schema's default form for elements if the given element
form is None."""
if isinstance(obj, xsd.Element) and obj.form is None and self.element_form:
obj.form = FormType(self.element_form)
[docs] def end_extension(self, obj: T, element: etree.Element):
if isinstance(obj, xsd.Extension) and not obj.open_content:
obj.open_content = self.default_open_content
[docs] @classmethod
def end_open_content(cls, obj: T, element: etree.Element):
if isinstance(obj, xsd.OpenContent):
if obj.any and obj.mode == Mode.SUFFIX:
obj.any.index = sys.maxsize
[docs] def end_restriction(self, obj: T, element: etree.Element):
if isinstance(obj, xsd.Restriction) and not obj.open_content:
obj.open_content = self.default_open_content
[docs] def end_schema(self, obj: T, element: etree.Element):
"""Normalize various properties for the schema and it's children."""
if isinstance(obj, xsd.Schema):
self.set_schema_forms(obj)
self.set_schema_namespaces(obj, element)
self.add_default_imports(obj)
self.resolve_schemas_locations(obj)
[docs] @classmethod
def parse_value(cls, types: List[Type], value: Any, default: Any = None) -> Any:
if int in types and value == "unbounded":
return sys.maxsize
try:
return super().parse_value(types, value, default)
except ValueError:
return str(value)