Bladeren bron

Merge 4daa79c1d6 into de52317c58

pull/960/merge
Vadim Kharitonov 1 maand geleden
committed by GitHub
bovenliggende
commit
3438809070
Geen bekende sleutel gevonden voor deze handtekening in de database GPG sleutel-ID: B5690EEEBB952194
9 gewijzigde bestanden met toevoegingen van 273 en 239 verwijderingen
  1. +2
    -0
      .github/workflows/test.yml
  2. +124
    -117
      diagrams/__init__.py
  3. +0
    -5
      diagrams/aws/general.py
  4. +19
    -17
      diagrams/c4/__init__.py
  5. +8
    -4
      diagrams/custom/__init__.py
  6. +54
    -36
      poetry.lock
  7. +9
    -0
      pyproject.toml
  8. +13
    -14
      tests/test_c4.py
  9. +44
    -46
      tests/test_diagram.py

+ 2
- 0
.github/workflows/test.yml Bestand weergeven

@@ -33,3 +33,5 @@ jobs:
run: |
poetry install
poetry run python -m unittest -v tests/*.py
- name: Run mypy
run: poetry run mypy

+ 124
- 117
diagrams/__init__.py Bestand weergeven

@@ -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

+ 0
- 5
diagrams/aws/general.py Bestand weergeven

@@ -102,8 +102,3 @@ class User(_General):

class Users(_General):
_icon = "users.png"


# Aliases

OfficeBuilding = GenericOfficeBuilding

+ 19
- 17
diagrams/c4/__init__.py Bestand weergeven

@@ -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 "",


+ 8
- 4
diagrams/custom/__init__.py Bestand weergeven

@@ -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)

+ 54
- 36
poetry.lock Bestand weergeven

@@ -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"

+ 9
- 0
pyproject.toml Bestand weergeven

@@ -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

+ 13
- 14
tests/test_c4.py Bestand weergeven

@@ -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")

+ 44
- 46
tests/test_diagram.py Bestand weergeven

@@ -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


Laden…
Annuleren
Opslaan