# -*- coding: utf-8 -*-
# serialization.py
"""
Utility function to serialize data and meta classes for export, to a HDF5 file, for example.
"""
__author__ = "Ingo Breßler"
__contact__ = "dev@ingobressler.net"
__license__ = "GPLv3+"
__date__ = "2023/02/07"
__status__ = "beta"
import xml.etree.ElementTree
from pathlib import Path, PurePosixPath
import graphviz
[docs]
def type2str(obj):
return ".".join((type(obj).__module__, type(obj).__name__))
[docs]
def dumpKV(obj: object, path: PurePosixPath = None, lvl: int = 0, dbg: bool = False):
"""Serializes the given hierarchical DACHS structure as key-value pairs (a dict).
:param objlst: A hierarchical instance for traversal.
:param path: Optional, a word to prepend to all generated keys, a top-level name.
It is replaced by the *ID* attribute if available.
:param lvl: The current level of invocation, tracks the recursion depth for debugging.
:param dbg: Enable debug output.
"""
indent = "".join([" " for _ in range(lvl)])
if dbg:
from pprint import pformat
print(indent, f"{obj=}")
if not path: # may be undefined for the top-level object
path = getattr(obj, "ID", None)
path = PurePosixPath(path) if path else PurePosixPath("")
if dbg:
print(indent, f"path from id: '{path}', {type(obj)}")
# find any children to traverse
children = [(storeKey, getattr(obj, storeKey)) for storeKey in getattr(obj, "_storeKeys", [])]
if not len(children):
if type(obj) in (list, tuple):
children = list(enumerate(obj))
if type(obj) in (dict,):
children = list(obj.items())
# translate children path names from type names to their stored ID
children = [(getattr(child, "ID", num), child) for num, child in children]
if dbg:
print(indent, f"{children=}")
if not len(children): # no children found
# omit this object if empty? TODO
return {path: obj}
# first store some meta info for later graphKV stage extended info
pathlst = {path / "type": type2str(obj)}
for name, child in children:
if dbg:
print(indent, f"{lvl}>", name)
subpath = path / str(name)
items = dumpKV(child, subpath, lvl + 1, dbg=dbg)
if dbg:
indentCount = (lvl + 1) * 2 + 1
print(indent, f"{lvl}<{{" + pformat(items, indent=indentCount, width=120)[1:].lstrip())
pathlst.update(items)
return pathlst
[docs]
def graphKV(paths):
docsPath = Path("dist/docs/reference/autosummary")
graphName = list(paths.keys())[0].parts[0]
# pprint.pprint(paths, width=120)
def nodeFromParts(parts, i, paths):
node = PurePosixPath(*parts[: i + 1])
try:
return node, paths[node / "type"]
except KeyError:
return node, type2str(paths[node])
nodes = dict(
nodeFromParts(path.parts, i, paths)
for path in paths
for i in range(len(path.parts))
if path.parts[-1] != "type" # filter type info, evaluated for node type info
)
edges = {
(PurePosixPath(*path.parts[: i + 1]), PurePosixPath(*path.parts[: i + 2]))
for path in paths
for i in range(len(path.parts) - 1)
if path.parts[-1] != "type" # filter type info, evaluated for node type info
}
graph = graphviz.Digraph(graphName, format="svg", graph_attr=dict(rankdir="LR"))
def sanitize(name):
return str(name).replace(":", ".")
for nodepath, nodetype in nodes.items():
lbl = f"{nodepath.name}({nodetype.split('.')[-1]})"
url, color = "", ""
if nodetype.startswith("dachs."):
url = docsPath / (nodetype + ".html")
color = "blue"
graph.node(sanitize(nodepath), label=lbl, URL=str(url), fontcolor=color)
for tail, head in edges:
graph.edge(sanitize(tail), sanitize(head))
outfn = graph.render(graph.name, cleanup=True)
fixSVG(outfn)
[docs]
def fixSVG(fn):
"""Apply some modifications to SVG generated by Graphviz.
Convert to PNG with::
convert -background transparent AutoMOF5.svg AutoMOF5.png
"""
fn = Path(fn)
xml.etree.ElementTree.register_namespace("", "http://www.w3.org/2000/svg")
tree = xml.etree.ElementTree.parse(fn)
# remove background color from first polygon element
elem = [elem for elem in tree.iter() if elem.tag.split("}")[-1] == "polygon"][0]
elem.set("fill", "none")
tree.write(fn)