@@ -33,3 +33,5 @@ jobs: | |||
run: | | |||
poetry install | |||
poetry run python -m unittest -v tests/*.py | |||
- name: Run mypy | |||
run: poetry run mypy |
@@ -1,49 +1,44 @@ | |||
import contextvars | |||
import os | |||
import uuid | |||
from contextvars import ContextVar | |||
from pathlib import Path | |||
from typing import Dict, List, Optional, Union | |||
from typing import Any, AnyStr, cast, Dict, List, Mapping, Optional, Tuple, Type, Union | |||
from types import TracebackType | |||
from graphviz import Digraph | |||
from graphviz import Digraph # type: ignore[import] | |||
# Global contexts for a diagrams and a cluster. | |||
# | |||
# These global contexts are for letting the clusters and nodes know | |||
# where context they are belong to. So the all clusters and nodes does | |||
# not need to specify the current diagrams or cluster via parameters. | |||
__diagram = contextvars.ContextVar("diagrams") | |||
__cluster = contextvars.ContextVar("cluster") | |||
__diagram: ContextVar[Optional["Diagram"]] = ContextVar("diagrams") | |||
__cluster: ContextVar[Optional["Cluster"]] = ContextVar("cluster") | |||
def getdiagram() -> "Diagram": | |||
try: | |||
return __diagram.get() | |||
except LookupError: | |||
return None | |||
def getdiagram() -> Optional["Diagram"]: | |||
return __diagram.get(None) | |||
def setdiagram(diagram: "Diagram"): | |||
def setdiagram(diagram: Optional["Diagram"]) -> None: | |||
__diagram.set(diagram) | |||
def getcluster() -> "Cluster": | |||
try: | |||
return __cluster.get() | |||
except LookupError: | |||
return None | |||
def getcluster() -> Optional["Cluster"]: | |||
return __cluster.get(None) | |||
def setcluster(cluster: "Cluster"): | |||
def setcluster(cluster: Optional["Cluster"]) -> None: | |||
__cluster.set(cluster) | |||
class Diagram: | |||
__directions = ("TB", "BT", "LR", "RL") | |||
__curvestyles = ("ortho", "curved") | |||
__outformats = ("png", "jpg", "svg", "pdf", "dot") | |||
__directions: Tuple[str, ...] = ("TB", "BT", "LR", "RL") | |||
__curvestyles: Tuple[str, ...] = ("ortho", "curved") | |||
__outformats: Tuple[str, ...] = ("png", "jpg", "svg", "pdf", "dot") | |||
# fmt: off | |||
_default_graph_attrs = { | |||
_default_graph_attrs: Mapping[str, str] = { | |||
"pad": "2.0", | |||
"splines": "ortho", | |||
"nodesep": "0.60", | |||
@@ -52,7 +47,7 @@ class Diagram: | |||
"fontsize": "15", | |||
"fontcolor": "#2D3436", | |||
} | |||
_default_node_attrs = { | |||
_default_node_attrs: Mapping[str, str] = { | |||
"shape": "box", | |||
"style": "rounded", | |||
"fixedsize": "true", | |||
@@ -68,7 +63,7 @@ class Diagram: | |||
"fontsize": "13", | |||
"fontcolor": "#2D3436", | |||
} | |||
_default_edge_attrs = { | |||
_default_edge_attrs: Mapping[str, str] = { | |||
"color": "#7B8894", | |||
} | |||
@@ -82,13 +77,13 @@ class Diagram: | |||
filename: str = "", | |||
direction: str = "LR", | |||
curvestyle: str = "ortho", | |||
outformat: str = "png", | |||
outformat: Union[List[str], str] = "png", | |||
autolabel: bool = False, | |||
show: bool = True, | |||
strict: bool = False, | |||
graph_attr: Optional[dict] = None, | |||
node_attr: Optional[dict] = None, | |||
edge_attr: Optional[dict] = None, | |||
graph_attr: Optional[Mapping[str, Any]] = None, | |||
node_attr: Optional[Mapping[str, Any]] = None, | |||
edge_attr: Optional[Mapping[str, Any]] = None, | |||
): | |||
"""Diagram represents a global diagrams context. | |||
@@ -116,8 +111,8 @@ class Diagram: | |||
filename = "diagrams_image" | |||
elif not filename: | |||
filename = "_".join(self.name.split()).lower() | |||
self.filename = filename | |||
self.dot = Digraph(self.name, filename=self.filename, strict=strict) | |||
self.filename: str = filename | |||
self.dot: Digraph = Digraph(self.name, filename=self.filename, strict=strict) | |||
# Set attributes. | |||
for k, v in self._default_graph_attrs.items(): | |||
@@ -143,7 +138,7 @@ class Diagram: | |||
else: | |||
if not self._validate_outformat(outformat): | |||
raise ValueError(f'"{outformat}" is not a valid output format') | |||
self.outformat = outformat | |||
self.outformat: Union[List[str], str] = outformat | |||
# Merge passed in attributes | |||
self.dot.graph_attr.update(graph_attr) | |||
@@ -156,18 +151,23 @@ class Diagram: | |||
def __str__(self) -> str: | |||
return str(self.dot) | |||
def __enter__(self): | |||
def __enter__(self) -> "Diagram": | |||
setdiagram(self) | |||
return self | |||
def __exit__(self, exc_type, exc_value, traceback): | |||
def __exit__( | |||
self, | |||
exc_type: Optional[Type[BaseException]], | |||
exc_value: Optional[BaseException], | |||
traceback: Optional[TracebackType], | |||
) -> None: | |||
self.render() | |||
# Remove the graphviz file leaving only the image. | |||
os.remove(self.filename) | |||
setdiagram(None) | |||
def _repr_png_(self): | |||
return self.dot.pipe(format="png") | |||
def _repr_png(self) -> AnyStr: | |||
return cast(AnyStr, self.dot.pipe(format="png")) | |||
def _validate_direction(self, direction: str) -> bool: | |||
return direction.upper() in self.__directions | |||
@@ -178,7 +178,7 @@ class Diagram: | |||
def _validate_outformat(self, outformat: str) -> bool: | |||
return outformat.lower() in self.__outformats | |||
def node(self, nodeid: str, label: str, **attrs) -> None: | |||
def node(self, nodeid: str, label: str, **attrs: Dict[Any, Any]) -> None: | |||
"""Create a new node.""" | |||
self.dot.node(nodeid, label=label, **attrs) | |||
@@ -199,11 +199,11 @@ class Diagram: | |||
class Cluster: | |||
__directions = ("TB", "BT", "LR", "RL") | |||
__bgcolors = ("#E5F5FD", "#EBF3E7", "#ECE8F6", "#FDF7E3") | |||
__directions: Tuple[str, ...] = ("TB", "BT", "LR", "RL") | |||
__bgcolors: Tuple[str, ...] = ("#E5F5FD", "#EBF3E7", "#ECE8F6", "#FDF7E3") | |||
# fmt: off | |||
_default_graph_attrs = { | |||
_default_graph_attrs: Mapping[str, str] = { | |||
"shape": "box", | |||
"style": "rounded", | |||
"labeljust": "l", | |||
@@ -221,7 +221,7 @@ class Cluster: | |||
self, | |||
label: str = "cluster", | |||
direction: str = "LR", | |||
graph_attr: Optional[dict] = None, | |||
graph_attr: Optional[Mapping[str, Any]] = None, | |||
): | |||
"""Cluster represents a cluster context. | |||
@@ -231,10 +231,10 @@ class Cluster: | |||
""" | |||
if graph_attr is None: | |||
graph_attr = {} | |||
self.label = label | |||
self.name = "cluster_" + self.label | |||
self.label: str = label | |||
self.name: str = f"cluster_{self.label}" | |||
self.dot = Digraph(self.name) | |||
self.dot: Digraph = Digraph(self.name) | |||
# Set attributes. | |||
for k, v in self._default_graph_attrs.items(): | |||
@@ -246,24 +246,30 @@ class Cluster: | |||
self.dot.graph_attr["rankdir"] = direction | |||
# Node must be belong to a diagrams. | |||
self._diagram = getdiagram() | |||
if self._diagram is None: | |||
diagram = getdiagram() | |||
if diagram is None: | |||
raise EnvironmentError("Global diagrams context not set up") | |||
self._parent = getcluster() | |||
self._diagram: Diagram = diagram | |||
self._parent: Optional["Cluster"] = getcluster() | |||
# Set cluster depth for distinguishing the background color | |||
self.depth = self._parent.depth + 1 if self._parent else 0 | |||
self.depth: int = self._parent.depth + 1 if self._parent else 0 | |||
coloridx = self.depth % len(self.__bgcolors) | |||
self.dot.graph_attr["bgcolor"] = self.__bgcolors[coloridx] | |||
# Merge passed in attributes | |||
self.dot.graph_attr.update(graph_attr) | |||
def __enter__(self): | |||
def __enter__(self) -> "Cluster": | |||
setcluster(self) | |||
return self | |||
def __exit__(self, exc_type, exc_value, traceback): | |||
def __exit__( | |||
self, | |||
exc_type: Optional[Type[BaseException]], | |||
exc_value: Optional[BaseException], | |||
traceback: Optional[TracebackType], | |||
) -> None: | |||
if self._parent: | |||
self._parent.subgraph(self.dot) | |||
else: | |||
@@ -273,7 +279,7 @@ class Cluster: | |||
def _validate_direction(self, direction: str) -> bool: | |||
return direction.upper() in self.__directions | |||
def node(self, nodeid: str, label: str, **attrs) -> None: | |||
def node(self, nodeid: str, label: str, **attrs: Dict[Any, Any]) -> None: | |||
"""Create a new node in the cluster.""" | |||
self.dot.node(nodeid, label=label, **attrs) | |||
@@ -284,32 +290,33 @@ class Cluster: | |||
class Node: | |||
"""Node represents a node for a specific backend service.""" | |||
_provider = None | |||
_type = None | |||
_provider: Optional[str] = None | |||
_type: Optional[str] = None | |||
_icon_dir = None | |||
_icon = None | |||
_icon_dir: Optional[str] = None | |||
_icon: Optional[str] = None | |||
_height = 1.9 | |||
_height: float = 1.9 | |||
def __init__(self, label: str = "", *, nodeid: str = None, **attrs: Dict): | |||
def __init__(self, label: str = "", *, nodeid: Optional[str] = None, **attrs: Dict[Any, Any]): | |||
"""Node represents a system component. | |||
:param label: Node label. | |||
""" | |||
# Generates an ID for identifying a node, unless specified | |||
self._id = nodeid or self._rand_id() | |||
self.label = label | |||
self._id: str = nodeid or self._rand_id() | |||
self.label: str = label | |||
# Node must be belong to a diagrams. | |||
self._diagram = getdiagram() | |||
if self._diagram is None: | |||
diagram = getdiagram() | |||
if diagram is None: | |||
raise EnvironmentError("Global diagrams context not set up") | |||
self._diagram: Diagram = diagram | |||
if self._diagram.autolabel: | |||
prefix = self.__class__.__name__ | |||
if self.label: | |||
self.label = prefix + "\n" + self.label | |||
self.label = f"{prefix}\n{self.label}" | |||
else: | |||
self.label = prefix | |||
@@ -318,16 +325,17 @@ class Node: | |||
# that label being spanned between icon image and white space. | |||
# Increase the height by the number of new lines included in the label. | |||
padding = 0.4 * (self.label.count('\n')) | |||
self._attrs = { | |||
icon = self._load_icon() | |||
self._attrs: Dict[str, Any] = { | |||
"shape": "none", | |||
"height": str(self._height + padding), | |||
"image": self._load_icon(), | |||
} if self._icon else {} | |||
"image": icon, | |||
} if icon is not None else {} | |||
# fmt: on | |||
self._attrs.update(attrs) | |||
self._cluster = getcluster() | |||
self._cluster: Optional[Cluster] = getcluster() | |||
# If a node is in the cluster context, add it to cluster. | |||
if self._cluster: | |||
@@ -335,23 +343,22 @@ class Node: | |||
else: | |||
self._diagram.node(self._id, self.label, **self._attrs) | |||
def __repr__(self): | |||
_name = self.__class__.__name__ | |||
return f"<{self._provider}.{self._type}.{_name}>" | |||
def __repr__(self) -> str: | |||
name = self.__class__.__name__ | |||
return f"<{self._provider}.{self._type}.{name}>" | |||
def __sub__(self, other: Union["Node", List["Node"], "Edge"]): | |||
def __sub__(self, other: Union["Node", List["Node"], "Edge"]) -> Union["Node", List["Node"], "Edge"]: | |||
"""Implement Self - Node, Self - [Nodes] and Self - Edge.""" | |||
if isinstance(other, list): | |||
for node in other: | |||
self.connect(node, Edge(self)) | |||
return other | |||
elif isinstance(other, Node): | |||
if isinstance(other, Node): | |||
return self.connect(other, Edge(self)) | |||
else: | |||
other.node = self | |||
return other | |||
other.node = self | |||
return other | |||
def __rsub__(self, other: Union[List["Node"], List["Edge"]]): | |||
def __rsub__(self, other: Union[List["Node"], List["Edge"]]) -> "Node": | |||
"""Called for [Nodes] and [Edges] - Self because list don't have __sub__ operators.""" | |||
for o in other: | |||
if isinstance(o, Edge): | |||
@@ -360,32 +367,30 @@ class Node: | |||
o.connect(self, Edge(self)) | |||
return self | |||
def __rshift__(self, other: Union["Node", List["Node"], "Edge"]): | |||
def __rshift__(self, other: Union["Node", List["Node"], "Edge"]) -> Union["Node", List["Node"], "Edge"]: | |||
"""Implements Self >> Node, Self >> [Nodes] and Self Edge.""" | |||
if isinstance(other, list): | |||
for node in other: | |||
self.connect(node, Edge(self, forward=True)) | |||
return other | |||
elif isinstance(other, Node): | |||
if isinstance(other, Node): | |||
return self.connect(other, Edge(self, forward=True)) | |||
else: | |||
other.forward = True | |||
other.node = self | |||
return other | |||
other.forward = True | |||
other.node = self | |||
return other | |||
def __lshift__(self, other: Union["Node", List["Node"], "Edge"]): | |||
def __lshift__(self, other: Union["Node", List["Node"], "Edge"]) -> Union["Node", List["Node"], "Edge"]: | |||
"""Implements Self << Node, Self << [Nodes] and Self << Edge.""" | |||
if isinstance(other, list): | |||
for node in other: | |||
self.connect(node, Edge(self, reverse=True)) | |||
return other | |||
elif isinstance(other, Node): | |||
if isinstance(other, Node): | |||
return self.connect(other, Edge(self, reverse=True)) | |||
else: | |||
other.reverse = True | |||
return other.connect(self) | |||
other.reverse = True | |||
return other.connect(self) | |||
def __rrshift__(self, other: Union[List["Node"], List["Edge"]]): | |||
def __rrshift__(self, other: Union[List["Node"], List["Edge"]]) -> "Node": | |||
"""Called for [Nodes] and [Edges] >> Self because list don't have __rshift__ operators.""" | |||
for o in other: | |||
if isinstance(o, Edge): | |||
@@ -395,7 +400,7 @@ class Node: | |||
o.connect(self, Edge(self, forward=True)) | |||
return self | |||
def __rlshift__(self, other: Union[List["Node"], List["Edge"]]): | |||
def __rlshift__(self, other: Union[List["Node"], List["Edge"]]) -> "Node": | |||
"""Called for [Nodes] << Self because list of Nodes don't have __lshift__ operators.""" | |||
for o in other: | |||
if isinstance(o, Edge): | |||
@@ -406,11 +411,11 @@ class Node: | |||
return self | |||
@property | |||
def nodeid(self): | |||
def nodeid(self) -> str: | |||
return self._id | |||
# TODO: option for adding flow description to the connection edge | |||
def connect(self, node: "Node", edge: "Edge"): | |||
def connect(self, node: "Node", edge: "Edge") -> "Node": | |||
"""Connect to other node. | |||
:param node: Other node instance. | |||
@@ -426,18 +431,19 @@ class Node: | |||
return node | |||
@staticmethod | |||
def _rand_id(): | |||
def _rand_id() -> str: | |||
return uuid.uuid4().hex | |||
def _load_icon(self): | |||
basedir = Path(os.path.abspath(os.path.dirname(__file__))) | |||
return os.path.join(basedir.parent, self._icon_dir, self._icon) | |||
def _load_icon(self) -> Optional[str]: | |||
if self._icon_dir is None or self._icon is None: | |||
return None | |||
return str(Path(__file__).parent / self._icon_dir / self._icon) | |||
class Edge: | |||
"""Edge represents an edge between two nodes.""" | |||
_default_edge_attrs = { | |||
_default_edge_attrs: Mapping[str, str] = { | |||
"fontcolor": "#2D3436", | |||
"fontname": "Sans-Serif", | |||
"fontsize": "13", | |||
@@ -445,13 +451,13 @@ class Edge: | |||
def __init__( | |||
self, | |||
node: "Node" = None, | |||
node: Optional["Node"] = None, | |||
forward: bool = False, | |||
reverse: bool = False, | |||
label: str = "", | |||
color: str = "", | |||
style: str = "", | |||
**attrs: Dict, | |||
**attrs: Dict[Any, Any], | |||
): | |||
"""Edge represents an edge between two nodes. | |||
@@ -466,11 +472,11 @@ class Edge: | |||
if node is not None: | |||
assert isinstance(node, Node) | |||
self.node = node | |||
self.forward = forward | |||
self.reverse = reverse | |||
self.node: Optional[Node] = node | |||
self.forward: bool = forward | |||
self.reverse: bool = reverse | |||
self._attrs = {} | |||
self._attrs: Dict[str, Any] = {} | |||
# Set attributes. | |||
for k, v in self._default_edge_attrs.items(): | |||
@@ -486,7 +492,7 @@ class Edge: | |||
self._attrs["style"] = style | |||
self._attrs.update(attrs) | |||
def __sub__(self, other: Union["Node", "Edge", List["Node"]]): | |||
def __sub__(self, other: Union["Node", "Edge", List["Node"]]) -> Union["Node", "Edge", List["Node"]]: | |||
"""Implement Self - Node or Edge and Self - [Nodes]""" | |||
return self.connect(other) | |||
@@ -494,12 +500,12 @@ class Edge: | |||
"""Called for [Nodes] or [Edges] - Self because list don't have __sub__ operators.""" | |||
return self.append(other) | |||
def __rshift__(self, other: Union["Node", "Edge", List["Node"]]): | |||
def __rshift__(self, other: Union["Node", "Edge", List["Node"]]) -> Union["Node", "Edge", List["Node"]]: | |||
"""Implements Self >> Node or Edge and Self >> [Nodes].""" | |||
self.forward = True | |||
return self.connect(other) | |||
def __lshift__(self, other: Union["Node", "Edge", List["Node"]]): | |||
def __lshift__(self, other: Union["Node", "Edge", List["Node"]]) -> Union["Node", "Edge", List["Node"]]: | |||
"""Implements Self << Node or Edge and Self << [Nodes].""" | |||
self.reverse = True | |||
return self.connect(other) | |||
@@ -512,35 +518,36 @@ class Edge: | |||
"""Called for [Nodes] or [Edges] << Self because list of Edges don't have __lshift__ operators.""" | |||
return self.append(other, reverse=True) | |||
def append(self, other: Union[List["Node"], List["Edge"]], forward=None, reverse=None) -> List["Edge"]: | |||
def append( | |||
self, other: Union[List["Node"], List["Edge"]], forward: Optional[bool] = None, reverse: Optional[bool] = None | |||
) -> List["Edge"]: | |||
result = [] | |||
for o in other: | |||
if isinstance(o, Edge): | |||
o.forward = forward if forward else o.forward | |||
o.reverse = forward if forward else o.reverse | |||
o.reverse = reverse if reverse else o.reverse | |||
self._attrs = o.attrs.copy() | |||
result.append(o) | |||
else: | |||
result.append(Edge(o, forward=forward, reverse=reverse, **self._attrs)) | |||
result.append(Edge(o, forward=bool(forward), reverse=bool(reverse), **self._attrs)) | |||
return result | |||
def connect(self, other: Union["Node", "Edge", List["Node"]]): | |||
def connect(self, other: Union["Node", "Edge", List["Node"]]) -> Union["Node", "Edge", List["Node"]]: | |||
if isinstance(other, list): | |||
for node in other: | |||
self.node.connect(node, self) | |||
if self.node is not None: | |||
for node in other: | |||
self.node.connect(node, self) | |||
return other | |||
elif isinstance(other, Edge): | |||
if isinstance(other, Edge): | |||
self._attrs = other._attrs.copy() | |||
return self | |||
else: | |||
if self.node is not None: | |||
return self.node.connect(other, self) | |||
else: | |||
self.node = other | |||
return self | |||
if self.node is not None: | |||
return self.node.connect(other, self) | |||
self.node = other | |||
return self | |||
@property | |||
def attrs(self) -> Dict: | |||
def attrs(self) -> Dict[str, Any]: | |||
if self.forward and self.reverse: | |||
direction = "both" | |||
elif self.forward: | |||
@@ -552,4 +559,4 @@ class Edge: | |||
return {**self._attrs, "dir": direction} | |||
Group = Cluster | |||
Group: Type[Cluster] = Cluster |
@@ -102,8 +102,3 @@ class User(_General): | |||
class Users(_General): | |||
_icon = "users.png" | |||
# Aliases | |||
OfficeBuilding = GenericOfficeBuilding |
@@ -3,10 +3,12 @@ A set of nodes and edges to visualize software architecture using the C4 model. | |||
""" | |||
import html | |||
import textwrap | |||
from typing import Any, Dict | |||
from diagrams import Cluster, Node, Edge | |||
def _format_node_label(name, key, description): | |||
def _format_node_label(name: str, key: str, description: str) -> str: | |||
"""Create a graphviz label string for a C4 node""" | |||
title = f'<font point-size="12"><b>{html.escape(name)}</b></font><br/>' | |||
subtitle = f'<font point-size="9">[{html.escape(key)}]<br/></font>' if key else "" | |||
@@ -14,7 +16,7 @@ def _format_node_label(name, key, description): | |||
return f"<{title}{subtitle}{text}>" | |||
def _format_description(description): | |||
def _format_description(description: str) -> str: | |||
""" | |||
Formats the description string so it fits into the C4 nodes. | |||
@@ -29,7 +31,7 @@ def _format_description(description): | |||
return "<br/>".join(lines) | |||
def _format_edge_label(description): | |||
def _format_edge_label(description: str) -> str: | |||
"""Create a graphviz label string for a C4 edge""" | |||
wrapper = textwrap.TextWrapper(width=24, max_lines=3) | |||
lines = [html.escape(line) for line in wrapper.wrap(description)] | |||
@@ -37,9 +39,9 @@ def _format_edge_label(description): | |||
return f'<<font point-size="10">{text}</font>>' | |||
def C4Node(name, technology="", description="", type="Container", **kwargs): | |||
def C4Node(name: str, technology: str = "", description: str = "", type: str = "Container", **kwargs: Dict[str, Any]) -> Node: | |||
key = f"{type}: {technology}" if technology else type | |||
node_attributes = { | |||
node_attributes: Dict[str, Any] = { | |||
"label": _format_node_label(name, key, description), | |||
"labelloc": "c", | |||
"shape": "rect", | |||
@@ -57,8 +59,8 @@ def C4Node(name, technology="", description="", type="Container", **kwargs): | |||
return Node(**node_attributes) | |||
def Container(name, technology="", description="", **kwargs): | |||
container_attributes = { | |||
def Container(name: str, technology: str = "", description: str = "", **kwargs: Dict[str, Any]) -> Node: | |||
container_attributes: Dict[str, Any] = { | |||
"name": name, | |||
"technology": technology, | |||
"description": description, | |||
@@ -68,8 +70,8 @@ def Container(name, technology="", description="", **kwargs): | |||
return C4Node(**container_attributes) | |||
def Database(name, technology="", description="", **kwargs): | |||
database_attributes = { | |||
def Database(name: str, technology: str = "", description: str = "", **kwargs: Dict[str, Any]) -> Node: | |||
database_attributes: Dict[str, Any] = { | |||
"name": name, | |||
"technology": technology, | |||
"description": description, | |||
@@ -81,8 +83,8 @@ def Database(name, technology="", description="", **kwargs): | |||
return C4Node(**database_attributes) | |||
def System(name, description="", external=False, **kwargs): | |||
system_attributes = { | |||
def System(name: str, description: str = "", external: bool = False, **kwargs: Dict[str, Any]) -> Node: | |||
system_attributes: Dict[str, Any] = { | |||
"name": name, | |||
"description": description, | |||
"type": "External System" if external else "System", | |||
@@ -92,8 +94,8 @@ def System(name, description="", external=False, **kwargs): | |||
return C4Node(**system_attributes) | |||
def Person(name, description="", external=False, **kwargs): | |||
person_attributes = { | |||
def Person(name: str, description: str = "", external: bool = False, **kwargs: Dict[str, Any]) -> Node: | |||
person_attributes: Dict[str, Any] = { | |||
"name": name, | |||
"description": description, | |||
"type": "External Person" if external else "Person", | |||
@@ -104,8 +106,8 @@ def Person(name, description="", external=False, **kwargs): | |||
return C4Node(**person_attributes) | |||
def SystemBoundary(name, **kwargs): | |||
graph_attributes = { | |||
def SystemBoundary(name: str, **kwargs: Dict[str, Any]) -> Cluster: | |||
graph_attributes: Dict[str, Any] = { | |||
"label": html.escape(name), | |||
"bgcolor": "white", | |||
"margin": "16", | |||
@@ -115,8 +117,8 @@ def SystemBoundary(name, **kwargs): | |||
return Cluster(name, graph_attr=graph_attributes) | |||
def Relationship(label="", **kwargs): | |||
edge_attributes = { | |||
def Relationship(label: str = "", **kwargs: Dict[str, Any]) -> Edge: | |||
edge_attributes: Dict[str, Any] = { | |||
"style": "dashed", | |||
"color": "gray60", | |||
"label": _format_edge_label(label) if label else "", | |||
@@ -2,6 +2,8 @@ | |||
Custom provides the possibility of load an image to be presented as a node. | |||
""" | |||
from typing import Any, Dict, Optional, override | |||
from diagrams import Node | |||
@@ -12,9 +14,11 @@ class Custom(Node): | |||
fontcolor = "#ffffff" | |||
def _load_icon(self): | |||
@override | |||
def _load_icon(self) -> Optional[str]: | |||
return self._icon | |||
def __init__(self, label, icon_path, *args, **kwargs): | |||
self._icon = icon_path | |||
super().__init__(label, *args, **kwargs) | |||
@override | |||
def __init__(self, label: str, icon_path: Optional[str], *, nodeid: Optional[str] = None, **attrs: Dict[Any, Any]): | |||
self._icon: Optional[str] = icon_path | |||
super().__init__(label, nodeid=nodeid, **attrs) |
@@ -1,10 +1,9 @@ | |||
# This file is automatically @generated by Poetry and should not be changed by hand. | |||
# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. | |||
[[package]] | |||
name = "astroid" | |||
version = "2.9.0" | |||
description = "An abstract syntax tree for Python with inference support." | |||
category = "dev" | |||
optional = false | |||
python-versions = "~=3.6" | |||
files = [ | |||
@@ -23,7 +22,6 @@ wrapt = ">=1.11,<1.14" | |||
name = "black" | |||
version = "22.12.0" | |||
description = "The uncompromising code formatter." | |||
category = "dev" | |||
optional = false | |||
python-versions = ">=3.7" | |||
files = [ | |||
@@ -60,7 +58,6 @@ uvloop = ["uvloop (>=0.15.2)"] | |||
name = "click" | |||
version = "8.1.3" | |||
description = "Composable command line interface toolkit" | |||
category = "dev" | |||
optional = false | |||
python-versions = ">=3.7" | |||
files = [ | |||
@@ -76,7 +73,6 @@ importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} | |||
name = "colorama" | |||
version = "0.4.3" | |||
description = "Cross-platform colored terminal text." | |||
category = "dev" | |||
optional = false | |||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" | |||
files = [ | |||
@@ -88,7 +84,6 @@ files = [ | |||
name = "exceptiongroup" | |||
version = "1.1.0" | |||
description = "Backport of PEP 654 (exception groups)" | |||
category = "dev" | |||
optional = false | |||
python-versions = ">=3.7" | |||
files = [ | |||
@@ -103,7 +98,6 @@ test = ["pytest (>=6)"] | |||
name = "graphviz" | |||
version = "0.20.1" | |||
description = "Simple Python interface for Graphviz" | |||
category = "main" | |||
optional = false | |||
python-versions = ">=3.7" | |||
files = [ | |||
@@ -120,7 +114,6 @@ test = ["coverage", "mock (>=4)", "pytest (>=7)", "pytest-cov", "pytest-mock (>= | |||
name = "importlib-metadata" | |||
version = "1.5.0" | |||
description = "Read metadata from Python packages" | |||
category = "dev" | |||
optional = false | |||
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" | |||
files = [ | |||
@@ -139,7 +132,6 @@ testing = ["importlib-resources", "packaging"] | |||
name = "iniconfig" | |||
version = "1.1.1" | |||
description = "iniconfig: brain-dead simple config-ini parsing" | |||
category = "dev" | |||
optional = false | |||
python-versions = "*" | |||
files = [ | |||
@@ -151,7 +143,6 @@ files = [ | |||
name = "isort" | |||
version = "4.3.21" | |||
description = "A Python utility / library to sort Python imports." | |||
category = "dev" | |||
optional = false | |||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" | |||
files = [ | |||
@@ -169,7 +160,6 @@ xdg-home = ["appdirs (>=1.4.0)"] | |||
name = "jinja2" | |||
version = "3.1.2" | |||
description = "A very fast and expressive template engine." | |||
category = "main" | |||
optional = false | |||
python-versions = ">=3.7" | |||
files = [ | |||
@@ -187,7 +177,6 @@ i18n = ["Babel (>=2.7)"] | |||
name = "lazy-object-proxy" | |||
version = "1.4.3" | |||
description = "A fast and thorough lazy object proxy." | |||
category = "dev" | |||
optional = false | |||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" | |||
files = [ | |||
@@ -218,7 +207,6 @@ files = [ | |||
name = "markupsafe" | |||
version = "2.0.1" | |||
description = "Safely add untrusted strings to HTML/XML markup." | |||
category = "main" | |||
optional = false | |||
python-versions = ">=3.6" | |||
files = [ | |||
@@ -297,7 +285,6 @@ files = [ | |||
name = "mccabe" | |||
version = "0.6.1" | |||
description = "McCabe checker, plugin for flake8" | |||
category = "dev" | |||
optional = false | |||
python-versions = "*" | |||
files = [ | |||
@@ -306,22 +293,67 @@ files = [ | |||
] | |||
[[package]] | |||
name = "mypy" | |||
version = "1.4.1" | |||
description = "Optional static typing for Python" | |||
optional = false | |||
python-versions = ">=3.7" | |||
files = [ | |||
{file = "mypy-1.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:566e72b0cd6598503e48ea610e0052d1b8168e60a46e0bfd34b3acf2d57f96a8"}, | |||
{file = "mypy-1.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ca637024ca67ab24a7fd6f65d280572c3794665eaf5edcc7e90a866544076878"}, | |||
{file = "mypy-1.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0dde1d180cd84f0624c5dcaaa89c89775550a675aff96b5848de78fb11adabcd"}, | |||
{file = "mypy-1.4.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8c4d8e89aa7de683e2056a581ce63c46a0c41e31bd2b6d34144e2c80f5ea53dc"}, | |||
{file = "mypy-1.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:bfdca17c36ae01a21274a3c387a63aa1aafe72bff976522886869ef131b937f1"}, | |||
{file = "mypy-1.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7549fbf655e5825d787bbc9ecf6028731973f78088fbca3a1f4145c39ef09462"}, | |||
{file = "mypy-1.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:98324ec3ecf12296e6422939e54763faedbfcc502ea4a4c38502082711867258"}, | |||
{file = "mypy-1.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:141dedfdbfe8a04142881ff30ce6e6653c9685b354876b12e4fe6c78598b45e2"}, | |||
{file = "mypy-1.4.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8207b7105829eca6f3d774f64a904190bb2231de91b8b186d21ffd98005f14a7"}, | |||
{file = "mypy-1.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:16f0db5b641ba159eff72cff08edc3875f2b62b2fa2bc24f68c1e7a4e8232d01"}, | |||
{file = "mypy-1.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:470c969bb3f9a9efcedbadcd19a74ffb34a25f8e6b0e02dae7c0e71f8372f97b"}, | |||
{file = "mypy-1.4.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e5952d2d18b79f7dc25e62e014fe5a23eb1a3d2bc66318df8988a01b1a037c5b"}, | |||
{file = "mypy-1.4.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:190b6bab0302cec4e9e6767d3eb66085aef2a1cc98fe04936d8a42ed2ba77bb7"}, | |||
{file = "mypy-1.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:9d40652cc4fe33871ad3338581dca3297ff5f2213d0df345bcfbde5162abf0c9"}, | |||
{file = "mypy-1.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:01fd2e9f85622d981fd9063bfaef1aed6e336eaacca00892cd2d82801ab7c042"}, | |||
{file = "mypy-1.4.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2460a58faeea905aeb1b9b36f5065f2dc9a9c6e4c992a6499a2360c6c74ceca3"}, | |||
{file = "mypy-1.4.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a2746d69a8196698146a3dbe29104f9eb6a2a4d8a27878d92169a6c0b74435b6"}, | |||
{file = "mypy-1.4.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:ae704dcfaa180ff7c4cfbad23e74321a2b774f92ca77fd94ce1049175a21c97f"}, | |||
{file = "mypy-1.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:43d24f6437925ce50139a310a64b2ab048cb2d3694c84c71c3f2a1626d8101dc"}, | |||
{file = "mypy-1.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c482e1246726616088532b5e964e39765b6d1520791348e6c9dc3af25b233828"}, | |||
{file = "mypy-1.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:43b592511672017f5b1a483527fd2684347fdffc041c9ef53428c8dc530f79a3"}, | |||
{file = "mypy-1.4.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:34a9239d5b3502c17f07fd7c0b2ae6b7dd7d7f6af35fbb5072c6208e76295816"}, | |||
{file = "mypy-1.4.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5703097c4936bbb9e9bce41478c8d08edd2865e177dc4c52be759f81ee4dd26c"}, | |||
{file = "mypy-1.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:e02d700ec8d9b1859790c0475df4e4092c7bf3272a4fd2c9f33d87fac4427b8f"}, | |||
{file = "mypy-1.4.1-py3-none-any.whl", hash = "sha256:45d32cec14e7b97af848bddd97d85ea4f0db4d5a149ed9676caa4eb2f7402bb4"}, | |||
{file = "mypy-1.4.1.tar.gz", hash = "sha256:9bbcd9ab8ea1f2e1c8031c21445b511442cc45c89951e49bbf852cbb70755b1b"}, | |||
] | |||
[package.dependencies] | |||
mypy-extensions = ">=1.0.0" | |||
tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} | |||
typed-ast = {version = ">=1.4.0,<2", markers = "python_version < \"3.8\""} | |||
typing-extensions = ">=4.1.0" | |||
[package.extras] | |||
dmypy = ["psutil (>=4.0)"] | |||
install-types = ["pip"] | |||
python2 = ["typed-ast (>=1.4.0,<2)"] | |||
reports = ["lxml"] | |||
[[package]] | |||
name = "mypy-extensions" | |||
version = "0.4.3" | |||
description = "Experimental type system extensions for programs checked with the mypy typechecker." | |||
category = "dev" | |||
version = "1.0.0" | |||
description = "Type system extensions for programs checked with the mypy type checker." | |||
optional = false | |||
python-versions = "*" | |||
python-versions = ">=3.5" | |||
files = [ | |||
{file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, | |||
{file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, | |||
{file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, | |||
{file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, | |||
] | |||
[[package]] | |||
name = "packaging" | |||
version = "20.8" | |||
description = "Core utilities for Python packages" | |||
category = "dev" | |||
optional = false | |||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" | |||
files = [ | |||
@@ -336,7 +368,6 @@ pyparsing = ">=2.0.2" | |||
name = "pathspec" | |||
version = "0.10.1" | |||
description = "Utility library for gitignore style pattern matching of file paths." | |||
category = "dev" | |||
optional = false | |||
python-versions = ">=3.7" | |||
files = [ | |||
@@ -348,7 +379,6 @@ files = [ | |||
name = "platformdirs" | |||
version = "2.4.0" | |||
description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." | |||
category = "dev" | |||
optional = false | |||
python-versions = ">=3.6" | |||
files = [ | |||
@@ -364,7 +394,6 @@ test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock | |||
name = "pluggy" | |||
version = "0.13.1" | |||
description = "plugin and hook calling mechanisms for python" | |||
category = "dev" | |||
optional = false | |||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" | |||
files = [ | |||
@@ -382,7 +411,6 @@ dev = ["pre-commit", "tox"] | |||
name = "pylint" | |||
version = "2.12.0" | |||
description = "python code static checker" | |||
category = "dev" | |||
optional = false | |||
python-versions = "~=3.6" | |||
files = [ | |||
@@ -403,7 +431,6 @@ typing-extensions = {version = ">=3.10.0", markers = "python_version < \"3.10\"" | |||
name = "pyparsing" | |||
version = "2.4.7" | |||
description = "Python parsing module" | |||
category = "dev" | |||
optional = false | |||
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" | |||
files = [ | |||
@@ -415,7 +442,6 @@ files = [ | |||
name = "pytest" | |||
version = "7.3.0" | |||
description = "pytest: simple powerful testing with Python" | |||
category = "dev" | |||
optional = false | |||
python-versions = ">=3.7" | |||
files = [ | |||
@@ -439,7 +465,6 @@ testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "no | |||
name = "rope" | |||
version = "0.14.0" | |||
description = "a python refactoring library..." | |||
category = "dev" | |||
optional = false | |||
python-versions = "*" | |||
files = [ | |||
@@ -452,7 +477,6 @@ files = [ | |||
name = "setuptools" | |||
version = "59.6.0" | |||
description = "Easily download, build, install, upgrade, and uninstall Python packages" | |||
category = "dev" | |||
optional = false | |||
python-versions = ">=3.6" | |||
files = [ | |||
@@ -468,7 +492,6 @@ testing = ["flake8-2020", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "mock" | |||
name = "toml" | |||
version = "0.10.0" | |||
description = "Python Library for Tom's Obvious, Minimal Language" | |||
category = "dev" | |||
optional = false | |||
python-versions = "*" | |||
files = [ | |||
@@ -480,7 +503,6 @@ files = [ | |||
name = "tomli" | |||
version = "1.2.3" | |||
description = "A lil' TOML parser" | |||
category = "dev" | |||
optional = false | |||
python-versions = ">=3.6" | |||
files = [ | |||
@@ -492,7 +514,6 @@ files = [ | |||
name = "typed-ast" | |||
version = "1.5.4" | |||
description = "a fork of Python 2 and 3 ast modules with type comment support" | |||
category = "main" | |||
optional = false | |||
python-versions = ">=3.6" | |||
files = [ | |||
@@ -526,7 +547,6 @@ files = [ | |||
name = "typing-extensions" | |||
version = "4.1.1" | |||
description = "Backported and Experimental Type Hints for Python 3.6+" | |||
category = "dev" | |||
optional = false | |||
python-versions = ">=3.6" | |||
files = [ | |||
@@ -538,7 +558,6 @@ files = [ | |||
name = "wrapt" | |||
version = "1.11.2" | |||
description = "Module for decorators, wrappers and monkey patching." | |||
category = "dev" | |||
optional = false | |||
python-versions = "*" | |||
files = [ | |||
@@ -549,7 +568,6 @@ files = [ | |||
name = "zipp" | |||
version = "3.1.0" | |||
description = "Backport of pathlib-compatible object wrapper for zip files" | |||
category = "dev" | |||
optional = false | |||
python-versions = ">=3.6" | |||
files = [ | |||
@@ -564,4 +582,4 @@ testing = ["func-timeout", "jaraco.itertools"] | |||
[metadata] | |||
lock-version = "2.0" | |||
python-versions = "^3.7" | |||
content-hash = "5c2b82d1c8a6a283f63558fc693271f42fdcaeab73616713eb9b38b0b59787fc" | |||
content-hash = "03625aefdb3d0562b74d70e00eda5ddb3a477a433adc9aa01b351e3cef897403" |
@@ -21,6 +21,15 @@ pylint = "^2.7" | |||
rope = "^0.14.0" | |||
isort = "^4.3" | |||
black = "^22.12.0" | |||
mypy = ">=1.4,<1.8" | |||
[tool.black] | |||
line-length = 120 | |||
[tool.mypy] | |||
files = "diagrams, tests" | |||
mypy_path = "src" | |||
namespace_packages = true | |||
explicit_package_bases = true | |||
show_error_codes = true | |||
strict = true |
@@ -3,47 +3,46 @@ import random | |||
import string | |||
import unittest | |||
from diagrams import Diagram | |||
from diagrams import setcluster, setdiagram | |||
from diagrams import setcluster, setdiagram, Diagram | |||
from diagrams.c4 import Person, Container, Database, System, SystemBoundary, Relationship | |||
class C4Test(unittest.TestCase): | |||
def setUp(self): | |||
self.name = "diagram-" + "".join([random.choice(string.hexdigits) for n in range(7)]).lower() | |||
def setUp(self) -> None: | |||
self.name: str = "diagram-" + "".join([random.choice(string.hexdigits) for n in range(7)]).lower() | |||
def tearDown(self): | |||
def tearDown(self) -> None: | |||
setdiagram(None) | |||
setcluster(None) | |||
try: | |||
os.remove(self.name + ".png") | |||
os.remove(f"{self.name}.png") | |||
except FileNotFoundError: | |||
pass | |||
def test_nodes(self): | |||
def test_nodes(self) -> None: | |||
with Diagram(name=self.name, show=False): | |||
person = Person("person", "A person.") | |||
container = Container("container", "Java application", "The application.") | |||
database = Database("database", "Oracle database", "Stores information.") | |||
def test_external_nodes(self): | |||
def test_external_nodes(self) -> None: | |||
with Diagram(name=self.name, show=False): | |||
external_person = Person("person", external=True) | |||
external_system = System("external", external=True) | |||
def test_systems(self): | |||
def test_systems(self) -> None: | |||
with Diagram(name=self.name, show=False): | |||
system = System("system", "The internal system.") | |||
system_without_description = System("unknown") | |||
def test_edges(self): | |||
def test_edges(self) -> None: | |||
with Diagram(name=self.name, show=False): | |||
c1 = Container("container1") | |||
c2 = Container("container2") | |||
c1 >> c2 | |||
def test_edges_with_labels(self): | |||
def test_edges_with_labels(self) -> None: | |||
with Diagram(name=self.name, show=False): | |||
c1 = Container("container1") | |||
c2 = Container("container2") | |||
@@ -51,14 +50,14 @@ class C4Test(unittest.TestCase): | |||
c1 >> Relationship("depends on") >> c2 | |||
c1 << Relationship("is depended on by") << c2 | |||
def test_edge_without_constraint(self): | |||
def test_edge_without_constraint(self) -> None: | |||
with Diagram(name=self.name, show=False): | |||
s1 = System("system 1") | |||
s2 = System("system 2") | |||
s1 >> Relationship(constraint="False") >> s2 | |||
s1 >> Relationship(constraint="False") >> s2 # type: ignore[arg-type] | |||
def test_cluster(self): | |||
def test_cluster(self) -> None: | |||
with Diagram(name=self.name, show=False): | |||
with SystemBoundary("System"): | |||
Container("container", "type", "description") |
@@ -3,15 +3,14 @@ import shutil | |||
import unittest | |||
import pathlib | |||
from diagrams import Cluster, Diagram, Edge, Node | |||
from diagrams import getcluster, getdiagram, setcluster, setdiagram | |||
from diagrams import Cluster, Diagram, Edge, Node, getcluster, getdiagram, setcluster, setdiagram | |||
class DiagramTest(unittest.TestCase): | |||
def setUp(self): | |||
self.name = "diagram_test" | |||
def setUp(self) -> None: | |||
self.name: str = "diagram_test" | |||
def tearDown(self): | |||
def tearDown(self) -> None: | |||
setdiagram(None) | |||
setcluster(None) | |||
# Only some tests generate the image file. | |||
@@ -20,11 +19,11 @@ class DiagramTest(unittest.TestCase): | |||
except OSError: | |||
# Consider it file | |||
try: | |||
os.remove(self.name + ".png") | |||
os.remove(f"{self.name}.png") | |||
except FileNotFoundError: | |||
pass | |||
def test_validate_direction(self): | |||
def test_validate_direction(self) -> None: | |||
# Normal directions. | |||
for dir in ("TB", "BT", "LR", "RL", "tb"): | |||
Diagram(direction=dir) | |||
@@ -34,7 +33,7 @@ class DiagramTest(unittest.TestCase): | |||
with self.assertRaises(ValueError): | |||
Diagram(direction=dir) | |||
def test_validate_curvestyle(self): | |||
def test_validate_curvestyle(self) -> None: | |||
# Normal directions. | |||
for cvs in ("ortho", "curved", "CURVED"): | |||
Diagram(curvestyle=cvs) | |||
@@ -44,7 +43,7 @@ class DiagramTest(unittest.TestCase): | |||
with self.assertRaises(ValueError): | |||
Diagram(curvestyle=cvs) | |||
def test_validate_outformat(self): | |||
def test_validate_outformat(self) -> None: | |||
# Normal output formats. | |||
for fmt in ("png", "jpg", "svg", "pdf", "PNG", "dot"): | |||
Diagram(outformat=fmt) | |||
@@ -54,18 +53,18 @@ class DiagramTest(unittest.TestCase): | |||
with self.assertRaises(ValueError): | |||
Diagram(outformat=fmt) | |||
def test_with_global_context(self): | |||
def test_with_global_context(self) -> None: | |||
self.assertIsNone(getdiagram()) | |||
with Diagram(name=os.path.join(self.name, "with_global_context"), show=False): | |||
self.assertIsNotNone(getdiagram()) | |||
self.assertIsNone(getdiagram()) | |||
def test_node_not_in_diagram(self): | |||
def test_node_not_in_diagram(self) -> None: | |||
# Node must be belong to a diagrams. | |||
with self.assertRaises(EnvironmentError): | |||
Node("node") | |||
def test_node_to_node(self): | |||
def test_node_to_node(self) -> None: | |||
with Diagram(name=os.path.join(self.name, "node_to_node"), show=False): | |||
node1 = Node("node1") | |||
node2 = Node("node2") | |||
@@ -73,7 +72,7 @@ class DiagramTest(unittest.TestCase): | |||
self.assertEqual(node1 >> node2, node2) | |||
self.assertEqual(node1 << node2, node2) | |||
def test_node_to_nodes(self): | |||
def test_node_to_nodes(self) -> None: | |||
with Diagram(name=os.path.join(self.name, "node_to_nodes"), show=False): | |||
node1 = Node("node1") | |||
nodes = [Node("node2"), Node("node3")] | |||
@@ -81,7 +80,7 @@ class DiagramTest(unittest.TestCase): | |||
self.assertEqual(node1 >> nodes, nodes) | |||
self.assertEqual(node1 << nodes, nodes) | |||
def test_nodes_to_node(self): | |||
def test_nodes_to_node(self) -> None: | |||
with Diagram(name=os.path.join(self.name, "nodes_to_node"), show=False): | |||
node1 = Node("node1") | |||
nodes = [Node("node2"), Node("node3")] | |||
@@ -89,32 +88,31 @@ class DiagramTest(unittest.TestCase): | |||
self.assertEqual(nodes >> node1, node1) | |||
self.assertEqual(nodes << node1, node1) | |||
def test_default_filename(self): | |||
def test_default_filename(self) -> None: | |||
self.name = "example_1" | |||
with Diagram(name="Example 1", show=False): | |||
Node("node1") | |||
self.assertTrue(os.path.exists(f"{self.name}.png")) | |||
def test_custom_filename(self): | |||
def test_custom_filename(self) -> None: | |||
self.name = "my_custom_name" | |||
with Diagram(name="Example 1", filename=self.name, show=False): | |||
Node("node1") | |||
self.assertTrue(os.path.exists(f"{self.name}.png")) | |||
def test_empty_name(self): | |||
def test_empty_name(self) -> None: | |||
"""Check that providing an empty name don't crash, but save in a diagrams_image.xxx file.""" | |||
self.name = 'diagrams_image' | |||
with Diagram(show=False): | |||
Node("node1") | |||
self.assertTrue(os.path.exists(f"{self.name}.png")) | |||
def test_autolabel(self): | |||
def test_autolabel(self) -> None: | |||
with Diagram(name=os.path.join(self.name, "nodes_to_node"), show=False): | |||
node1 = Node("node1") | |||
self.assertTrue(node1.label,"Node\nnode1") | |||
def test_outformat_list(self): | |||
def test_outformat_list(self) -> None: | |||
"""Check that outformat render all the files from the list.""" | |||
self.name = 'diagrams_image' | |||
with Diagram(show=False, outformat=["dot", "png"]): | |||
@@ -128,10 +126,10 @@ class DiagramTest(unittest.TestCase): | |||
class ClusterTest(unittest.TestCase): | |||
def setUp(self): | |||
self.name = "cluster_test" | |||
def setUp(self) -> None: | |||
self.name: str = "cluster_test" | |||
def tearDown(self): | |||
def tearDown(self) -> None: | |||
setdiagram(None) | |||
setcluster(None) | |||
# Only some tests generate the image file. | |||
@@ -140,7 +138,7 @@ class ClusterTest(unittest.TestCase): | |||
except OSError: | |||
pass | |||
def test_validate_direction(self): | |||
def test_validate_direction(self) -> None: | |||
# Normal directions. | |||
for dir in ("TB", "BT", "LR", "RL"): | |||
with Diagram(name=os.path.join(self.name, "validate_direction"), show=False): | |||
@@ -152,14 +150,14 @@ class ClusterTest(unittest.TestCase): | |||
with Diagram(name=os.path.join(self.name, "validate_direction"), show=False): | |||
Cluster(direction=dir) | |||
def test_with_global_context(self): | |||
def test_with_global_context(self) -> None: | |||
with Diagram(name=os.path.join(self.name, "with_global_context"), show=False): | |||
self.assertIsNone(getcluster()) | |||
with Cluster(): | |||
self.assertIsNotNone(getcluster()) | |||
self.assertIsNone(getcluster()) | |||
def test_with_nested_cluster(self): | |||
def test_with_nested_cluster(self) -> None: | |||
with Diagram(name=os.path.join(self.name, "with_nested_cluster"), show=False): | |||
self.assertIsNone(getcluster()) | |||
with Cluster() as c1: | |||
@@ -169,12 +167,12 @@ class ClusterTest(unittest.TestCase): | |||
self.assertEqual(c1, getcluster()) | |||
self.assertIsNone(getcluster()) | |||
def test_node_not_in_diagram(self): | |||
def test_node_not_in_diagram(self) -> None: | |||
# Node must be belong to a diagrams. | |||
with self.assertRaises(EnvironmentError): | |||
Node("node") | |||
def test_node_to_node(self): | |||
def test_node_to_node(self) -> None: | |||
with Diagram(name=os.path.join(self.name, "node_to_node"), show=False): | |||
with Cluster(): | |||
node1 = Node("node1") | |||
@@ -183,7 +181,7 @@ class ClusterTest(unittest.TestCase): | |||
self.assertEqual(node1 >> node2, node2) | |||
self.assertEqual(node1 << node2, node2) | |||
def test_node_to_nodes(self): | |||
def test_node_to_nodes(self) -> None: | |||
with Diagram(name=os.path.join(self.name, "node_to_nodes"), show=False): | |||
with Cluster(): | |||
node1 = Node("node1") | |||
@@ -192,7 +190,7 @@ class ClusterTest(unittest.TestCase): | |||
self.assertEqual(node1 >> nodes, nodes) | |||
self.assertEqual(node1 << nodes, nodes) | |||
def test_nodes_to_node(self): | |||
def test_nodes_to_node(self) -> None: | |||
with Diagram(name=os.path.join(self.name, "nodes_to_node"), show=False): | |||
with Cluster(): | |||
node1 = Node("node1") | |||
@@ -203,10 +201,10 @@ class ClusterTest(unittest.TestCase): | |||
class EdgeTest(unittest.TestCase): | |||
def setUp(self): | |||
self.name = "edge_test" | |||
def setUp(self) -> None: | |||
self.name: str = "edge_test" | |||
def tearDown(self): | |||
def tearDown(self) -> None: | |||
setdiagram(None) | |||
setcluster(None) | |||
# Only some tests generate the image file. | |||
@@ -215,34 +213,34 @@ class EdgeTest(unittest.TestCase): | |||
except OSError: | |||
pass | |||
def test_node_to_node(self): | |||
def test_node_to_node(self) -> None: | |||
with Diagram(name=os.path.join(self.name, "node_to_node"), show=False): | |||
node1 = Node("node1") | |||
node2 = Node("node2") | |||
self.assertEqual(node1 - Edge(color="red") - node2, node2) | |||
def test_node_to_nodes(self): | |||
def test_node_to_nodes(self) -> None: | |||
with Diagram(name=os.path.join(self.name, "node_to_nodes"), show=False): | |||
with Cluster(): | |||
node1 = Node("node1") | |||
nodes = [Node("node2"), Node("node3")] | |||
self.assertEqual(node1 - Edge(color="red") - nodes, nodes) | |||
self.assertEqual(node1 - Edge(color="red") - nodes, nodes) # type: ignore[operator] | |||
def test_nodes_to_node(self): | |||
def test_nodes_to_node(self) -> None: | |||
with Diagram(name=os.path.join(self.name, "nodes_to_node"), show=False): | |||
with Cluster(): | |||
node1 = Node("node1") | |||
nodes = [Node("node2"), Node("node3")] | |||
self.assertEqual(nodes - Edge(color="red") - node1, node1) | |||
def test_nodes_to_node_with_additional_attributes(self): | |||
def test_nodes_to_node_with_additional_attributes(self) -> None: | |||
with Diagram(name=os.path.join(self.name, "nodes_to_node_with_additional_attributes"), show=False): | |||
with Cluster(): | |||
node1 = Node("node1") | |||
nodes = [Node("node2"), Node("node3")] | |||
self.assertEqual(nodes - Edge(color="red") - Edge(color="green") - node1, node1) | |||
def test_node_to_node_with_attributes(self): | |||
def test_node_to_node_with_attributes(self) -> None: | |||
with Diagram(name=os.path.join(self.name, "node_to_node_with_attributes"), show=False): | |||
with Cluster(): | |||
node1 = Node("node1") | |||
@@ -251,7 +249,7 @@ class EdgeTest(unittest.TestCase): | |||
self.assertEqual(node1 >> Edge(color="green", label="1.2") >> node2, node2) | |||
self.assertEqual(node1 << Edge(color="blue", label="1.3") >> node2, node2) | |||
def test_node_to_node_with_additional_attributes(self): | |||
def test_node_to_node_with_additional_attributes(self) -> None: | |||
with Diagram(name=os.path.join(self.name, "node_to_node_with_additional_attributes"), show=False): | |||
with Cluster(): | |||
node1 = Node("node1") | |||
@@ -260,7 +258,7 @@ class EdgeTest(unittest.TestCase): | |||
self.assertEqual(node1 >> Edge(color="green", label="2.2") >> Edge(color="red") >> node2, node2) | |||
self.assertEqual(node1 << Edge(color="blue", label="2.3") >> Edge(color="black") >> node2, node2) | |||
def test_nodes_to_node_with_attributes_loop(self): | |||
def test_nodes_to_node_with_attributes_loop(self) -> None: | |||
with Diagram(name=os.path.join(self.name, "nodes_to_node_with_attributes_loop"), show=False): | |||
with Cluster(): | |||
node = Node("node") | |||
@@ -269,21 +267,21 @@ class EdgeTest(unittest.TestCase): | |||
self.assertEqual(node >> Edge(color="blue", label="3.3") << node, node) | |||
self.assertEqual(node << Edge(color="pink", label="3.4") >> node, node) | |||
def test_nodes_to_node_with_attributes_bothdirectional(self): | |||
def test_nodes_to_node_with_attributes_bothdirectional(self) -> None: | |||
with Diagram(name=os.path.join(self.name, "nodes_to_node_with_attributes_bothdirectional"), show=False): | |||
with Cluster(): | |||
node1 = Node("node1") | |||
nodes = [Node("node2"), Node("node3")] | |||
self.assertEqual(nodes << Edge(color="green", label="4") >> node1, node1) | |||
def test_nodes_to_node_with_attributes_bidirectional(self): | |||
def test_nodes_to_node_with_attributes_bidirectional(self) -> None: | |||
with Diagram(name=os.path.join(self.name, "nodes_to_node_with_attributes_bidirectional"), show=False): | |||
with Cluster(): | |||
node1 = Node("node1") | |||
nodes = [Node("node2"), Node("node3")] | |||
self.assertEqual(nodes << Edge(color="blue", label="5") >> node1, node1) | |||
def test_nodes_to_node_with_attributes_onedirectional(self): | |||
def test_nodes_to_node_with_attributes_onedirectional(self) -> None: | |||
with Diagram(name=os.path.join(self.name, "nodes_to_node_with_attributes_onedirectional"), show=False): | |||
with Cluster(): | |||
node1 = Node("node1") | |||
@@ -291,7 +289,7 @@ class EdgeTest(unittest.TestCase): | |||
self.assertEqual(nodes >> Edge(color="red", label="6.1") >> node1, node1) | |||
self.assertEqual(nodes << Edge(color="green", label="6.2") << node1, node1) | |||
def test_nodes_to_node_with_additional_attributes_directional(self): | |||
def test_nodes_to_node_with_additional_attributes_directional(self) -> None: | |||
with Diagram(name=os.path.join(self.name, "nodes_to_node_with_additional_attributes_directional"), show=False): | |||
with Cluster(): | |||
node1 = Node("node1") | |||
@@ -305,7 +303,7 @@ class EdgeTest(unittest.TestCase): | |||
class ResourcesTest(unittest.TestCase): | |||
def test_folder_depth(self): | |||
def test_folder_depth(self) -> None: | |||
""" | |||
The code currently only handles resource folders up to a dir depth of 2 | |||
i.e. resources/<provider>/<type>/<image>, so check that this depth isn't | |||