Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat custom diagram #159

Merged
merged 26 commits into from
Jan 15, 2025
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
a584e6f
feat: Implement custom_diagram
huyenngn Nov 9, 2024
c65647a
fix: Diagram target dictates unified edge direction
huyenngn Nov 9, 2024
cf6fdca
feat(custom_diagram): Add recursion option
huyenngn Nov 10, 2024
40c009c
fix: Move edges to owners
huyenngn Nov 11, 2024
bfa43bc
docs: Add docs for custom diagram
huyenngn Nov 11, 2024
be8a5b8
feat(custom_diagram): Add nested recursion and recursion depth
huyenngn Nov 12, 2024
810ca66
docs: Add expamles for custom_diagram
huyenngn Nov 12, 2024
8e6baea
fix: Fix recursion depth
huyenngn Nov 18, 2024
d7e94b9
fix: Straighten target edge
huyenngn Nov 18, 2024
d6592a0
feat(context-diagram): Add support for PhysicalPorts
huyenngn Nov 19, 2024
261ea06
docs: Add PhysicalPort to docs
huyenngn Nov 19, 2024
2a5f966
fix: Apply code review suggestions
huyenngn Nov 25, 2024
6305008
fix: Apply suggestions from code review
huyenngn Nov 25, 2024
e4dfd0d
fix: Fix minimum size calculation of boxes
huyenngn Nov 26, 2024
f091a65
refactor: Implement generic make_owner_boxes function
huyenngn Nov 26, 2024
bed6a30
fix: Fix mutability bug
huyenngn Nov 27, 2024
c744a6b
feat: Add support for python lambda filter
huyenngn Nov 27, 2024
3f99c27
fix: Add custom_diagram attribute to Class elements
huyenngn Dec 2, 2024
5398b91
refactor: Add generic make_owner_box method
huyenngn Dec 9, 2024
ad39f99
refactor(custom_diagram): Take iterable as collection
huyenngn Dec 9, 2024
d2162d4
docs(custom_diagram): Add example to docs
huyenngn Dec 9, 2024
51772a8
docs: Update custom_diagram docs
huyenngn Dec 16, 2024
fd3b4b1
fix: Remove duplicates from port context
ewuerger Jan 6, 2025
e492ffe
fix: Change custom diagram names
huyenngn Jan 8, 2025
7ace887
merge: Merge 'main'
ewuerger Jan 15, 2025
60d743b
docs: Fix cross-reference
ewuerger Jan 15, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
193 changes: 15 additions & 178 deletions capellambse_context_diagrams/collectors/custom.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,7 @@ def __init__(

self.data = makers.make_diagram(diagram)
self.params = params
self.instructions = self.diagram._collect
self.repeat_instructions: dict[str, t.Any] = {}
self.repeat_depth: int = 0
self.visited: set[str] = set()
self.collection = self.diagram._collect
self.boxes: dict[str, _elkjs.ELKInputChild] = {}
self.edges: dict[str, _elkjs.ELKInputEdge] = {}
self.ports: dict[str, _elkjs.ELKInputPort] = {}
Expand All @@ -74,15 +71,18 @@ def __call__(self) -> _elkjs.ELKInputData:
self._make_port_and_owner(self.target)
else:
self._make_target(self.target)

if target_edge := self.edges.get(self.target.uuid):
target_edge.layoutOptions = copy.deepcopy(
_elkjs.EDGE_STRAIGHTENING_LAYOUT_OPTIONS
)
if not self.instructions:
return self._get_data()

if self.diagram._unify_edge_direction == "UNIFORM":
self.directions[self.boxable_target.uuid] = False
self._perform_instructions(self.target, self.instructions)

for elem in self.collection:
self._make_target(elem)

if self.diagram._display_parent_relation:
current = self.boxable_target
while (
Expand All @@ -92,7 +92,9 @@ def __call__(self) -> _elkjs.ELKInputData:
and not isinstance(current.owner, generic.PackageTypes)
):
self.common_owners.discard(current.uuid)
current = self._make_owner_box(current)
current = generic.make_owner_box(
current, self._make_box, self.boxes, self.boxes_to_delete
)
self.common_owners.discard(current.uuid)
huyenngn marked this conversation as resolved.
Show resolved Hide resolved
for edge_uuid, box_uuid in self.edge_owners.items():
if box := self.boxes.get(box_uuid):
Expand All @@ -118,152 +120,6 @@ def _fix_box_heights(self) -> None:
box = self.boxes[uuid]
box.height = max([box.height] + list(min_heights.values()))

def _safely_eval_filter(self, obj: m.ModelElement, filter: str) -> bool:
if not filter.startswith("lambda"):
raise ValueError(f"Filter '{filter}' is not a lambda expression.")

safe_builtins = {
"abs",
"all",
"any",
"ascii",
"bin",
"bool",
"bytearray",
"bytes",
"callable",
"chr",
"classmethod",
"complex",
"dict",
"divmod",
"enumerate",
"filter",
"float",
"format",
"frozenset",
"getattr",
"hasattr",
"hash",
"hex",
"id",
"int",
"isinstance",
"issubclass",
"iter",
"len",
"list",
"map",
"max",
"memoryview",
"min",
"next",
"object",
"oct",
"ord",
"pow",
"print",
"property",
"range",
"repr",
"reversed",
"round",
"set",
"slice",
"sorted",
"staticmethod",
"str",
"sum",
"tuple",
"type",
"vars",
"zip",
}
allowed_builtins = {
name: getattr(builtins, name) for name in safe_builtins
}
allowed_builtins.update(
{
"True": True,
"False": False,
"capellambse": capellambse,
}
)

try:
# pylint: disable=eval-used
result = eval(filter, {"__builtins__": allowed_builtins})(obj)
except Exception as e:
raise ValueError(
f"Filter '{filter}' raised an exception: {e}"
) from e

if not isinstance(result, bool):
raise ValueError(
f"Filter '{filter}' did not return a boolean value."
)

return result

def _matches_filters(
self, obj: m.ModelElement, filters: dict[str, t.Any] | str
) -> bool:
if isinstance(filters, str):
return self._safely_eval_filter(obj, filters)
for key, value in filters.items():
if getattr(obj, key) != value:
return False
return True

def _perform_instructions(
self, obj: m.ModelElement, instructions: dict[str, t.Any]
) -> None:
if max_depth := instructions.pop("repeat", None):
self.repeat_instructions = instructions
self.repeat_depth = max_depth
if get_targets := instructions.get("get"):
self._perform_get_or_include(obj, get_targets, False)
if include_targets := instructions.get("include"):
self._perform_get_or_include(obj, include_targets, True)
if not get_targets and not include_targets:
if self.repeat_depth != 0:
self.repeat_depth -= 1
self._perform_instructions(obj, self.repeat_instructions)

def _perform_get_or_include(
self,
obj: m.ModelElement,
targets: dict[str, t.Any] | list[dict[str, t.Any]],
create: bool,
) -> None:
if isinstance(targets, dict):
targets = [targets]
assert isinstance(targets, list)
if self.repeat_depth > 0:
self.repeat_depth += len(targets)
for i in targets:
attr = i.get("name")
assert attr, "Attribute name is required."
target = getattr(obj, attr, None)
if isinstance(target, cabc.Iterable):
filters = i.get("filter", {})
for item in target:
if item.uuid in self.visited:
continue
self.visited.add(item.uuid)
if not self._matches_filters(item, filters):
continue
if create:
self._make_target(item)
self._perform_instructions(item, i)
elif isinstance(target, m.ModelElement):
if target.uuid in self.visited:
continue
self.visited.add(target.uuid)
if create:
self._make_target(target)
self._perform_instructions(target, i)

def _make_target(
self, obj: m.ModelElement
) -> _elkjs.ELKInputChild | _elkjs.ELKInputEdge | None:
Expand Down Expand Up @@ -293,34 +149,15 @@ def _make_box(
if self.diagram._display_parent_relation:
self.common_owners.add(
generic.make_owner_boxes(
obj, self.diagram_target_owners, self._make_owner_box
obj,
self.diagram_target_owners,
self._make_box,
self.boxes,
self.boxes_to_delete,
)
)
return box

def _make_owner_box(
self,
obj: t.Any,
) -> t.Any:
parent_box = self._make_box(
obj.owner,
layout_options=makers.DEFAULT_LABEL_LAYOUT_OPTIONS,
)
assert (obj_box := self.boxes.get(obj.uuid))
for box in (children := parent_box.children):
if box.id == obj.uuid:
break
else:
children.append(obj_box)
obj_box.width = max(
obj_box.width,
parent_box.width,
)
for label in parent_box.labels:
label.layoutOptions = makers.DEFAULT_LABEL_LAYOUT_OPTIONS
self.boxes_to_delete.add(obj.uuid)
return obj.owner

def _make_edge_and_ports(
self,
edge_obj: m.ModelElement,
Expand Down
35 changes: 10 additions & 25 deletions capellambse_context_diagrams/collectors/default.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,6 @@ def process_context(self):
):
box = self._make_box(
self.diagram.target.owner,
no_symbol=self.diagram._display_symbols_as_boxes,
layout_options=makers.DEFAULT_LABEL_LAYOUT_OPTIONS,
)
box.children = [self.centerbox]
Expand All @@ -83,8 +82,11 @@ def process_context(self):
and hasattr(current, "owner")
and not isinstance(current.owner, generic.PackageTypes)
):
current = self._make_owner_box(
current = generic.make_owner_box(
current,
self._make_box,
self.global_boxes,
self.boxes_to_delete,
)
self.common_owners.discard(current.uuid)

Expand Down Expand Up @@ -248,7 +250,6 @@ def _process_ports(self) -> None:
box = self._make_box(
owner,
height=height,
no_symbol=self.diagram._display_symbols_as_boxes,
)
box.ports = local_port_objs

Expand All @@ -257,7 +258,11 @@ def _process_ports(self) -> None:
if self.diagram._display_parent_relation:
self.common_owners.add(
generic.make_owner_boxes(
owner, self.diagram_target_owners, self._make_owner_box
owner,
self.diagram_target_owners,
self._make_box,
self.global_boxes,
self.boxes_to_delete,
)
)

Expand All @@ -284,33 +289,13 @@ def _make_box(
return box
box = makers.make_box(
obj,
no_symbol=self.diagram._display_symbols_as_boxes,
**kwargs,
)
self.global_boxes[obj.uuid] = box
self.made_boxes[obj.uuid] = box
return box

def _make_owner_box(
self,
obj: t.Any,
) -> t.Any:
parent_box = self._make_box(
obj.owner,
no_symbol=self.diagram._display_symbols_as_boxes,
layout_options=makers.DEFAULT_LABEL_LAYOUT_OPTIONS,
)
assert (obj_box := self.global_boxes.get(obj.uuid))
for box in (children := parent_box.children):
if box.id == obj.uuid:
break
else:
children.append(obj_box)
for label in parent_box.labels:
label.layoutOptions = makers.DEFAULT_LABEL_LAYOUT_OPTIONS

self.boxes_to_delete.add(obj.uuid)
return obj.owner


def collector(
diagram: context.ContextDiagram, params: dict[str, t.Any] | None = None
Expand Down
36 changes: 34 additions & 2 deletions capellambse_context_diagrams/collectors/generic.py
Original file line number Diff line number Diff line change
Expand Up @@ -269,8 +269,38 @@ def get_all_owners(obj: m.ModelElement) -> cabc.Iterator[str]:
current = getattr(current, "owner", None)


def make_owner_box(
obj: t.Any,
make_box_func: t.Callable,
boxes: dict[str, _elkjs.ELKInputChild],
boxes_to_delete: set[str],
) -> t.Any:
parent_box = make_box_func(
obj.owner,
layout_options=makers.DEFAULT_LABEL_LAYOUT_OPTIONS,
)
assert (obj_box := boxes.get(obj.uuid))
for box in (children := parent_box.children):
if box.id == obj.uuid:
break
else:
children.append(obj_box)
obj_box.width = max(
obj_box.width,
parent_box.width,
)
for label in parent_box.labels:
label.layoutOptions = makers.DEFAULT_LABEL_LAYOUT_OPTIONS
boxes_to_delete.add(obj.uuid)
return obj.owner


def make_owner_boxes(
obj: m.ModelElement, excluded: list[str], make_func: t.Callable
obj: m.ModelElement,
excluded: list[str],
make_box_func: t.Callable,
boxes: dict[str, _elkjs.ELKInputChild],
boxes_to_delete: set[str],
) -> str:
"""Create owner boxes for all owners of ``obj``."""
current = obj
Expand All @@ -280,5 +310,7 @@ def make_owner_boxes(
and getattr(current, "owner", None) is not None
and not isinstance(current.owner, PackageTypes)
):
current = make_func(current)
current = make_owner_box(
current, make_box_func, boxes, boxes_to_delete
)
return current.uuid
Loading
Loading