137 lines
4.5 KiB
Python
137 lines
4.5 KiB
Python
"""
|
|
The @manual_xmlify decorator used to add the .xsd, .xml, .xml_value and .parse
|
|
methods to a class that already has .xsd_dependencies, .xsd_forward and
|
|
.get_xobject
|
|
"""
|
|
|
|
from typing import Any
|
|
from lxml.etree import _Element, Element, _ElementTree, ElementTree
|
|
from lxml.objectify import ObjectifiedElement
|
|
|
|
from xmlable._utils import typename, AnyType, ordered_iter
|
|
from xmlable._lxml_helpers import with_children, XMLSchema
|
|
from xmlable._errors import XError, XErrorCtx, ErrorTypes
|
|
|
|
|
|
def validate_manual_class(cls: AnyType):
|
|
attrs = {"get_xobject", "xsd_forward", "xsd_dependencies"}
|
|
for attr in attrs:
|
|
if not hasattr(cls, attr):
|
|
raise ErrorTypes.MissingAttribute(cls, attrs, attr)
|
|
|
|
|
|
def type_cycle(from_type: AnyType) -> list[AnyType]:
|
|
# INV: it is an xmlified type for a user define structure
|
|
cycle: list[AnyType] = []
|
|
|
|
def visit_dep(curr: AnyType) -> bool:
|
|
if curr == from_type or any(
|
|
visit_dep(dep) for dep in ordered_iter(curr.xsd_dependencies()) # type: ignore[attr-defined]
|
|
):
|
|
cycle.append(curr)
|
|
return True
|
|
else:
|
|
return False
|
|
|
|
assert visit_dep(from_type)
|
|
cycle.append(from_type)
|
|
return cycle
|
|
|
|
|
|
def manual_xmlify(cls: type) -> type:
|
|
"""
|
|
Generate the following methods:
|
|
```
|
|
def xsd(
|
|
id: str = cls_name,
|
|
namespaces: dict[str, str] = {},
|
|
imports: dict[str, str] = {},
|
|
) -> _ElementTree:
|
|
# ...
|
|
|
|
def xml(schema_name: str = cls_name) -> _ElementTree:
|
|
# ...
|
|
|
|
def xml_value(self, id: str = cls_name) -> _ElementTree:
|
|
# ...
|
|
|
|
def parse(obj: ObjectifiedElement) -> Any:
|
|
# ...
|
|
```
|
|
"""
|
|
try:
|
|
validate_manual_class(cls)
|
|
cls_name = typename(cls)
|
|
|
|
cls_xobject = cls.get_xobject() # type: ignore[attr-defined]
|
|
|
|
def xsd(
|
|
id: str = cls_name,
|
|
namespaces: dict[str, str] = {},
|
|
imports: dict[str, str] = {},
|
|
) -> _ElementTree:
|
|
# Get dependencies (user classes that need to be declared before)
|
|
visited: set[AnyType] = set()
|
|
dec_order: list[AnyType] = []
|
|
|
|
def toposort(
|
|
curr: AnyType, visited: set[AnyType], dec_order: list[AnyType]
|
|
):
|
|
if curr in visited:
|
|
raise ErrorTypes.DependencyCycle(type_cycle(curr))
|
|
visited.add(curr)
|
|
deps = curr.xsd_dependencies() # type: ignore[attr-defined]
|
|
for d in ordered_iter(deps):
|
|
if d not in visited:
|
|
toposort(d, visited, dec_order)
|
|
dec_order.append(curr)
|
|
|
|
toposort(cls, visited, dec_order)
|
|
|
|
# Create forward declarations, potentially adding to namespaces
|
|
decs: list[_Element] = [dec.xsd_forward(namespaces) for dec in dec_order] # type: ignore[attr-defined]
|
|
|
|
# generate main element (can add to namespaces)
|
|
main_element = cls_xobject.xsd_out(id, add_ns=namespaces)
|
|
|
|
return ElementTree(
|
|
with_children(
|
|
Element(
|
|
f"{XMLSchema}schema",
|
|
id=id,
|
|
elementFormDefault="qualified",
|
|
nsmap=namespaces,
|
|
),
|
|
[
|
|
Element(
|
|
f"{XMLSchema}import",
|
|
namespace=ns,
|
|
schemaLocation=sloc,
|
|
)
|
|
for ns, sloc in imports.items()
|
|
]
|
|
+ decs
|
|
+ [main_element],
|
|
)
|
|
)
|
|
|
|
def xml(schema_name: str = cls_name) -> _ElementTree:
|
|
return ElementTree(cls_xobject.xml_temp(schema_name))
|
|
|
|
def xml_value(self, id: str = cls_name) -> _ElementTree:
|
|
return ElementTree(cls_xobject.xml_out(id, self, XErrorCtx([id])))
|
|
|
|
def parse(obj: ObjectifiedElement) -> Any:
|
|
return cls_xobject.xml_in(obj, XErrorCtx([obj.tag]))
|
|
|
|
cls.xsd = xsd # type: ignore[attr-defined]
|
|
cls.xml = xml # type: ignore[attr-defined]
|
|
setattr(cls, "xml_value", xml_value) # needs to use self to get values
|
|
cls.parse = parse # type: ignore[attr-defined]
|
|
|
|
return cls
|
|
except XError as e:
|
|
# NOTE: Trick to remove dirty 'internal' traceback, and raise from
|
|
# xmlify (makes more sense to user than seeing internals)
|
|
e.__traceback__ = None
|
|
raise e
|