Skip to content
Snippets Groups Projects

Refactored the plantuml module

Merged Alexander Schlemmer requested to merge f-refactor-plantuml into dev

Files

+ 142
66
@@ -34,10 +34,15 @@ plantuml FILENAME.pu -> FILENAME.png
@@ -34,10 +34,15 @@ plantuml FILENAME.pu -> FILENAME.png
"""
"""
import os
import os
 
import shutil
import caosdb as db
import caosdb as db
from caosdb.common.datatype import is_reference, get_referenced_recordtype
from caosdb.common.datatype import is_reference, get_referenced_recordtype
 
from typing import Optional
 
 
import tempfile
 
REFERENCE = "REFERENCE"
REFERENCE = "REFERENCE"
@@ -79,13 +84,23 @@ class Grouped(object):
@@ -79,13 +84,23 @@ class Grouped(object):
return self.parents
return self.parents
def recordtypes_to_plantuml_string(iterable):
def recordtypes_to_plantuml_string(iterable,
 
add_properties: bool = True,
 
add_recordtypes: bool = True,
 
add_legend: bool = True,
 
style: str = "default"):
"""Converts RecordTypes into a string for PlantUML.
"""Converts RecordTypes into a string for PlantUML.
This function obtains an iterable and returns a string which can
This function obtains an iterable and returns a string which can
be input into PlantUML for a representation of all RecordTypes in
be input into PlantUML for a representation of all RecordTypes in
the iterable.
the iterable.
 
Current options for style
 
-------------------------
 
 
"default" - Standard rectangles with uml class circle and methods section
 
"salexan" - Round rectangles, hide circle and methods section
 
Current limitations
Current limitations
-------------------
-------------------
@@ -96,6 +111,8 @@ def recordtypes_to_plantuml_string(iterable):
@@ -96,6 +111,8 @@ def recordtypes_to_plantuml_string(iterable):
- Inheritance of Properties is not rendered nicely at the moment.
- Inheritance of Properties is not rendered nicely at the moment.
"""
"""
 
# TODO: This function needs a review of python type hints.
 
classes = [el for el in iterable
classes = [el for el in iterable
if isinstance(el, db.RecordType)]
if isinstance(el, db.RecordType)]
dependencies = {}
dependencies = {}
@@ -140,74 +157,87 @@ def recordtypes_to_plantuml_string(iterable):
@@ -140,74 +157,87 @@ def recordtypes_to_plantuml_string(iterable):
return result
return result
result = "@startuml\n\n"
result = "@startuml\n\n"
result += "skinparam classAttributeIconSize 0\n"
result += "package Properties #DDDDDD {\n"
if style == "default":
 
result += "skinparam classAttributeIconSize 0\n"
 
elif style == "salexan":
 
result += """skinparam roundcorner 20\n
 
skinparam boxpadding 20\n
 
\n
 
hide methods\n
 
hide circle\n
 
"""
 
else:
 
raise ValueError("Unknown style.")
for p in properties:
if add_properties:
inheritances[p] = p.get_parents()
result += "package Properties #DDDDDD {\n"
dependencies[p] = []
for p in properties:
 
inheritances[p] = p.get_parents()
 
dependencies[p] = []
result += "class \"{klass}\" << (P,#008800) >> {{\n".format(klass=p.name)
result += "class \"{klass}\" << (P,#008800) >> {{\n".format(klass=p.name)
if p.description is not None:
if p.description is not None:
result += get_description(p.description)
result += get_description(p.description)
result += "\n..\n"
result += "\n..\n"
if isinstance(p.datatype, str):
if isinstance(p.datatype, str):
result += "datatype: " + p.datatype + "\n"
result += "datatype: " + p.datatype + "\n"
elif isinstance(p.datatype, db.Entity):
elif isinstance(p.datatype, db.Entity):
result += "datatype: " + p.datatype.name + "\n"
result += "datatype: " + p.datatype.name + "\n"
else:
else:
result += "datatype: " + str(p.datatype) + "\n"
result += "datatype: " + str(p.datatype) + "\n"
 
result += "}\n\n"
result += "}\n\n"
result += "}\n\n"
result += "}\n\n"
result += "package RecordTypes #DDDDDD {\n"
if add_recordtypes:
 
