Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • caosdb/src/caosdb-pylib
1 result
Show changes
Commits on Source (7)
......@@ -13,6 +13,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed ###
- Added additional customization options to the plantuml module.
- The to_graphics function in the plantuml module uses a temporary directory now for creating the output files.
### Deprecated ###
### Removed ###
......
......@@ -125,7 +125,6 @@ class Entity(object):
self.id = id
self.state = None
def copy(self):
"""
Return a copy of entity.
......
......@@ -34,10 +34,15 @@ plantuml FILENAME.pu -> FILENAME.png
"""
import os
import shutil
import caosdb as db
from caosdb.common.datatype import is_reference, get_referenced_recordtype
from typing import Optional
import tempfile
REFERENCE = "REFERENCE"
......@@ -79,13 +84,23 @@ class Grouped(object):
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.
This function obtains an iterable and returns a string which can
be input into PlantUML for a representation of all RecordTypes in
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
-------------------
......@@ -96,6 +111,8 @@ def recordtypes_to_plantuml_string(iterable):
- 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
if isinstance(el, db.RecordType)]
dependencies = {}
......@@ -140,74 +157,87 @@ def recordtypes_to_plantuml_string(iterable):
return result
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:
inheritances[p] = p.get_parents()
dependencies[p] = []
if add_properties:
result += "package Properties #DDDDDD {\n"
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:
result += get_description(p.description)
result += "\n..\n"
if p.description is not None:
result += get_description(p.description)
result += "\n..\n"
if isinstance(p.datatype, str):
result += "datatype: " + p.datatype + "\n"
elif isinstance(p.datatype, db.Entity):
result += "datatype: " + p.datatype.name + "\n"
else:
result += "datatype: " + str(p.datatype) + "\n"
if isinstance(p.datatype, str):
result += "datatype: " + p.datatype + "\n"
elif isinstance(p.datatype, db.Entity):
result += "datatype: " + p.datatype.name + "\n"
else:
result += "datatype: " + str(p.datatype) + "\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:
inheritances[c] = c.get_parents()
dependencies[c] = []
result += "class \"{klass}\" << (C,#FF1111) >> {{\n".format(klass=c.name)
for c in classes:
inheritances[c] = c.get_parents()
dependencies[c] = []
result += "class \"{klass}\" << (C,#FF1111) >> {{\n".format(klass=c.name)
if c.description is not None:
result += get_description(c.description)
if c.description is not None:
result += get_description(c.description)
props = ""
props += _add_properties(c, importance=db.FIX)
props += _add_properties(c, importance=db.OBLIGATORY)
props += _add_properties(c, importance=db.RECOMMENDED)
props += _add_properties(c, importance=db.SUGGESTED)
props = ""
props += _add_properties(c, importance=db.FIX)
props += _add_properties(c, importance=db.OBLIGATORY)
props += _add_properties(c, importance=db.RECOMMENDED)
props += _add_properties(c, importance=db.SUGGESTED)
if len(props) > 0:
result += "__Properties__\n" + props
else:
result += "\n..\n"
result += "}\n\n"
if len(props) > 0:
result += "__Properties__\n" + props
else:
result += "\n..\n"
result += "}\n\n"
for g in grouped:
inheritances[g] = g.get_parents()
result += "class \"{klass}\" << (G,#0000FF) >> {{\n".format(klass=g.name)
result += "}\n\n"
for g in grouped:
inheritances[g] = g.get_parents()
result += "class \"{klass}\" << (G,#0000FF) >> {{\n".format(klass=g.name)
result += "}\n\n"
for c, parents in inheritances.items():
for par in parents:
result += "\"{par}\" <|-- \"{klass}\"\n".format(
klass=c.name, par=par.name)
for c, parents in inheritances.items():
for par in parents:
result += "\"{par}\" <|-- \"{klass}\"\n".format(
klass=c.name, par=par.name)
for c, deps in dependencies.items():
for dep in deps:
result += "\"{klass}\" *-- \"{dep}\"\n".format(
klass=c.name, dep=dep)
for c, deps in dependencies.items():
for dep in deps:
result += "\"{klass}\" *-- \"{dep}\"\n".format(
klass=c.name, dep=dep)
result += """
if add_legend:
result += """
package \"B is a subtype of A\" <<Rectangle>> {
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
}
"""
result += """
result += """
package \"The property P references an instance of D\" <<Rectangle>> {
class C {
......@@ -246,7 +276,8 @@ def retrieve_substructure(start_record_types, depth, result_id_set=None, result_
Returns
-------
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:
if result_id_set is None:
......@@ -260,9 +291,11 @@ def retrieve_substructure(start_record_types, depth, result_id_set=None, result_
result_container.append(entity)
result_id_set.add(entity.id)
for prop in entity.properties:
if is_reference(prop.datatype) and prop.datatype != db.FILE and depth > 0:
rt = db.RecordType(name=get_referenced_recordtype(prop.datatype)).retrieve()
retrieve_substructure([rt], depth-1, result_id_set, result_container, False)
if (is_reference(prop.datatype) and prop.datatype != db.FILE and depth > 0):
rt = db.RecordType(
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:
result_container.append(prop)
......@@ -274,14 +307,22 @@ def retrieve_substructure(start_record_types, depth, result_id_set=None, result_
result_container.append(rt)
result_id_set.add(parent.id)
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:
return result_container
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
creates an svg image
......@@ -293,17 +334,52 @@ def to_graphics(recordtypes, filename):
Iterable with the entities to be displayed.
filename : str
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.)
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_filename = filename+".pu"
with open(pu_filename, "w") as pu_file:
pu_file.write(pu)
cmd = "plantuml -tsvg %s" % pu_filename
print("Executing:", cmd)
if os.system(cmd) != 0:
raise Exception("An error occured during the execution of plantuml. "
"Is plantuml installed?")
pu = recordtypes_to_plantuml_string(recordtypes,
add_properties,
add_recordtypes,
add_legend,
style)
if output_dirname is None:
output_dirname = os.getcwd()
allowed_formats = [
"tpng", "tsvg", "teps", "tpdf", "tvdx", "txmi",
"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)
#!/usr/bin/env python3
# encoding: utf-8
#
# ** header v3.0
# This file is a part of the CaosDB Project.
#
# Copyright (C) 2022 Indiscale GmbH <info@indiscale.com>
# Copyright (C) 2022 Henrik tom Wörden <h.tomwoerden@indiscale.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
#
# ** end header
#
"""
test plantuml utility
"""
import tempfile
import pytest
import caosdb as db
import shutil
from caosdb.utils.plantuml import to_graphics
@pytest.fixture
def setup_n_teardown(autouse=True):
with tempfile.TemporaryDirectory() as td:
global output
output = td
yield
@pytest.fixture
def entities():
return [db.RecordType(name="TestRT1").add_property("testprop"),
db.RecordType(name="TestRT2").add_property("testprop2"),
db.Property("testprop")]
@pytest.mark.skipif(shutil.which('plantuml') is None, reason="No plantuml found")
def test_to_graphics1(entities, setup_n_teardown):
to_graphics(entities, "data_model", output_dirname=output)
@pytest.mark.skipif(shutil.which('plantuml') is None, reason="No plantuml found")
def test_to_graphics2(entities, setup_n_teardown):
to_graphics(entities, "data_model", output_dirname=output, formats=["tpng", "tsvg"],
add_properties=False, add_legend=False, style="salexan")