diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2fd60696c..ad9237753 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -33,3 +33,5 @@ jobs: run: | poetry install poetry run python -m unittest -v tests/*.py + - name: Run mypy + run: poetry run mypy diff --git a/diagrams/__init__.py b/diagrams/__init__.py index 6fe2a8024..0416cd593 100644 --- a/diagrams/__init__.py +++ b/diagrams/__init__.py @@ -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 @@ def __init__( 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 @@ def __init__( 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 @@ def __init__( 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 @@ def __init__( 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 @@ def _validate_curvestyle(self, curvestyle: str) -> bool: 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 @@ def render(self) -> None: 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 @@ def __init__( 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 @@ def __init__( """ 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 @@ def __init__( 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 @@ def __exit__(self, exc_type, exc_value, traceback): 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 @@ def subgraph(self, dot: Digraph) -> None: 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 @@ def __init__(self, label: str = "", *, nodeid: str = None, **attrs: Dict): # 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 @@ def __init__(self, label: str = "", *, nodeid: str = None, **attrs: Dict): 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 @@ def __rsub__(self, other: Union[List["Node"], List["Edge"]]): 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 @@ def __rrshift__(self, other: Union[List["Node"], List["Edge"]]): 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 @@ def __rlshift__(self, other: Union[List["Node"], List["Edge"]]): 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 @@ def connect(self, node: "Node", edge: "Edge"): 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 @@ def __init__( 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 @@ def __init__( 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 @@ def __rsub__(self, other: Union[List["Node"], List["Edge"]]) -> List["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 @@ def __rlshift__(self, other: Union[List["Node"], List["Edge"]]) -> List["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 @@ def attrs(self) -> Dict: return {**self._attrs, "dir": direction} -Group = Cluster +Group: Type[Cluster] = Cluster diff --git a/diagrams/aws/general.py b/diagrams/aws/general.py index dd6d4bea7..ce0270a0c 100644 --- a/diagrams/aws/general.py +++ b/diagrams/aws/general.py @@ -102,8 +102,3 @@ class User(_General): class Users(_General): _icon = "users.png" - - -# Aliases - -OfficeBuilding = GenericOfficeBuilding diff --git a/diagrams/c4/__init__.py b/diagrams/c4/__init__.py index 90ce7a926..b383eb78a 100644 --- a/diagrams/c4/__init__.py +++ b/diagrams/c4/__init__.py @@ -3,10 +3,12 @@ """ 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'{html.escape(name)}
' subtitle = f'[{html.escape(key)}]
' 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 "
".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'<{text}>' -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 "", diff --git a/diagrams/custom/__init__.py b/diagrams/custom/__init__.py index 9845932dd..80488d430 100644 --- a/diagrams/custom/__init__.py +++ b/diagrams/custom/__init__.py @@ -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) diff --git a/poetry.lock b/poetry.lock index 3c271de36..b7a684453 100644 --- a/poetry.lock +++ b/poetry.lock @@ -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 = [ @@ -305,23 +292,68 @@ files = [ {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, ] +[[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" diff --git a/pyproject.toml b/pyproject.toml index 7b638a8fc..99f8fa7cc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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 \ No newline at end of file diff --git a/tests/test_c4.py b/tests/test_c4.py index 3877ec0fb..b76b84034 100644 --- a/tests/test_c4.py +++ b/tests/test_c4.py @@ -3,47 +3,46 @@ 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 @@ def test_edges_with_labels(self): 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") diff --git a/tests/test_diagram.py b/tests/test_diagram.py index 00bdacc6e..52e78f774 100644 --- a/tests/test_diagram.py +++ b/tests/test_diagram.py @@ -3,15 +3,14 @@ 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 @@ def tearDown(self): 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 @@ def test_validate_direction(self): 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 @@ def test_validate_curvestyle(self): 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 @@ def test_validate_outformat(self): 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 @@ def test_node_to_node(self): 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 @@ def test_node_to_nodes(self): 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 @@ def test_nodes_to_node(self): 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 @@ def test_outformat_list(self): 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 @@ def tearDown(self): 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 @@ def test_validate_direction(self): 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 @@ def test_with_nested_cluster(self): 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 @@ def test_node_to_node(self): 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 @@ def test_node_to_nodes(self): 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 @@ def test_nodes_to_node(self): 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 @@ def tearDown(self): 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 @@ def test_node_to_node_with_attributes(self): 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 @@ def test_node_to_node_with_additional_attributes(self): 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 @@ def test_nodes_to_node_with_attributes_loop(self): 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 @@ def test_nodes_to_node_with_attributes_onedirectional(self): 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 @@ def test_nodes_to_node_with_additional_attributes_directional(self): 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///, so check that this depth isn't