@@ -33,3 +33,5 @@ jobs: | |||||
run: | | run: | | ||||
poetry install | poetry install | ||||
poetry run python -m unittest -v tests/*.py | poetry run python -m unittest -v tests/*.py | ||||
- name: Run mypy | |||||
run: poetry run mypy |
@@ -1,49 +1,44 @@ | |||||
import contextvars | |||||
import os | import os | ||||
import uuid | import uuid | ||||
from contextvars import ContextVar | |||||
from pathlib import Path | 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. | # Global contexts for a diagrams and a cluster. | ||||
# | # | ||||
# These global contexts are for letting the clusters and nodes know | # These global contexts are for letting the clusters and nodes know | ||||
# where context they are belong to. So the all clusters and nodes does | # where context they are belong to. So the all clusters and nodes does | ||||
# not need to specify the current diagrams or cluster via parameters. | # 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) | __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) | __cluster.set(cluster) | ||||
class Diagram: | 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 | # fmt: off | ||||
_default_graph_attrs = { | |||||
_default_graph_attrs: Mapping[str, str] = { | |||||
"pad": "2.0", | "pad": "2.0", | ||||
"splines": "ortho", | "splines": "ortho", | ||||
"nodesep": "0.60", | "nodesep": "0.60", | ||||
@@ -52,7 +47,7 @@ class Diagram: | |||||
"fontsize": "15", | "fontsize": "15", | ||||
"fontcolor": "#2D3436", | "fontcolor": "#2D3436", | ||||
} | } | ||||
_default_node_attrs = { | |||||
_default_node_attrs: Mapping[str, str] = { | |||||
"shape": "box", | "shape": "box", | ||||
"style": "rounded", | "style": "rounded", | ||||
"fixedsize": "true", | "fixedsize": "true", | ||||
@@ -68,7 +63,7 @@ class Diagram: | |||||
"fontsize": "13", | "fontsize": "13", | ||||
"fontcolor": "#2D3436", | "fontcolor": "#2D3436", | ||||
} | } | ||||
_default_edge_attrs = { | |||||
_default_edge_attrs: Mapping[str, str] = { | |||||
"color": "#7B8894", | "color": "#7B8894", | ||||
} | } | ||||
@@ -82,13 +77,13 @@ class Diagram: | |||||
filename: str = "", | filename: str = "", | ||||
direction: str = "LR", | direction: str = "LR", | ||||
curvestyle: str = "ortho", | curvestyle: str = "ortho", | ||||
outformat: str = "png", | |||||
outformat: Union[List[str], str] = "png", | |||||
autolabel: bool = False, | autolabel: bool = False, | ||||
show: bool = True, | show: bool = True, | ||||
strict: bool = False, | 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. | """Diagram represents a global diagrams context. | ||||
@@ -116,8 +111,8 @@ class Diagram: | |||||
filename = "diagrams_image" | filename = "diagrams_image" | ||||
elif not filename: | elif not filename: | ||||
filename = "_".join(self.name.split()).lower() | 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. | # Set attributes. | ||||
for k, v in self._default_graph_attrs.items(): | for k, v in self._default_graph_attrs.items(): | ||||
@@ -143,7 +138,7 @@ class Diagram: | |||||
else: | else: | ||||
if not self._validate_outformat(outformat): | if not self._validate_outformat(outformat): | ||||
raise ValueError(f'"{outformat}" is not a valid output format') | raise ValueError(f'"{outformat}" is not a valid output format') | ||||
self.outformat = outformat | |||||
self.outformat: Union[List[str], str] = outformat | |||||
# Merge passed in attributes | # Merge passed in attributes | ||||
self.dot.graph_attr.update(graph_attr) | self.dot.graph_attr.update(graph_attr) | ||||
@@ -156,18 +151,23 @@ class Diagram: | |||||
def __str__(self) -> str: | def __str__(self) -> str: | ||||
return str(self.dot) | return str(self.dot) | ||||
def __enter__(self): | |||||
def __enter__(self) -> "Diagram": | |||||
setdiagram(self) | setdiagram(self) | ||||
return 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() | self.render() | ||||
# Remove the graphviz file leaving only the image. | # Remove the graphviz file leaving only the image. | ||||
os.remove(self.filename) | os.remove(self.filename) | ||||
setdiagram(None) | 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: | def _validate_direction(self, direction: str) -> bool: | ||||
return direction.upper() in self.__directions | return direction.upper() in self.__directions | ||||
@@ -178,7 +178,7 @@ class Diagram: | |||||
def _validate_outformat(self, outformat: str) -> bool: | def _validate_outformat(self, outformat: str) -> bool: | ||||
return outformat.lower() in self.__outformats | 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.""" | """Create a new node.""" | ||||
self.dot.node(nodeid, label=label, **attrs) | self.dot.node(nodeid, label=label, **attrs) | ||||
@@ -199,11 +199,11 @@ class Diagram: | |||||
class Cluster: | 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 | # fmt: off | ||||
_default_graph_attrs = { | |||||
_default_graph_attrs: Mapping[str, str] = { | |||||
"shape": "box", | "shape": "box", | ||||
"style": "rounded", | "style": "rounded", | ||||
"labeljust": "l", | "labeljust": "l", | ||||
@@ -221,7 +221,7 @@ class Cluster: | |||||
self, | self, | ||||
label: str = "cluster", | label: str = "cluster", | ||||
direction: str = "LR", | direction: str = "LR", | ||||
graph_attr: Optional[dict] = None, | |||||
graph_attr: Optional[Mapping[str, Any]] = None, | |||||
): | ): | ||||
"""Cluster represents a cluster context. | """Cluster represents a cluster context. | ||||
@@ -231,10 +231,10 @@ class Cluster: | |||||
""" | """ | ||||
if graph_attr is None: | if graph_attr is None: | ||||
graph_attr = {} | 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. | # Set attributes. | ||||
for k, v in self._default_graph_attrs.items(): | for k, v in self._default_graph_attrs.items(): | ||||
@@ -246,24 +246,30 @@ class Cluster: | |||||
self.dot.graph_attr["rankdir"] = direction | self.dot.graph_attr["rankdir"] = direction | ||||
# Node must be belong to a diagrams. | # 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") | 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 | # 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) | coloridx = self.depth % len(self.__bgcolors) | ||||
self.dot.graph_attr["bgcolor"] = self.__bgcolors[coloridx] | self.dot.graph_attr["bgcolor"] = self.__bgcolors[coloridx] | ||||
# Merge passed in attributes | # Merge passed in attributes | ||||
self.dot.graph_attr.update(graph_attr) | self.dot.graph_attr.update(graph_attr) | ||||
def __enter__(self): | |||||
def __enter__(self) -> "Cluster": | |||||
setcluster(self) | setcluster(self) | ||||
return 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: | if self._parent: | ||||
self._parent.subgraph(self.dot) | self._parent.subgraph(self.dot) | ||||
else: | else: | ||||
@@ -273,7 +279,7 @@ class Cluster: | |||||
def _validate_direction(self, direction: str) -> bool: | def _validate_direction(self, direction: str) -> bool: | ||||
return direction.upper() in self.__directions | 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.""" | """Create a new node in the cluster.""" | ||||
self.dot.node(nodeid, label=label, **attrs) | self.dot.node(nodeid, label=label, **attrs) | ||||
@@ -284,32 +290,33 @@ class Cluster: | |||||
class Node: | class Node: | ||||
"""Node represents a node for a specific backend service.""" | """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. | """Node represents a system component. | ||||
:param label: Node label. | :param label: Node label. | ||||
""" | """ | ||||
# Generates an ID for identifying a node, unless specified | # 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. | # 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") | raise EnvironmentError("Global diagrams context not set up") | ||||
self._diagram: Diagram = diagram | |||||
if self._diagram.autolabel: | if self._diagram.autolabel: | ||||
prefix = self.__class__.__name__ | prefix = self.__class__.__name__ | ||||
if self.label: | if self.label: | ||||
self.label = prefix + "\n" + self.label | |||||
self.label = f"{prefix}\n{self.label}" | |||||
else: | else: | ||||
self.label = prefix | self.label = prefix | ||||
@@ -318,16 +325,17 @@ class Node: | |||||
# that label being spanned between icon image and white space. | # that label being spanned between icon image and white space. | ||||
# Increase the height by the number of new lines included in the label. | # Increase the height by the number of new lines included in the label. | ||||
padding = 0.4 * (self.label.count('\n')) | padding = 0.4 * (self.label.count('\n')) | ||||
self._attrs = { | |||||
icon = self._load_icon() | |||||
self._attrs: Dict[str, Any] = { | |||||
"shape": "none", | "shape": "none", | ||||
"height": str(self._height + padding), | "height": str(self._height + padding), | ||||
"image": self._load_icon(), | |||||
} if self._icon else {} | |||||
"image": icon, | |||||
} if icon is not None else {} | |||||
# fmt: on | # fmt: on | ||||
self._attrs.update(attrs) | 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 a node is in the cluster context, add it to cluster. | ||||
if self._cluster: | if self._cluster: | ||||
@@ -335,23 +343,22 @@ class Node: | |||||
else: | else: | ||||
self._diagram.node(self._id, self.label, **self._attrs) | 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.""" | """Implement Self - Node, Self - [Nodes] and Self - Edge.""" | ||||
if isinstance(other, list): | if isinstance(other, list): | ||||
for node in other: | for node in other: | ||||
self.connect(node, Edge(self)) | self.connect(node, Edge(self)) | ||||
return other | return other | ||||
elif isinstance(other, Node): | |||||
if isinstance(other, Node): | |||||
return self.connect(other, Edge(self)) | 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.""" | """Called for [Nodes] and [Edges] - Self because list don't have __sub__ operators.""" | ||||
for o in other: | for o in other: | ||||
if isinstance(o, Edge): | if isinstance(o, Edge): | ||||
@@ -360,32 +367,30 @@ class Node: | |||||
o.connect(self, Edge(self)) | o.connect(self, Edge(self)) | ||||
return 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.""" | """Implements Self >> Node, Self >> [Nodes] and Self Edge.""" | ||||
if isinstance(other, list): | if isinstance(other, list): | ||||
for node in other: | for node in other: | ||||
self.connect(node, Edge(self, forward=True)) | self.connect(node, Edge(self, forward=True)) | ||||
return other | return other | ||||
elif isinstance(other, Node): | |||||
if isinstance(other, Node): | |||||
return self.connect(other, Edge(self, forward=True)) | 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.""" | """Implements Self << Node, Self << [Nodes] and Self << Edge.""" | ||||
if isinstance(other, list): | if isinstance(other, list): | ||||
for node in other: | for node in other: | ||||
self.connect(node, Edge(self, reverse=True)) | self.connect(node, Edge(self, reverse=True)) | ||||
return other | return other | ||||
elif isinstance(other, Node): | |||||
if isinstance(other, Node): | |||||
return self.connect(other, Edge(self, reverse=True)) | 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.""" | """Called for [Nodes] and [Edges] >> Self because list don't have __rshift__ operators.""" | ||||
for o in other: | for o in other: | ||||
if isinstance(o, Edge): | if isinstance(o, Edge): | ||||
@@ -395,7 +400,7 @@ class Node: | |||||
o.connect(self, Edge(self, forward=True)) | o.connect(self, Edge(self, forward=True)) | ||||
return self | 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.""" | """Called for [Nodes] << Self because list of Nodes don't have __lshift__ operators.""" | ||||
for o in other: | for o in other: | ||||
if isinstance(o, Edge): | if isinstance(o, Edge): | ||||
@@ -406,11 +411,11 @@ class Node: | |||||
return self | return self | ||||
@property | @property | ||||
def nodeid(self): | |||||
def nodeid(self) -> str: | |||||
return self._id | return self._id | ||||
# TODO: option for adding flow description to the connection edge | # 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. | """Connect to other node. | ||||
:param node: Other node instance. | :param node: Other node instance. | ||||
@@ -426,18 +431,19 @@ class Node: | |||||
return node | return node | ||||
@staticmethod | @staticmethod | ||||
def _rand_id(): | |||||
def _rand_id() -> str: | |||||
return uuid.uuid4().hex | 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: | class Edge: | ||||
"""Edge represents an edge between two nodes.""" | """Edge represents an edge between two nodes.""" | ||||
_default_edge_attrs = { | |||||
_default_edge_attrs: Mapping[str, str] = { | |||||
"fontcolor": "#2D3436", | "fontcolor": "#2D3436", | ||||
"fontname": "Sans-Serif", | "fontname": "Sans-Serif", | ||||
"fontsize": "13", | "fontsize": "13", | ||||
@@ -445,13 +451,13 @@ class Edge: | |||||
def __init__( | def __init__( | ||||
self, | self, | ||||
node: "Node" = None, | |||||
node: Optional["Node"] = None, | |||||
forward: bool = False, | forward: bool = False, | ||||
reverse: bool = False, | reverse: bool = False, | ||||
label: str = "", | label: str = "", | ||||
color: str = "", | color: str = "", | ||||
style: str = "", | style: str = "", | ||||
**attrs: Dict, | |||||
**attrs: Dict[Any, Any], | |||||
): | ): | ||||
"""Edge represents an edge between two nodes. | """Edge represents an edge between two nodes. | ||||
@@ -466,11 +472,11 @@ class Edge: | |||||
if node is not None: | if node is not None: | ||||
assert isinstance(node, Node) | 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. | # Set attributes. | ||||
for k, v in self._default_edge_attrs.items(): | for k, v in self._default_edge_attrs.items(): | ||||
@@ -486,7 +492,7 @@ class Edge: | |||||
self._attrs["style"] = style | self._attrs["style"] = style | ||||
self._attrs.update(attrs) | 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]""" | """Implement Self - Node or Edge and Self - [Nodes]""" | ||||
return self.connect(other) | return self.connect(other) | ||||
@@ -494,12 +500,12 @@ class Edge: | |||||
"""Called for [Nodes] or [Edges] - Self because list don't have __sub__ operators.""" | """Called for [Nodes] or [Edges] - Self because list don't have __sub__ operators.""" | ||||
return self.append(other) | 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].""" | """Implements Self >> Node or Edge and Self >> [Nodes].""" | ||||
self.forward = True | self.forward = True | ||||
return self.connect(other) | 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].""" | """Implements Self << Node or Edge and Self << [Nodes].""" | ||||
self.reverse = True | self.reverse = True | ||||
return self.connect(other) | 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.""" | """Called for [Nodes] or [Edges] << Self because list of Edges don't have __lshift__ operators.""" | ||||
return self.append(other, reverse=True) | 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 = [] | result = [] | ||||
for o in other: | for o in other: | ||||
if isinstance(o, Edge): | if isinstance(o, Edge): | ||||
o.forward = forward if forward else o.forward | 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() | self._attrs = o.attrs.copy() | ||||
result.append(o) | result.append(o) | ||||
else: | 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 | 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): | 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 | return other | ||||
elif isinstance(other, Edge): | |||||
if isinstance(other, Edge): | |||||
self._attrs = other._attrs.copy() | self._attrs = other._attrs.copy() | ||||
return self | 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 | @property | ||||
def attrs(self) -> Dict: | |||||
def attrs(self) -> Dict[str, Any]: | |||||
if self.forward and self.reverse: | if self.forward and self.reverse: | ||||
direction = "both" | direction = "both" | ||||
elif self.forward: | elif self.forward: | ||||
@@ -552,4 +559,4 @@ class Edge: | |||||
return {**self._attrs, "dir": direction} | return {**self._attrs, "dir": direction} | ||||
Group = Cluster | |||||
Group: Type[Cluster] = Cluster |
@@ -102,8 +102,3 @@ class User(_General): | |||||
class Users(_General): | class Users(_General): | ||||
_icon = "users.png" | _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 html | ||||
import textwrap | import textwrap | ||||
from typing import Any, Dict | |||||
from diagrams import Cluster, Node, Edge | 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""" | """Create a graphviz label string for a C4 node""" | ||||
title = f'<font point-size="12"><b>{html.escape(name)}</b></font><br/>' | 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 "" | 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}>" | 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. | Formats the description string so it fits into the C4 nodes. | ||||
@@ -29,7 +31,7 @@ def _format_description(description): | |||||
return "<br/>".join(lines) | 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""" | """Create a graphviz label string for a C4 edge""" | ||||
wrapper = textwrap.TextWrapper(width=24, max_lines=3) | wrapper = textwrap.TextWrapper(width=24, max_lines=3) | ||||
lines = [html.escape(line) for line in wrapper.wrap(description)] | 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>>' | 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 | key = f"{type}: {technology}" if technology else type | ||||
node_attributes = { | |||||
node_attributes: Dict[str, Any] = { | |||||
"label": _format_node_label(name, key, description), | "label": _format_node_label(name, key, description), | ||||
"labelloc": "c", | "labelloc": "c", | ||||
"shape": "rect", | "shape": "rect", | ||||
@@ -57,8 +59,8 @@ def C4Node(name, technology="", description="", type="Container", **kwargs): | |||||
return Node(**node_attributes) | 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, | "name": name, | ||||
"technology": technology, | "technology": technology, | ||||
"description": description, | "description": description, | ||||
@@ -68,8 +70,8 @@ def Container(name, technology="", description="", **kwargs): | |||||
return C4Node(**container_attributes) | 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, | "name": name, | ||||
"technology": technology, | "technology": technology, | ||||
"description": description, | "description": description, | ||||
@@ -81,8 +83,8 @@ def Database(name, technology="", description="", **kwargs): | |||||
return C4Node(**database_attributes) | 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, | "name": name, | ||||
"description": description, | "description": description, | ||||
"type": "External System" if external else "System", | "type": "External System" if external else "System", | ||||
@@ -92,8 +94,8 @@ def System(name, description="", external=False, **kwargs): | |||||
return C4Node(**system_attributes) | 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, | "name": name, | ||||
"description": description, | "description": description, | ||||
"type": "External Person" if external else "Person", | "type": "External Person" if external else "Person", | ||||
@@ -104,8 +106,8 @@ def Person(name, description="", external=False, **kwargs): | |||||
return C4Node(**person_attributes) | 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), | "label": html.escape(name), | ||||
"bgcolor": "white", | "bgcolor": "white", | ||||
"margin": "16", | "margin": "16", | ||||
@@ -115,8 +117,8 @@ def SystemBoundary(name, **kwargs): | |||||
return Cluster(name, graph_attr=graph_attributes) | 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", | "style": "dashed", | ||||
"color": "gray60", | "color": "gray60", | ||||
"label": _format_edge_label(label) if label else "", | "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. | 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 | from diagrams import Node | ||||
@@ -12,9 +14,11 @@ class Custom(Node): | |||||
fontcolor = "#ffffff" | fontcolor = "#ffffff" | ||||
def _load_icon(self): | |||||
@override | |||||
def _load_icon(self) -> Optional[str]: | |||||
return self._icon | 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]] | [[package]] | ||||
name = "astroid" | name = "astroid" | ||||
version = "2.9.0" | version = "2.9.0" | ||||
description = "An abstract syntax tree for Python with inference support." | description = "An abstract syntax tree for Python with inference support." | ||||
category = "dev" | |||||
optional = false | optional = false | ||||
python-versions = "~=3.6" | python-versions = "~=3.6" | ||||
files = [ | files = [ | ||||
@@ -23,7 +22,6 @@ wrapt = ">=1.11,<1.14" | |||||
name = "black" | name = "black" | ||||
version = "22.12.0" | version = "22.12.0" | ||||
description = "The uncompromising code formatter." | description = "The uncompromising code formatter." | ||||
category = "dev" | |||||
optional = false | optional = false | ||||
python-versions = ">=3.7" | python-versions = ">=3.7" | ||||
files = [ | files = [ | ||||
@@ -60,7 +58,6 @@ uvloop = ["uvloop (>=0.15.2)"] | |||||
name = "click" | name = "click" | ||||
version = "8.1.3" | version = "8.1.3" | ||||
description = "Composable command line interface toolkit" | description = "Composable command line interface toolkit" | ||||
category = "dev" | |||||
optional = false | optional = false | ||||
python-versions = ">=3.7" | python-versions = ">=3.7" | ||||
files = [ | files = [ | ||||
@@ -76,7 +73,6 @@ importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} | |||||
name = "colorama" | name = "colorama" | ||||
version = "0.4.3" | version = "0.4.3" | ||||
description = "Cross-platform colored terminal text." | description = "Cross-platform colored terminal text." | ||||
category = "dev" | |||||
optional = false | optional = false | ||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" | ||||
files = [ | files = [ | ||||
@@ -88,7 +84,6 @@ files = [ | |||||
name = "exceptiongroup" | name = "exceptiongroup" | ||||
version = "1.1.0" | version = "1.1.0" | ||||
description = "Backport of PEP 654 (exception groups)" | description = "Backport of PEP 654 (exception groups)" | ||||
category = "dev" | |||||
optional = false | optional = false | ||||
python-versions = ">=3.7" | python-versions = ">=3.7" | ||||
files = [ | files = [ | ||||
@@ -103,7 +98,6 @@ test = ["pytest (>=6)"] | |||||
name = "graphviz" | name = "graphviz" | ||||
version = "0.20.1" | version = "0.20.1" | ||||
description = "Simple Python interface for Graphviz" | description = "Simple Python interface for Graphviz" | ||||
category = "main" | |||||
optional = false | optional = false | ||||
python-versions = ">=3.7" | python-versions = ">=3.7" | ||||
files = [ | files = [ | ||||
@@ -120,7 +114,6 @@ test = ["coverage", "mock (>=4)", "pytest (>=7)", "pytest-cov", "pytest-mock (>= | |||||
name = "importlib-metadata" | name = "importlib-metadata" | ||||
version = "1.5.0" | version = "1.5.0" | ||||
description = "Read metadata from Python packages" | description = "Read metadata from Python packages" | ||||
category = "dev" | |||||
optional = false | optional = false | ||||
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" | ||||
files = [ | files = [ | ||||
@@ -139,7 +132,6 @@ testing = ["importlib-resources", "packaging"] | |||||
name = "iniconfig" | name = "iniconfig" | ||||
version = "1.1.1" | version = "1.1.1" | ||||
description = "iniconfig: brain-dead simple config-ini parsing" | description = "iniconfig: brain-dead simple config-ini parsing" | ||||
category = "dev" | |||||
optional = false | optional = false | ||||
python-versions = "*" | python-versions = "*" | ||||
files = [ | files = [ | ||||
@@ -151,7 +143,6 @@ files = [ | |||||
name = "isort" | name = "isort" | ||||
version = "4.3.21" | version = "4.3.21" | ||||
description = "A Python utility / library to sort Python imports." | description = "A Python utility / library to sort Python imports." | ||||
category = "dev" | |||||
optional = false | optional = false | ||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" | ||||
files = [ | files = [ | ||||
@@ -169,7 +160,6 @@ xdg-home = ["appdirs (>=1.4.0)"] | |||||
name = "jinja2" | name = "jinja2" | ||||
version = "3.1.2" | version = "3.1.2" | ||||
description = "A very fast and expressive template engine." | description = "A very fast and expressive template engine." | ||||
category = "main" | |||||
optional = false | optional = false | ||||
python-versions = ">=3.7" | python-versions = ">=3.7" | ||||
files = [ | files = [ | ||||
@@ -187,7 +177,6 @@ i18n = ["Babel (>=2.7)"] | |||||
name = "lazy-object-proxy" | name = "lazy-object-proxy" | ||||
version = "1.4.3" | version = "1.4.3" | ||||
description = "A fast and thorough lazy object proxy." | description = "A fast and thorough lazy object proxy." | ||||
category = "dev" | |||||
optional = false | optional = false | ||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" | ||||
files = [ | files = [ | ||||
@@ -218,7 +207,6 @@ files = [ | |||||
name = "markupsafe" | name = "markupsafe" | ||||
version = "2.0.1" | version = "2.0.1" | ||||
description = "Safely add untrusted strings to HTML/XML markup." | description = "Safely add untrusted strings to HTML/XML markup." | ||||
category = "main" | |||||
optional = false | optional = false | ||||
python-versions = ">=3.6" | python-versions = ">=3.6" | ||||
files = [ | files = [ | ||||
@@ -297,7 +285,6 @@ files = [ | |||||
name = "mccabe" | name = "mccabe" | ||||
version = "0.6.1" | version = "0.6.1" | ||||
description = "McCabe checker, plugin for flake8" | description = "McCabe checker, plugin for flake8" | ||||
category = "dev" | |||||
optional = false | optional = false | ||||
python-versions = "*" | python-versions = "*" | ||||
files = [ | files = [ | ||||
@@ -306,22 +293,67 @@ files = [ | |||||
] | ] | ||||
[[package]] | [[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" | 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 | optional = false | ||||
python-versions = "*" | |||||
python-versions = ">=3.5" | |||||
files = [ | 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]] | [[package]] | ||||
name = "packaging" | name = "packaging" | ||||
version = "20.8" | version = "20.8" | ||||
description = "Core utilities for Python packages" | description = "Core utilities for Python packages" | ||||
category = "dev" | |||||
optional = false | optional = false | ||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" | ||||
files = [ | files = [ | ||||
@@ -336,7 +368,6 @@ pyparsing = ">=2.0.2" | |||||
name = "pathspec" | name = "pathspec" | ||||
version = "0.10.1" | version = "0.10.1" | ||||
description = "Utility library for gitignore style pattern matching of file paths." | description = "Utility library for gitignore style pattern matching of file paths." | ||||
category = "dev" | |||||
optional = false | optional = false | ||||
python-versions = ">=3.7" | python-versions = ">=3.7" | ||||
files = [ | files = [ | ||||
@@ -348,7 +379,6 @@ files = [ | |||||
name = "platformdirs" | name = "platformdirs" | ||||
version = "2.4.0" | version = "2.4.0" | ||||
description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." | description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." | ||||
category = "dev" | |||||
optional = false | optional = false | ||||
python-versions = ">=3.6" | python-versions = ">=3.6" | ||||
files = [ | files = [ | ||||
@@ -364,7 +394,6 @@ test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock | |||||
name = "pluggy" | name = "pluggy" | ||||
version = "0.13.1" | version = "0.13.1" | ||||
description = "plugin and hook calling mechanisms for python" | description = "plugin and hook calling mechanisms for python" | ||||
category = "dev" | |||||
optional = false | optional = false | ||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" | ||||
files = [ | files = [ | ||||
@@ -382,7 +411,6 @@ dev = ["pre-commit", "tox"] | |||||
name = "pylint" | name = "pylint" | ||||
version = "2.12.0" | version = "2.12.0" | ||||
description = "python code static checker" | description = "python code static checker" | ||||
category = "dev" | |||||
optional = false | optional = false | ||||
python-versions = "~=3.6" | python-versions = "~=3.6" | ||||
files = [ | files = [ | ||||
@@ -403,7 +431,6 @@ typing-extensions = {version = ">=3.10.0", markers = "python_version < \"3.10\"" | |||||
name = "pyparsing" | name = "pyparsing" | ||||
version = "2.4.7" | version = "2.4.7" | ||||
description = "Python parsing module" | description = "Python parsing module" | ||||
category = "dev" | |||||
optional = false | optional = false | ||||
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" | python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" | ||||
files = [ | files = [ | ||||
@@ -415,7 +442,6 @@ files = [ | |||||
name = "pytest" | name = "pytest" | ||||
version = "7.3.0" | version = "7.3.0" | ||||
description = "pytest: simple powerful testing with Python" | description = "pytest: simple powerful testing with Python" | ||||
category = "dev" | |||||
optional = false | optional = false | ||||
python-versions = ">=3.7" | python-versions = ">=3.7" | ||||
files = [ | files = [ | ||||
@@ -439,7 +465,6 @@ testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "no | |||||
name = "rope" | name = "rope" | ||||
version = "0.14.0" | version = "0.14.0" | ||||
description = "a python refactoring library..." | description = "a python refactoring library..." | ||||
category = "dev" | |||||
optional = false | optional = false | ||||
python-versions = "*" | python-versions = "*" | ||||
files = [ | files = [ | ||||
@@ -452,7 +477,6 @@ files = [ | |||||
name = "setuptools" | name = "setuptools" | ||||
version = "59.6.0" | version = "59.6.0" | ||||
description = "Easily download, build, install, upgrade, and uninstall Python packages" | description = "Easily download, build, install, upgrade, and uninstall Python packages" | ||||
category = "dev" | |||||
optional = false | optional = false | ||||
python-versions = ">=3.6" | python-versions = ">=3.6" | ||||
files = [ | files = [ | ||||
@@ -468,7 +492,6 @@ testing = ["flake8-2020", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "mock" | |||||
name = "toml" | name = "toml" | ||||
version = "0.10.0" | version = "0.10.0" | ||||
description = "Python Library for Tom's Obvious, Minimal Language" | description = "Python Library for Tom's Obvious, Minimal Language" | ||||
category = "dev" | |||||
optional = false | optional = false | ||||
python-versions = "*" | python-versions = "*" | ||||
files = [ | files = [ | ||||
@@ -480,7 +503,6 @@ files = [ | |||||
name = "tomli" | name = "tomli" | ||||
version = "1.2.3" | version = "1.2.3" | ||||
description = "A lil' TOML parser" | description = "A lil' TOML parser" | ||||
category = "dev" | |||||
optional = false | optional = false | ||||
python-versions = ">=3.6" | python-versions = ">=3.6" | ||||
files = [ | files = [ | ||||
@@ -492,7 +514,6 @@ files = [ | |||||
name = "typed-ast" | name = "typed-ast" | ||||
version = "1.5.4" | version = "1.5.4" | ||||
description = "a fork of Python 2 and 3 ast modules with type comment support" | description = "a fork of Python 2 and 3 ast modules with type comment support" | ||||
category = "main" | |||||
optional = false | optional = false | ||||
python-versions = ">=3.6" | python-versions = ">=3.6" | ||||
files = [ | files = [ | ||||
@@ -526,7 +547,6 @@ files = [ | |||||
name = "typing-extensions" | name = "typing-extensions" | ||||
version = "4.1.1" | version = "4.1.1" | ||||
description = "Backported and Experimental Type Hints for Python 3.6+" | description = "Backported and Experimental Type Hints for Python 3.6+" | ||||
category = "dev" | |||||
optional = false | optional = false | ||||
python-versions = ">=3.6" | python-versions = ">=3.6" | ||||
files = [ | files = [ | ||||
@@ -538,7 +558,6 @@ files = [ | |||||
name = "wrapt" | name = "wrapt" | ||||
version = "1.11.2" | version = "1.11.2" | ||||
description = "Module for decorators, wrappers and monkey patching." | description = "Module for decorators, wrappers and monkey patching." | ||||
category = "dev" | |||||
optional = false | optional = false | ||||
python-versions = "*" | python-versions = "*" | ||||
files = [ | files = [ | ||||
@@ -549,7 +568,6 @@ files = [ | |||||
name = "zipp" | name = "zipp" | ||||
version = "3.1.0" | version = "3.1.0" | ||||
description = "Backport of pathlib-compatible object wrapper for zip files" | description = "Backport of pathlib-compatible object wrapper for zip files" | ||||
category = "dev" | |||||
optional = false | optional = false | ||||
python-versions = ">=3.6" | python-versions = ">=3.6" | ||||
files = [ | files = [ | ||||
@@ -564,4 +582,4 @@ testing = ["func-timeout", "jaraco.itertools"] | |||||
[metadata] | [metadata] | ||||
lock-version = "2.0" | lock-version = "2.0" | ||||
python-versions = "^3.7" | python-versions = "^3.7" | ||||
content-hash = "5c2b82d1c8a6a283f63558fc693271f42fdcaeab73616713eb9b38b0b59787fc" | |||||
content-hash = "03625aefdb3d0562b74d70e00eda5ddb3a477a433adc9aa01b351e3cef897403" |
@@ -21,6 +21,15 @@ pylint = "^2.7" | |||||
rope = "^0.14.0" | rope = "^0.14.0" | ||||
isort = "^4.3" | isort = "^4.3" | ||||
black = "^22.12.0" | black = "^22.12.0" | ||||
mypy = ">=1.4,<1.8" | |||||
[tool.black] | [tool.black] | ||||
line-length = 120 | 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 string | ||||
import unittest | 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 | from diagrams.c4 import Person, Container, Database, System, SystemBoundary, Relationship | ||||
class C4Test(unittest.TestCase): | 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) | setdiagram(None) | ||||
setcluster(None) | setcluster(None) | ||||
try: | try: | ||||
os.remove(self.name + ".png") | |||||
os.remove(f"{self.name}.png") | |||||
except FileNotFoundError: | except FileNotFoundError: | ||||
pass | pass | ||||
def test_nodes(self): | |||||
def test_nodes(self) -> None: | |||||
with Diagram(name=self.name, show=False): | with Diagram(name=self.name, show=False): | ||||
person = Person("person", "A person.") | person = Person("person", "A person.") | ||||
container = Container("container", "Java application", "The application.") | container = Container("container", "Java application", "The application.") | ||||
database = Database("database", "Oracle database", "Stores information.") | 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): | with Diagram(name=self.name, show=False): | ||||
external_person = Person("person", external=True) | external_person = Person("person", external=True) | ||||
external_system = System("external", external=True) | external_system = System("external", external=True) | ||||
def test_systems(self): | |||||
def test_systems(self) -> None: | |||||
with Diagram(name=self.name, show=False): | with Diagram(name=self.name, show=False): | ||||
system = System("system", "The internal system.") | system = System("system", "The internal system.") | ||||
system_without_description = System("unknown") | system_without_description = System("unknown") | ||||
def test_edges(self): | |||||
def test_edges(self) -> None: | |||||
with Diagram(name=self.name, show=False): | with Diagram(name=self.name, show=False): | ||||
c1 = Container("container1") | c1 = Container("container1") | ||||
c2 = Container("container2") | c2 = Container("container2") | ||||
c1 >> c2 | c1 >> c2 | ||||
def test_edges_with_labels(self): | |||||
def test_edges_with_labels(self) -> None: | |||||
with Diagram(name=self.name, show=False): | with Diagram(name=self.name, show=False): | ||||
c1 = Container("container1") | c1 = Container("container1") | ||||
c2 = Container("container2") | c2 = Container("container2") | ||||
@@ -51,14 +50,14 @@ class C4Test(unittest.TestCase): | |||||
c1 >> Relationship("depends on") >> c2 | c1 >> Relationship("depends on") >> c2 | ||||
c1 << Relationship("is depended on by") << 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): | with Diagram(name=self.name, show=False): | ||||
s1 = System("system 1") | s1 = System("system 1") | ||||
s2 = System("system 2") | 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 Diagram(name=self.name, show=False): | ||||
with SystemBoundary("System"): | with SystemBoundary("System"): | ||||
Container("container", "type", "description") | Container("container", "type", "description") |
@@ -3,15 +3,14 @@ import shutil | |||||
import unittest | import unittest | ||||
import pathlib | 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): | 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) | setdiagram(None) | ||||
setcluster(None) | setcluster(None) | ||||
# Only some tests generate the image file. | # Only some tests generate the image file. | ||||
@@ -20,11 +19,11 @@ class DiagramTest(unittest.TestCase): | |||||
except OSError: | except OSError: | ||||
# Consider it file | # Consider it file | ||||
try: | try: | ||||
os.remove(self.name + ".png") | |||||
os.remove(f"{self.name}.png") | |||||
except FileNotFoundError: | except FileNotFoundError: | ||||
pass | pass | ||||
def test_validate_direction(self): | |||||
def test_validate_direction(self) -> None: | |||||
# Normal directions. | # Normal directions. | ||||
for dir in ("TB", "BT", "LR", "RL", "tb"): | for dir in ("TB", "BT", "LR", "RL", "tb"): | ||||
Diagram(direction=dir) | Diagram(direction=dir) | ||||
@@ -34,7 +33,7 @@ class DiagramTest(unittest.TestCase): | |||||
with self.assertRaises(ValueError): | with self.assertRaises(ValueError): | ||||
Diagram(direction=dir) | Diagram(direction=dir) | ||||
def test_validate_curvestyle(self): | |||||
def test_validate_curvestyle(self) -> None: | |||||
# Normal directions. | # Normal directions. | ||||
for cvs in ("ortho", "curved", "CURVED"): | for cvs in ("ortho", "curved", "CURVED"): | ||||
Diagram(curvestyle=cvs) | Diagram(curvestyle=cvs) | ||||
@@ -44,7 +43,7 @@ class DiagramTest(unittest.TestCase): | |||||
with self.assertRaises(ValueError): | with self.assertRaises(ValueError): | ||||
Diagram(curvestyle=cvs) | Diagram(curvestyle=cvs) | ||||
def test_validate_outformat(self): | |||||
def test_validate_outformat(self) -> None: | |||||
# Normal output formats. | # Normal output formats. | ||||
for fmt in ("png", "jpg", "svg", "pdf", "PNG", "dot"): | for fmt in ("png", "jpg", "svg", "pdf", "PNG", "dot"): | ||||
Diagram(outformat=fmt) | Diagram(outformat=fmt) | ||||
@@ -54,18 +53,18 @@ class DiagramTest(unittest.TestCase): | |||||
with self.assertRaises(ValueError): | with self.assertRaises(ValueError): | ||||
Diagram(outformat=fmt) | Diagram(outformat=fmt) | ||||
def test_with_global_context(self): | |||||
def test_with_global_context(self) -> None: | |||||
self.assertIsNone(getdiagram()) | self.assertIsNone(getdiagram()) | ||||
with Diagram(name=os.path.join(self.name, "with_global_context"), show=False): | with Diagram(name=os.path.join(self.name, "with_global_context"), show=False): | ||||
self.assertIsNotNone(getdiagram()) | self.assertIsNotNone(getdiagram()) | ||||
self.assertIsNone(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. | # Node must be belong to a diagrams. | ||||
with self.assertRaises(EnvironmentError): | with self.assertRaises(EnvironmentError): | ||||
Node("node") | 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 Diagram(name=os.path.join(self.name, "node_to_node"), show=False): | ||||
node1 = Node("node1") | node1 = Node("node1") | ||||
node2 = Node("node2") | node2 = Node("node2") | ||||
@@ -73,7 +72,7 @@ class DiagramTest(unittest.TestCase): | |||||
self.assertEqual(node1 >> node2, node2) | self.assertEqual(node1 >> node2, node2) | ||||
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 Diagram(name=os.path.join(self.name, "node_to_nodes"), show=False): | ||||
node1 = Node("node1") | node1 = Node("node1") | ||||
nodes = [Node("node2"), Node("node3")] | nodes = [Node("node2"), Node("node3")] | ||||
@@ -81,7 +80,7 @@ class DiagramTest(unittest.TestCase): | |||||
self.assertEqual(node1 >> nodes, nodes) | self.assertEqual(node1 >> nodes, nodes) | ||||
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 Diagram(name=os.path.join(self.name, "nodes_to_node"), show=False): | ||||
node1 = Node("node1") | node1 = Node("node1") | ||||
nodes = [Node("node2"), Node("node3")] | nodes = [Node("node2"), Node("node3")] | ||||
@@ -89,32 +88,31 @@ class DiagramTest(unittest.TestCase): | |||||
self.assertEqual(nodes >> node1, node1) | self.assertEqual(nodes >> node1, node1) | ||||
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" | self.name = "example_1" | ||||
with Diagram(name="Example 1", show=False): | with Diagram(name="Example 1", show=False): | ||||
Node("node1") | Node("node1") | ||||
self.assertTrue(os.path.exists(f"{self.name}.png")) | 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" | self.name = "my_custom_name" | ||||
with Diagram(name="Example 1", filename=self.name, show=False): | with Diagram(name="Example 1", filename=self.name, show=False): | ||||
Node("node1") | Node("node1") | ||||
self.assertTrue(os.path.exists(f"{self.name}.png")) | 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.""" | """Check that providing an empty name don't crash, but save in a diagrams_image.xxx file.""" | ||||
self.name = 'diagrams_image' | self.name = 'diagrams_image' | ||||
with Diagram(show=False): | with Diagram(show=False): | ||||
Node("node1") | Node("node1") | ||||
self.assertTrue(os.path.exists(f"{self.name}.png")) | 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): | with Diagram(name=os.path.join(self.name, "nodes_to_node"), show=False): | ||||
node1 = Node("node1") | node1 = Node("node1") | ||||
self.assertTrue(node1.label,"Node\nnode1") | 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.""" | """Check that outformat render all the files from the list.""" | ||||
self.name = 'diagrams_image' | self.name = 'diagrams_image' | ||||
with Diagram(show=False, outformat=["dot", "png"]): | with Diagram(show=False, outformat=["dot", "png"]): | ||||
@@ -128,10 +126,10 @@ class DiagramTest(unittest.TestCase): | |||||
class ClusterTest(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) | setdiagram(None) | ||||
setcluster(None) | setcluster(None) | ||||
# Only some tests generate the image file. | # Only some tests generate the image file. | ||||
@@ -140,7 +138,7 @@ class ClusterTest(unittest.TestCase): | |||||
except OSError: | except OSError: | ||||
pass | pass | ||||
def test_validate_direction(self): | |||||
def test_validate_direction(self) -> None: | |||||
# Normal directions. | # Normal directions. | ||||
for dir in ("TB", "BT", "LR", "RL"): | for dir in ("TB", "BT", "LR", "RL"): | ||||
with Diagram(name=os.path.join(self.name, "validate_direction"), show=False): | 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): | with Diagram(name=os.path.join(self.name, "validate_direction"), show=False): | ||||
Cluster(direction=dir) | 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): | with Diagram(name=os.path.join(self.name, "with_global_context"), show=False): | ||||
self.assertIsNone(getcluster()) | self.assertIsNone(getcluster()) | ||||
with Cluster(): | with Cluster(): | ||||
self.assertIsNotNone(getcluster()) | self.assertIsNotNone(getcluster()) | ||||
self.assertIsNone(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): | with Diagram(name=os.path.join(self.name, "with_nested_cluster"), show=False): | ||||
self.assertIsNone(getcluster()) | self.assertIsNone(getcluster()) | ||||
with Cluster() as c1: | with Cluster() as c1: | ||||
@@ -169,12 +167,12 @@ class ClusterTest(unittest.TestCase): | |||||
self.assertEqual(c1, getcluster()) | self.assertEqual(c1, getcluster()) | ||||
self.assertIsNone(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. | # Node must be belong to a diagrams. | ||||
with self.assertRaises(EnvironmentError): | with self.assertRaises(EnvironmentError): | ||||
Node("node") | 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 Diagram(name=os.path.join(self.name, "node_to_node"), show=False): | ||||
with Cluster(): | with Cluster(): | ||||
node1 = Node("node1") | node1 = Node("node1") | ||||
@@ -183,7 +181,7 @@ class ClusterTest(unittest.TestCase): | |||||
self.assertEqual(node1 >> node2, node2) | self.assertEqual(node1 >> node2, node2) | ||||
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 Diagram(name=os.path.join(self.name, "node_to_nodes"), show=False): | ||||
with Cluster(): | with Cluster(): | ||||
node1 = Node("node1") | node1 = Node("node1") | ||||
@@ -192,7 +190,7 @@ class ClusterTest(unittest.TestCase): | |||||
self.assertEqual(node1 >> nodes, nodes) | self.assertEqual(node1 >> nodes, nodes) | ||||
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 Diagram(name=os.path.join(self.name, "nodes_to_node"), show=False): | ||||
with Cluster(): | with Cluster(): | ||||
node1 = Node("node1") | node1 = Node("node1") | ||||
@@ -203,10 +201,10 @@ class ClusterTest(unittest.TestCase): | |||||
class EdgeTest(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) | setdiagram(None) | ||||
setcluster(None) | setcluster(None) | ||||
# Only some tests generate the image file. | # Only some tests generate the image file. | ||||
@@ -215,34 +213,34 @@ class EdgeTest(unittest.TestCase): | |||||
except OSError: | except OSError: | ||||
pass | 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): | with Diagram(name=os.path.join(self.name, "node_to_node"), show=False): | ||||
node1 = Node("node1") | node1 = Node("node1") | ||||
node2 = Node("node2") | node2 = Node("node2") | ||||
self.assertEqual(node1 - Edge(color="red") - node2, 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 Diagram(name=os.path.join(self.name, "node_to_nodes"), show=False): | ||||
with Cluster(): | with Cluster(): | ||||
node1 = Node("node1") | node1 = Node("node1") | ||||
nodes = [Node("node2"), Node("node3")] | 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 Diagram(name=os.path.join(self.name, "nodes_to_node"), show=False): | ||||
with Cluster(): | with Cluster(): | ||||
node1 = Node("node1") | node1 = Node("node1") | ||||
nodes = [Node("node2"), Node("node3")] | nodes = [Node("node2"), Node("node3")] | ||||
self.assertEqual(nodes - Edge(color="red") - node1, node1) | 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 Diagram(name=os.path.join(self.name, "nodes_to_node_with_additional_attributes"), show=False): | ||||
with Cluster(): | with Cluster(): | ||||
node1 = Node("node1") | node1 = Node("node1") | ||||
nodes = [Node("node2"), Node("node3")] | nodes = [Node("node2"), Node("node3")] | ||||
self.assertEqual(nodes - Edge(color="red") - Edge(color="green") - node1, node1) | 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 Diagram(name=os.path.join(self.name, "node_to_node_with_attributes"), show=False): | ||||
with Cluster(): | with Cluster(): | ||||
node1 = Node("node1") | 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="green", label="1.2") >> node2, node2) | ||||
self.assertEqual(node1 << Edge(color="blue", label="1.3") >> 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 Diagram(name=os.path.join(self.name, "node_to_node_with_additional_attributes"), show=False): | ||||
with Cluster(): | with Cluster(): | ||||
node1 = Node("node1") | 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="green", label="2.2") >> Edge(color="red") >> node2, node2) | ||||
self.assertEqual(node1 << Edge(color="blue", label="2.3") >> Edge(color="black") >> 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 Diagram(name=os.path.join(self.name, "nodes_to_node_with_attributes_loop"), show=False): | ||||
with Cluster(): | with Cluster(): | ||||
node = Node("node") | 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="blue", label="3.3") << node, node) | ||||
self.assertEqual(node << Edge(color="pink", label="3.4") >> 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 Diagram(name=os.path.join(self.name, "nodes_to_node_with_attributes_bothdirectional"), show=False): | ||||
with Cluster(): | with Cluster(): | ||||
node1 = Node("node1") | node1 = Node("node1") | ||||
nodes = [Node("node2"), Node("node3")] | nodes = [Node("node2"), Node("node3")] | ||||
self.assertEqual(nodes << Edge(color="green", label="4") >> node1, node1) | 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 Diagram(name=os.path.join(self.name, "nodes_to_node_with_attributes_bidirectional"), show=False): | ||||
with Cluster(): | with Cluster(): | ||||
node1 = Node("node1") | node1 = Node("node1") | ||||
nodes = [Node("node2"), Node("node3")] | nodes = [Node("node2"), Node("node3")] | ||||
self.assertEqual(nodes << Edge(color="blue", label="5") >> node1, node1) | 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 Diagram(name=os.path.join(self.name, "nodes_to_node_with_attributes_onedirectional"), show=False): | ||||
with Cluster(): | with Cluster(): | ||||
node1 = Node("node1") | 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="red", label="6.1") >> node1, node1) | ||||
self.assertEqual(nodes << Edge(color="green", label="6.2") << 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 Diagram(name=os.path.join(self.name, "nodes_to_node_with_additional_attributes_directional"), show=False): | ||||
with Cluster(): | with Cluster(): | ||||
node1 = Node("node1") | node1 = Node("node1") | ||||
@@ -305,7 +303,7 @@ class EdgeTest(unittest.TestCase): | |||||
class ResourcesTest(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 | 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 | i.e. resources/<provider>/<type>/<image>, so check that this depth isn't | ||||