result += "package RecordTypes #DDDDDD {\n"
for c in classes:
for c in classes:
inheritances[c] = c.get_parents()
inheritances[c] = c.get_parents()
dependencies[c] = []
dependencies[c] = []
result += "class \"{klass}\" << (C,#FF1111) >> {{\n".format(klass=c.name)
result += "class \"{klass}\" << (C,#FF1111) >> {{\n".format(klass=c.name)
if c.description is not None:
if c.description is not None:
result += get_description(c.description)
result += get_description(c.description)
props = ""
props = ""
props += _add_properties(c, importance=db.FIX)
props += _add_properties(c, importance=db.FIX)
props += _add_properties(c, importance=db.OBLIGATORY)
props += _add_properties(c, importance=db.OBLIGATORY)
props += _add_properties(c, importance=db.RECOMMENDED)
props += _add_properties(c, importance=db.RECOMMENDED)
props += _add_properties(c, importance=db.SUGGESTED)
props += _add_properties(c, importance=db.SUGGESTED)
if len(props) > 0:
if len(props) > 0:
result += "__Properties__\n" + props
result += "__Properties__\n" + props
else:
else:
result += "\n..\n"
result += "\n..\n"
result += "}\n\n"
result += "}\n\n"
for g in grouped:
for g in grouped:
inheritances[g] = g.get_parents()
inheritances[g] = g.get_parents()
result += "class \"{klass}\" << (G,#0000FF) >> {{\n".format(klass=g.name)
result += "class \"{klass}\" << (G,#0000FF) >> {{\n".format(klass=g.name)
result += "}\n\n"
result += "}\n\n"
for c, parents in inheritances.items():
for c, parents in inheritances.items():
for par in parents:
for par in parents:
result += "\"{par}\" <|-- \"{klass}\"\n".format(
result += "\"{par}\" <|-- \"{klass}\"\n".format(
klass=c.name, par=par.name)
klass=c.name, par=par.name)
for c, deps in dependencies.items():
for c, deps in dependencies.items():
for dep in deps:
for dep in deps:
result += "\"{klass}\" *-- \"{dep}\"\n".format(
result += "\"{klass}\" *-- \"{dep}\"\n".format(
klass=c.name, dep=dep)
klass=c.name, dep=dep)
result += """
if add_legend:
 
result += """
package \"B is a subtype of A\" <<Rectangle>> {
package \"B is a subtype of A\" <<Rectangle>> {
A <|-right- B
A <|-right- B
note "This determines what you find when you query for the RecordType.\\n'FIND RECORD A' will provide Records which have a parent\\nA or B, while 'FIND RECORD B' will provide only Records which have a parent B." as N1
note "This determines what you find when you query for the RecordType.\\n'FIND RECORD A' will provide Records which have a parent\\nA or B, while 'FIND RECORD B' will provide only Records which have a parent B." as N1
}
}
"""
"""
result += """
result += """
package \"The property P references an instance of D\" <<Rectangle>> {
package \"The property P references an instance of D\" <<Rectangle>> {
class C {
class C {
@@ -246,7 +276,8 @@ def retrieve_substructure(start_record_types, depth, result_id_set=None, result_
@@ -246,7 +276,8 @@ def retrieve_substructure(start_record_types, depth, result_id_set=None, result_
Returns
Returns
-------
-------
db.Container
db.Container
A container containing all the retrieved entites or None if cleanup is False.
A container containing all the retrieved entites
 
or None if cleanup is False.
"""
"""
# Initialize the id set and result container for level zero recursion depth:
# Initialize the id set and result container for level zero recursion depth:
if result_id_set is None:
if result_id_set is None:
@@ -260,9 +291,11 @@ def retrieve_substructure(start_record_types, depth, result_id_set=None, result_
@@ -260,9 +291,11 @@ def retrieve_substructure(start_record_types, depth, result_id_set=None, result_
result_container.append(entity)
result_container.append(entity)
result_id_set.add(entity.id)
result_id_set.add(entity.id)
for prop in entity.properties:
for prop in entity.properties:
if is_reference(prop.datatype) and prop.datatype != db.FILE and depth > 0:
if (is_reference(prop.datatype) and prop.datatype != db.FILE and depth > 0):
rt = db.RecordType(name=get_referenced_recordtype(prop.datatype)).retrieve()
rt = db.RecordType(
retrieve_substructure([rt], depth-1, result_id_set, result_container, False)
name=get_referenced_recordtype(prop.datatype)).retrieve()
 
retrieve_substructure([rt], depth-1, result_id_set,
 
result_container, False)
if prop.id not in result_id_set:
if prop.id not in result_id_set:
result_container.append(prop)
result_container.append(prop)
@@ -274,14 +307,22 @@ def retrieve_substructure(start_record_types, depth, result_id_set=None, result_
@@ -274,14 +307,22 @@ def retrieve_substructure(start_record_types, depth, result_id_set=None, result_
result_container.append(rt)
result_container.append(rt)
result_id_set.add(parent.id)
result_id_set.add(parent.id)
if depth > 0:
if depth > 0:
retrieve_substructure([rt], depth-1, result_id_set, result_container, False)
retrieve_substructure([rt], depth-1, result_id_set,
 
result_container, False)
if cleanup:
if cleanup:
return result_container
return result_container
return None
return None
def to_graphics(recordtypes, filename):
def to_graphics(recordtypes: list[db.Entity], filename: str,
 
output_dirname: Optional[str] = None,
 
formats: list[str] = ["tsvg"],
 
silent: bool = True,
 
add_properties: bool = True,
 
add_recordtypes: bool = True,
 
add_legend: bool = True,
 
style: str = "default"):
"""Calls recordtypes_to_plantuml_string(), saves result to file and
"""Calls recordtypes_to_plantuml_string(), saves result to file and
creates an svg image
creates an svg image
@@ -293,17 +334,52 @@ def to_graphics(recordtypes, filename):
@@ -293,17 +334,52 @@ def to_graphics(recordtypes, filename):
Iterable with the entities to be displayed.
Iterable with the entities to be displayed.
filename : str
filename : str
filename of the image without the extension(e.g. data_structure;
filename of the image without the extension(e.g. data_structure;
 
also without the preceeding path.
data_structure.pu and data_structure.svg will be created.)
data_structure.pu and data_structure.svg will be created.)
 
output_dirname : str
 
the destination directory for the resulting images as defined by the "-o"
 
option by plantuml
 
default is to use current working dir
 
formats : list[str]
 
list of target formats as defined by the -t"..." options by plantuml, e.g. "tsvg"
 
silent : bool
 
Don't output messages.
"""
"""
pu = recordtypes_to_plantuml_string(recordtypes)
pu = recordtypes_to_plantuml_string(recordtypes,
add_properties,
pu_filename = filename+".pu"
add_recordtypes,
with open(pu_filename, "w") as pu_file:
add_legend,
pu_file.write(pu)
style)
cmd = "plantuml -tsvg %s" % pu_filename
if output_dirname is None:
print("Executing:", cmd)
output_dirname = os.getcwd()
if os.system(cmd) != 0:
allowed_formats = [
raise Exception("An error occured during the execution of plantuml. "
"tpng", "tsvg", "teps", "tpdf", "tvdx", "txmi",
"Is plantuml installed?")
"tscxml", "thtml", "ttxt", "tutxt", "tlatex", "tlatex:nopreamble"]
 
 
with tempfile.TemporaryDirectory() as td:
 
 
pu_filename = os.path.join(td, filename + ".pu")
 
with open(pu_filename, "w") as pu_file:
 
pu_file.write(pu)
 
 
for format in formats:
 
extension = format[1:]
 
if ":" in extension:
 
extension = extension[:extension.index(":")]
 
 
if format not in allowed_formats:
 
raise RuntimeError("Format not allowed.")
 
cmd = "plantuml -{} {}".format(format, pu_filename)
 
if not silent:
 
print("Executing:", cmd)
 
 
if os.system(cmd) != 0: # TODO: replace with subprocess.run
 
raise Exception("An error occured during the execution of "
 
"plantuml when using the format {}. "
 
"Is plantuml installed? "
 
"You might want to dry a different format.".format(format))
 
# copy only the final product into the target directory
 
shutil.copy(os.path.join(td, filename + "." + extension),
 
output_dirname)
Loading