Source code for rnets.dot

# -*- coding: utf-8 -*-
"""This module is a minimal implementation of a writer for the dot language. It
does contain the minimum functionality to allow the writing of the reaction
networks in dot format.

Attributes:
    Opts (type): Type synonym to define options.
    OptsGlob (type): Type synonym defining global options.

    IDENT (int): Identation level when writing the dot file.
    SEP_S_LV (str): Single level separation, e.g: between two node definition.
    SEP_D_LV (str): Double level separation, e.g: between node and edge
       definition.
"""


from collections.abc import Sequence
from itertools import starmap
from typing import NamedTuple
from enum import auto, StrEnum


type Opts = dict[str, str]
type OptsGlob = dict[str, Opts]

IDENT = 4
SEP_S_LV = '\n' * 2
SEP_D_LV = '\n' * 3


[docs] class OptKind(StrEnum): "Enum representing the possible kind of global :obj:`Opts` values" Graph = auto() Node = auto() Edge = auto()
[docs] class Node(NamedTuple): """Structure representing a dot graph node. Attributes: name (str): Name of the node. options (:obj:`Opts`): dot options for the node. """ name: str options: Opts | None = None def __str__(self): return node_to_str(self)
[docs] class Edge(NamedTuple): """Structure representing a dot edge between two nodes. Attributes: origin (str): Starting node name. target (str): Target node name. direction (str): Symbol to use to connect both nodes. See dot manual for possible values. options (:obj:`Opts`): Options of the edge. """ origin: str target: str direction: str = "->" options: Opts | None = None def __str__(self): return edge_to_str(self)
[docs] class Graph(NamedTuple): """Structure representing a dot graph. Attributes: kind (str): Graph type. nodes (sequence of :obj:`Node`): Nodes in the graph. edges (sequence of :obj:`Edge`): Edges in the graph. options(dict of str as keys and :obj:`Opts` as values): Dictionary containing multiple global options. """ kind: str nodes: Sequence[Node] edges: Sequence[Edge] options: OptsGlob | None HEADER = "strict {}" def __str__(self): return graph_to_str(self)
[docs] def ident( s: str , i: int , first: bool = True ) -> str: """Ident the given string. Args: s (str): String that will be idented. i (int): Identation level. first (bool, optional): Wether or not ident the first line. defaults to True Returns: str: Idented string. Note: This is not the optimal way to perform the identation as we should build new string every time that we ident. However, I think that it is more useful than to put the identation during the writing. """ return (" " * first * i) + s.replace('\n', '\n' + ' ' * i)
[docs] def ident_if( s: str | None , i: int , first: bool = True ) -> str: """Same as `ident` but returns an empty string if the input string is empty or None. Args: s (str or None): String that will be idented. i (int): Identation level. first (bool, optional): Wether or not ident the first line. defaults to True. Returns: str: Either the idented string or an empty string. """ if not s: return "" return ident(s, i, first)
[docs] def graph_to_str( g: Graph ) -> str: """Converts a :obj:`Graph` into a dot string. Args: g (:obj:`Graph`): Graph that will be converted. Returns: str: Graph in dot format. """ if not g.options: return "" return ( g.HEADER.format(g.kind) + " " + '{' + SEP_S_LV + ident(opts_glob_to_str(g.options), IDENT, True) + SEP_D_LV + SEP_D_LV.join(map( lambda x: ident_if("\n\n".join(map(str, x)), IDENT, True) , (g.nodes, g.edges))) + "\n}" )
[docs] def opts_to_str( o: Opts ) -> str: """Converts a :obj:`Opts` into a string. Args: n (:obj:`Opts`): Options that will be converted. Returns: str: of the dot format """ out = ',\n'.join(('='.join(o) for o in o.items())) return f"[\n{ident(out, IDENT, True)}\n]"
[docs] def edge_to_str( e: Edge ) -> str: """Converts a :obj:`Edge` into a string. Args: n (:obj:`Edge`): Edge that will be converted. Returns: str: Edge in dot format. """ return ( f'"{e.origin}"' + ' ' + e.direction + ' ' + f'"{e.target}"' + ("" if e.options is None else opts_to_str(e.options)) + ';' )
[docs] def node_to_str( n: Node ) -> str: """Converts a :obj:`Node` into a string Args: n (:obj:`Node`): Node that will be converted. Returns: str: Node in the dot format. """ return ( f'"{n.name}"' + ' ' + ("" if n.options is None else opts_to_str(n.options)) + ';' )
[docs] def opt_glob_to_str( k: str , o: Opts ) -> str: """Format a name followed by :obj:`Opts`. Used to define global variables. Args: k (str): :obj:`OptKind` for which the global options will be decided. o (:obj:`Opts`): Global options to define. Returns: str: Options in dot format. """ return ( k + ' ' + opts_to_str(o) + ';' )
[docs] def opts_glob_to_str( os: OptsGlob ) -> str: """Format a :obj:`OptsGlob` in dot format. Args: os (:obj:`OptsGlob`): To convert to dot format. Returns: str: Global options in dot format. """ return ( SEP_S_LV.join( starmap(opt_glob_to_str, os.items()) ) )