comparison env/lib/python3.9/site-packages/pydot.py @ 0:4f3585e2f14b draft default tip

"planemo upload commit 60cee0fc7c0cda8592644e1aad72851dec82c959"
author shellac
date Mon, 22 Mar 2021 18:12:50 +0000
parents
children
comparison
equal deleted inserted replaced
-1:000000000000 0:4f3585e2f14b
1 """An interface to GraphViz."""
2 from __future__ import division
3 from __future__ import print_function
4 import copy
5 import io
6 import errno
7 import os
8 import re
9 import subprocess
10 import sys
11 import tempfile
12 import warnings
13
14 try:
15 import dot_parser
16 except Exception as e:
17 warnings.warn(
18 "`pydot` could not import `dot_parser`, "
19 "so `pydot` will be unable to parse DOT files. "
20 "The error was: {e}".format(e=e))
21
22
23 __author__ = 'Ero Carrera'
24 __version__ = '1.4.2'
25 __license__ = 'MIT'
26
27
28 PY3 = sys.version_info >= (3, 0, 0)
29 if PY3:
30 str_type = str
31 else:
32 str_type = basestring
33
34
35 GRAPH_ATTRIBUTES = { 'Damping', 'K', 'URL', 'aspect', 'bb', 'bgcolor',
36 'center', 'charset', 'clusterrank', 'colorscheme', 'comment', 'compound',
37 'concentrate', 'defaultdist', 'dim', 'dimen', 'diredgeconstraints',
38 'dpi', 'epsilon', 'esep', 'fontcolor', 'fontname', 'fontnames',
39 'fontpath', 'fontsize', 'id', 'label', 'labeljust', 'labelloc',
40 'landscape', 'layers', 'layersep', 'layout', 'levels', 'levelsgap',
41 'lheight', 'lp', 'lwidth', 'margin', 'maxiter', 'mclimit', 'mindist',
42 'mode', 'model', 'mosek', 'nodesep', 'nojustify', 'normalize', 'nslimit',
43 'nslimit1', 'ordering', 'orientation', 'outputorder', 'overlap',
44 'overlap_scaling', 'pack', 'packmode', 'pad', 'page', 'pagedir',
45 'quadtree', 'quantum', 'rankdir', 'ranksep', 'ratio', 'remincross',
46 'repulsiveforce', 'resolution', 'root', 'rotate', 'searchsize', 'sep',
47 'showboxes', 'size', 'smoothing', 'sortv', 'splines', 'start',
48 'stylesheet', 'target', 'truecolor', 'viewport', 'voro_margin',
49 # for subgraphs
50 'rank' }
51
52
53 EDGE_ATTRIBUTES = { 'URL', 'arrowhead', 'arrowsize', 'arrowtail',
54 'color', 'colorscheme', 'comment', 'constraint', 'decorate', 'dir',
55 'edgeURL', 'edgehref', 'edgetarget', 'edgetooltip', 'fontcolor',
56 'fontname', 'fontsize', 'headURL', 'headclip', 'headhref', 'headlabel',
57 'headport', 'headtarget', 'headtooltip', 'href', 'id', 'label',
58 'labelURL', 'labelangle', 'labeldistance', 'labelfloat', 'labelfontcolor',
59 'labelfontname', 'labelfontsize', 'labelhref', 'labeltarget',
60 'labeltooltip', 'layer', 'len', 'lhead', 'lp', 'ltail', 'minlen',
61 'nojustify', 'penwidth', 'pos', 'samehead', 'sametail', 'showboxes',
62 'style', 'tailURL', 'tailclip', 'tailhref', 'taillabel', 'tailport',
63 'tailtarget', 'tailtooltip', 'target', 'tooltip', 'weight',
64 'rank' }
65
66
67 NODE_ATTRIBUTES = { 'URL', 'color', 'colorscheme', 'comment',
68 'distortion', 'fillcolor', 'fixedsize', 'fontcolor', 'fontname',
69 'fontsize', 'group', 'height', 'id', 'image', 'imagescale', 'label',
70 'labelloc', 'layer', 'margin', 'nojustify', 'orientation', 'penwidth',
71 'peripheries', 'pin', 'pos', 'rects', 'regular', 'root', 'samplepoints',
72 'shape', 'shapefile', 'showboxes', 'sides', 'skew', 'sortv', 'style',
73 'target', 'tooltip', 'vertices', 'width', 'z',
74 # The following are attributes dot2tex
75 'texlbl', 'texmode' }
76
77
78 CLUSTER_ATTRIBUTES = { 'K', 'URL', 'bgcolor', 'color', 'colorscheme',
79 'fillcolor', 'fontcolor', 'fontname', 'fontsize', 'label', 'labeljust',
80 'labelloc', 'lheight', 'lp', 'lwidth', 'nojustify', 'pencolor',
81 'penwidth', 'peripheries', 'sortv', 'style', 'target', 'tooltip' }
82
83
84 DEFAULT_PROGRAMS = {
85 'dot',
86 'twopi',
87 'neato',
88 'circo',
89 'fdp',
90 'sfdp',
91 }
92
93
94 def is_windows():
95 # type: () -> bool
96 return os.name == 'nt'
97
98
99 def is_anaconda():
100 # type: () -> bool
101 import glob
102 return glob.glob(os.path.join(sys.prefix, 'conda-meta\\graphviz*.json')) != []
103
104
105 def get_executable_extension():
106 # type: () -> str
107 if is_windows():
108 return '.bat' if is_anaconda() else '.exe'
109 else:
110 return ''
111
112
113 def call_graphviz(program, arguments, working_dir, **kwargs):
114 # explicitly inherit `$PATH`, on Windows too,
115 # with `shell=False`
116
117 if program in DEFAULT_PROGRAMS:
118 extension = get_executable_extension()
119 program += extension
120
121 if arguments is None:
122 arguments = []
123
124 env = {
125 'PATH': os.environ.get('PATH', ''),
126 'LD_LIBRARY_PATH': os.environ.get('LD_LIBRARY_PATH', ''),
127 'SYSTEMROOT': os.environ.get('SYSTEMROOT', ''),
128 }
129
130 program_with_args = [program, ] + arguments
131
132 process = subprocess.Popen(
133 program_with_args,
134 env=env,
135 cwd=working_dir,
136 shell=False,
137 stderr=subprocess.PIPE,
138 stdout=subprocess.PIPE,
139 **kwargs
140 )
141 stdout_data, stderr_data = process.communicate()
142
143 return stdout_data, stderr_data, process
144
145
146 #
147 # Extended version of ASPN's Python Cookbook Recipe:
148 # Frozen dictionaries.
149 # https://code.activestate.com/recipes/414283/
150 #
151 # This version freezes dictionaries used as values within dictionaries.
152 #
153 class frozendict(dict):
154 def _blocked_attribute(obj):
155 raise AttributeError('A frozendict cannot be modified.')
156 _blocked_attribute = property(_blocked_attribute)
157
158 __delitem__ = __setitem__ = clear = _blocked_attribute
159 pop = popitem = setdefault = update = _blocked_attribute
160
161 def __new__(cls, *args, **kw):
162 new = dict.__new__(cls)
163
164 args_ = []
165 for arg in args:
166 if isinstance(arg, dict):
167 arg = copy.copy(arg)
168 for k in arg:
169 v = arg[k]
170 if isinstance(v, frozendict):
171 arg[k] = v
172 elif isinstance(v, dict):
173 arg[k] = frozendict(v)
174 elif isinstance(v, list):
175 v_ = list()
176 for elm in v:
177 if isinstance(elm, dict):
178 v_.append( frozendict(elm) )
179 else:
180 v_.append( elm )
181 arg[k] = tuple(v_)
182 args_.append( arg )
183 else:
184 args_.append( arg )
185
186 dict.__init__(new, *args_, **kw)
187 return new
188
189 def __init__(self, *args, **kw):
190 pass
191
192 def __hash__(self):
193 try:
194 return self._cached_hash
195 except AttributeError:
196 h = self._cached_hash = hash(tuple(sorted(self.items())))
197 return h
198
199 def __repr__(self):
200 return "frozendict(%s)" % dict.__repr__(self)
201
202
203 dot_keywords = ['graph', 'subgraph', 'digraph', 'node', 'edge', 'strict']
204
205 id_re_alpha_nums = re.compile('^[_a-zA-Z][a-zA-Z0-9_,]*$', re.UNICODE)
206 id_re_alpha_nums_with_ports = re.compile(
207 '^[_a-zA-Z][a-zA-Z0-9_,:\"]*[a-zA-Z0-9_,\"]+$', re.UNICODE)
208 id_re_num = re.compile('^[0-9,]+$', re.UNICODE)
209 id_re_with_port = re.compile('^([^:]*):([^:]*)$', re.UNICODE)
210 id_re_dbl_quoted = re.compile('^\".*\"$', re.S|re.UNICODE)
211 id_re_html = re.compile('^<.*>$', re.S|re.UNICODE)
212
213
214 def needs_quotes( s ):
215 """Checks whether a string is a dot language ID.
216
217 It will check whether the string is solely composed
218 by the characters allowed in an ID or not.
219 If the string is one of the reserved keywords it will
220 need quotes too but the user will need to add them
221 manually.
222 """
223
224 # If the name is a reserved keyword it will need quotes but pydot
225 # can't tell when it's being used as a keyword or when it's simply
226 # a name. Hence the user needs to supply the quotes when an element
227 # would use a reserved keyword as name. This function will return
228 # false indicating that a keyword string, if provided as-is, won't
229 # need quotes.
230 if s in dot_keywords:
231 return False
232
233 chars = [ord(c) for c in s if ord(c)>0x7f or ord(c)==0]
234 if chars and not id_re_dbl_quoted.match(s) and not id_re_html.match(s):
235 return True
236
237 for test_re in [id_re_alpha_nums, id_re_num,
238 id_re_dbl_quoted, id_re_html,
239 id_re_alpha_nums_with_ports]:
240 if test_re.match(s):
241 return False
242
243 m = id_re_with_port.match(s)
244 if m:
245 return needs_quotes(m.group(1)) or needs_quotes(m.group(2))
246
247 return True
248
249
250 def quote_if_necessary(s):
251 """Enclose attribute value in quotes, if needed."""
252 if isinstance(s, bool):
253 if s is True:
254 return 'True'
255 return 'False'
256
257 if not isinstance( s, str_type):
258 return s
259
260 if not s:
261 return s
262
263 if needs_quotes(s):
264 replace = {'"' : r'\"',
265 "\n" : r'\n',
266 "\r" : r'\r'}
267 for (a,b) in replace.items():
268 s = s.replace(a, b)
269
270 return '"' + s + '"'
271
272 return s
273
274
275
276 def graph_from_dot_data(s):
277 """Load graphs from DOT description in string `s`.
278
279 @param s: string in [DOT language](
280 https://en.wikipedia.org/wiki/DOT_(graph_description_language))
281
282 @return: Graphs that result from parsing.
283 @rtype: `list` of `pydot.Dot`
284 """
285 return dot_parser.parse_dot_data(s)
286
287
288 def graph_from_dot_file(path, encoding=None):
289 """Load graphs from DOT file at `path`.
290
291 @param path: to DOT file
292 @param encoding: as passed to `io.open`.
293 For example, `'utf-8'`.
294
295 @return: Graphs that result from parsing.
296 @rtype: `list` of `pydot.Dot`
297 """
298 with io.open(path, 'rt', encoding=encoding) as f:
299 s = f.read()
300 if not PY3:
301 s = unicode(s)
302 graphs = graph_from_dot_data(s)
303 return graphs
304
305
306
307 def graph_from_edges(edge_list, node_prefix='', directed=False):
308 """Creates a basic graph out of an edge list.
309
310 The edge list has to be a list of tuples representing
311 the nodes connected by the edge.
312 The values can be anything: bool, int, float, str.
313
314 If the graph is undirected by default, it is only
315 calculated from one of the symmetric halves of the matrix.
316 """
317
318 if directed:
319 graph = Dot(graph_type='digraph')
320
321 else:
322 graph = Dot(graph_type='graph')
323
324 for edge in edge_list:
325
326 if isinstance(edge[0], str):
327 src = node_prefix + edge[0]
328 else:
329 src = node_prefix + str(edge[0])
330
331 if isinstance(edge[1], str):
332 dst = node_prefix + edge[1]
333 else:
334 dst = node_prefix + str(edge[1])
335
336 e = Edge( src, dst )
337 graph.add_edge(e)
338
339 return graph
340
341
342 def graph_from_adjacency_matrix(matrix, node_prefix= u'', directed=False):
343 """Creates a basic graph out of an adjacency matrix.
344
345 The matrix has to be a list of rows of values
346 representing an adjacency matrix.
347 The values can be anything: bool, int, float, as long
348 as they can evaluate to True or False.
349 """
350
351 node_orig = 1
352
353 if directed:
354 graph = Dot(graph_type='digraph')
355 else:
356 graph = Dot(graph_type='graph')
357
358 for row in matrix:
359 if not directed:
360 skip = matrix.index(row)
361 r = row[skip:]
362 else:
363 skip = 0
364 r = row
365 node_dest = skip+1
366
367 for e in r:
368 if e:
369 graph.add_edge(
370 Edge('%s%s' % (node_prefix, node_orig),
371 '%s%s' % (node_prefix, node_dest)))
372 node_dest += 1
373 node_orig += 1
374
375 return graph
376
377
378 def graph_from_incidence_matrix(matrix, node_prefix='', directed=False):
379 """Creates a basic graph out of an incidence matrix.
380
381 The matrix has to be a list of rows of values
382 representing an incidence matrix.
383 The values can be anything: bool, int, float, as long
384 as they can evaluate to True or False.
385 """
386
387 node_orig = 1
388
389 if directed:
390 graph = Dot(graph_type='digraph')
391 else:
392 graph = Dot(graph_type='graph')
393
394 for row in matrix:
395 nodes = []
396 c = 1
397
398 for node in row:
399 if node:
400 nodes.append(c*node)
401 c += 1
402 nodes.sort()
403
404 if len(nodes) == 2:
405 graph.add_edge(
406 Edge('%s%s' % (node_prefix, abs(nodes[0])),
407 '%s%s' % (node_prefix, nodes[1])))
408
409 if not directed:
410 graph.set_simplify(True)
411
412 return graph
413
414
415 class Common(object):
416 """Common information to several classes.
417
418 Should not be directly used, several classes are derived from
419 this one.
420 """
421
422
423 def __getstate__(self):
424
425 dict = copy.copy(self.obj_dict)
426
427 return dict
428
429
430 def __setstate__(self, state):
431
432 self.obj_dict = state
433
434
435 def __get_attribute__(self, attr):
436 """Look for default attributes for this node"""
437
438 attr_val = self.obj_dict['attributes'].get(attr, None)
439
440 if attr_val is None:
441 # get the defaults for nodes/edges
442
443 default_node_name = self.obj_dict['type']
444
445 # The defaults for graphs are set on a node named 'graph'
446 if default_node_name in ('subgraph', 'digraph', 'cluster'):
447 default_node_name = 'graph'
448
449 g = self.get_parent_graph()
450 if g is not None:
451 defaults = g.get_node( default_node_name )
452 else:
453 return None
454
455 # Multiple defaults could be set by having repeated 'graph [...]'
456 # 'node [...]', 'edge [...]' statements. In such case, if the
457 # same attribute is set in different statements, only the first
458 # will be returned. In order to get all, one would call the
459 # get_*_defaults() methods and handle those. Or go node by node
460 # (of the ones specifying defaults) and modify the attributes
461 # individually.
462 #
463 if not isinstance(defaults, (list, tuple)):
464 defaults = [defaults]
465
466 for default in defaults:
467 attr_val = default.obj_dict['attributes'].get(attr, None)
468 if attr_val:
469 return attr_val
470 else:
471 return attr_val
472
473 return None
474
475
476 def set_parent_graph(self, parent_graph):
477
478 self.obj_dict['parent_graph'] = parent_graph
479
480
481 def get_parent_graph(self):
482
483 return self.obj_dict.get('parent_graph', None)
484
485
486 def set(self, name, value):
487 """Set an attribute value by name.
488
489 Given an attribute 'name' it will set its value to 'value'.
490 There's always the possibility of using the methods:
491
492 set_'name'(value)
493
494 which are defined for all the existing attributes.
495 """
496
497 self.obj_dict['attributes'][name] = value
498
499
500 def get(self, name):
501 """Get an attribute value by name.
502
503 Given an attribute 'name' it will get its value.
504 There's always the possibility of using the methods:
505
506 get_'name'()
507
508 which are defined for all the existing attributes.
509 """
510
511 return self.obj_dict['attributes'].get(name, None)
512
513
514 def get_attributes(self):
515 """"""
516
517 return self.obj_dict['attributes']
518
519
520 def set_sequence(self, seq):
521
522 self.obj_dict['sequence'] = seq
523
524
525 def get_sequence(self):
526
527 return self.obj_dict['sequence']
528
529
530 def create_attribute_methods(self, obj_attributes):
531
532 #for attr in self.obj_dict['attributes']:
533 for attr in obj_attributes:
534
535 # Generate all the Setter methods.
536 #
537 self.__setattr__(
538 'set_'+attr,
539 lambda x, a=attr :
540 self.obj_dict['attributes'].__setitem__(a, x) )
541
542 # Generate all the Getter methods.
543 #
544 self.__setattr__(
545 'get_'+attr, lambda a=attr : self.__get_attribute__(a))
546
547
548
549 class Error(Exception):
550 """General error handling class.
551 """
552 def __init__(self, value):
553 self.value = value
554 def __str__(self):
555 return self.value
556
557
558 class InvocationException(Exception):
559 """Indicate problem while running any GraphViz executable.
560 """
561 def __init__(self, value):
562 self.value = value
563 def __str__(self):
564 return self.value
565
566
567
568 class Node(Common):
569 """A graph node.
570
571 This class represents a graph's node with all its attributes.
572
573 node(name, attribute=value, ...)
574
575 name: node's name
576
577 All the attributes defined in the Graphviz dot language should
578 be supported.
579 """
580
581 def __init__(self, name = '', obj_dict = None, **attrs):
582
583 #
584 # Nodes will take attributes of
585 # all other types because the defaults
586 # for any GraphViz object are dealt with
587 # as if they were Node definitions
588 #
589
590 if obj_dict is not None:
591
592 self.obj_dict = obj_dict
593
594 else:
595
596 self.obj_dict = dict()
597
598 # Copy the attributes
599 #
600 self.obj_dict[ 'attributes' ] = dict( attrs )
601 self.obj_dict[ 'type' ] = 'node'
602 self.obj_dict[ 'parent_graph' ] = None
603 self.obj_dict[ 'parent_node_list' ] = None
604 self.obj_dict[ 'sequence' ] = None
605
606 # Remove the compass point
607 #
608 port = None
609 if isinstance(name, str_type) and not name.startswith('"'):
610 idx = name.find(':')
611 if idx > 0 and idx+1 < len(name):
612 name, port = name[:idx], name[idx:]
613
614 if isinstance(name, int):
615 name = str(name)
616
617 self.obj_dict['name'] = quote_if_necessary(name)
618 self.obj_dict['port'] = port
619
620 self.create_attribute_methods(NODE_ATTRIBUTES)
621
622 def __str__(self):
623 return self.to_string()
624
625
626 def set_name(self, node_name):
627 """Set the node's name."""
628
629 self.obj_dict['name'] = node_name
630
631
632 def get_name(self):
633 """Get the node's name."""
634
635 return self.obj_dict['name']
636
637
638 def get_port(self):
639 """Get the node's port."""
640
641 return self.obj_dict['port']
642
643
644 def add_style(self, style):
645
646 styles = self.obj_dict['attributes'].get('style', None)
647 if not styles and style:
648 styles = [ style ]
649 else:
650 styles = styles.split(',')
651 styles.append( style )
652
653 self.obj_dict['attributes']['style'] = ','.join( styles )
654
655
656 def to_string(self):
657 """Return string representation of node in DOT language."""
658
659
660 # RMF: special case defaults for node, edge and graph properties.
661 #
662 node = quote_if_necessary(self.obj_dict['name'])
663
664 node_attr = list()
665
666 for attr in sorted(self.obj_dict['attributes']):
667 value = self.obj_dict['attributes'][attr]
668 if value == '':
669 value = '""'
670 if value is not None:
671 node_attr.append(
672 '%s=%s' % (attr, quote_if_necessary(value) ) )
673 else:
674 node_attr.append( attr )
675
676
677 # No point in having nodes setting any defaults if the don't set
678 # any attributes...
679 #
680 if node in ('graph', 'node', 'edge') and len(node_attr) == 0:
681 return ''
682
683 node_attr = ', '.join(node_attr)
684
685 if node_attr:
686 node += ' [' + node_attr + ']'
687
688 return node + ';'
689
690
691
692 class Edge(Common):
693 """A graph edge.
694
695 This class represents a graph's edge with all its attributes.
696
697 edge(src, dst, attribute=value, ...)
698
699 src: source node, subgraph or cluster
700 dst: destination node, subgraph or cluster
701
702 `src` and `dst` can be specified as a `Node`, `Subgraph` or
703 `Cluster` object, or as the name string of such a component.
704
705 All the attributes defined in the Graphviz dot language should
706 be supported.
707
708 Attributes can be set through the dynamically generated methods:
709
710 set_[attribute name], i.e. set_label, set_fontname
711
712 or directly by using the instance's special dictionary:
713
714 Edge.obj_dict['attributes'][attribute name], i.e.
715
716 edge_instance.obj_dict['attributes']['label']
717 edge_instance.obj_dict['attributes']['fontname']
718
719 """
720
721 def __init__(self, src='', dst='', obj_dict=None, **attrs):
722 self.obj_dict = dict()
723 if isinstance(src, (Node, Subgraph, Cluster)):
724 src = src.get_name()
725 if isinstance(dst, (Node, Subgraph, Cluster)):
726 dst = dst.get_name()
727 points = (quote_if_necessary(src),
728 quote_if_necessary(dst))
729 self.obj_dict['points'] = points
730 if obj_dict is None:
731 # Copy the attributes
732 self.obj_dict[ 'attributes' ] = dict( attrs )
733 self.obj_dict[ 'type' ] = 'edge'
734 self.obj_dict[ 'parent_graph' ] = None
735 self.obj_dict[ 'parent_edge_list' ] = None
736 self.obj_dict[ 'sequence' ] = None
737 else:
738 self.obj_dict = obj_dict
739 self.create_attribute_methods(EDGE_ATTRIBUTES)
740
741 def __str__(self):
742 return self.to_string()
743
744
745 def get_source(self):
746 """Get the edges source node name."""
747
748 return self.obj_dict['points'][0]
749
750
751 def get_destination(self):
752 """Get the edge's destination node name."""
753
754 return self.obj_dict['points'][1]
755
756
757 def __hash__(self):
758
759 return hash( hash(self.get_source()) +
760 hash(self.get_destination()) )
761
762
763 def __eq__(self, edge):
764 """Compare two edges.
765
766 If the parent graph is directed, arcs linking
767 node A to B are considered equal and A->B != B->A
768
769 If the parent graph is undirected, any edge
770 connecting two nodes is equal to any other
771 edge connecting the same nodes, A->B == B->A
772 """
773
774 if not isinstance(edge, Edge):
775 raise Error('Can not compare and '
776 'edge to a non-edge object.')
777
778 if self.get_parent_graph().get_top_graph_type() == 'graph':
779
780 # If the graph is undirected, the edge has neither
781 # source nor destination.
782 #
783 if ( ( self.get_source() == edge.get_source() and
784 self.get_destination() == edge.get_destination() ) or
785 ( edge.get_source() == self.get_destination() and
786 edge.get_destination() == self.get_source() ) ):
787 return True
788
789 else:
790
791 if (self.get_source()==edge.get_source() and
792 self.get_destination()==edge.get_destination()):
793 return True
794
795 return False
796
797 if not PY3:
798 def __ne__(self, other):
799 result = self.__eq__(other)
800 if result is NotImplemented:
801 return NotImplemented
802 return not result
803
804 def parse_node_ref(self, node_str):
805
806 if not isinstance(node_str, str):
807 return node_str
808
809 if node_str.startswith('"') and node_str.endswith('"'):
810
811 return node_str
812
813 node_port_idx = node_str.rfind(':')
814
815 if (node_port_idx>0 and node_str[0]=='"' and
816 node_str[node_port_idx-1]=='"'):
817
818 return node_str
819
820 if node_port_idx>0:
821
822 a = node_str[:node_port_idx]
823 b = node_str[node_port_idx+1:]
824
825 node = quote_if_necessary(a)
826
827 node += ':'+quote_if_necessary(b)
828
829 return node
830
831 return node_str
832
833
834 def to_string(self):
835 """Return string representation of edge in DOT language."""
836
837 src = self.parse_node_ref( self.get_source() )
838 dst = self.parse_node_ref( self.get_destination() )
839
840 if isinstance(src, frozendict):
841 edge = [ Subgraph(obj_dict=src).to_string() ]
842 elif isinstance(src, int):
843 edge = [ str(src) ]
844 else:
845 edge = [ src ]
846
847 if (self.get_parent_graph() and
848 self.get_parent_graph().get_top_graph_type() and
849 self.get_parent_graph().get_top_graph_type() == 'digraph' ):
850
851 edge.append( '->' )
852
853 else:
854 edge.append( '--' )
855
856 if isinstance(dst, frozendict):
857 edge.append( Subgraph(obj_dict=dst).to_string() )
858 elif isinstance(dst, int):
859 edge.append( str(dst) )
860 else:
861 edge.append( dst )
862
863
864 edge_attr = list()
865
866 for attr in sorted(self.obj_dict['attributes']):
867 value = self.obj_dict['attributes'][attr]
868 if value == '':
869 value = '""'
870 if value is not None:
871 edge_attr.append(
872 '%s=%s' % (attr, quote_if_necessary(value) ) )
873 else:
874 edge_attr.append( attr )
875
876 edge_attr = ', '.join(edge_attr)
877
878 if edge_attr:
879 edge.append( ' [' + edge_attr + ']' )
880
881 return ' '.join(edge) + ';'
882
883
884
885
886
887 class Graph(Common):
888 """Class representing a graph in Graphviz's dot language.
889
890 This class implements the methods to work on a representation
891 of a graph in Graphviz's dot language.
892
893 graph( graph_name='G', graph_type='digraph',
894 strict=False, suppress_disconnected=False, attribute=value, ...)
895
896 graph_name:
897 the graph's name
898 graph_type:
899 can be 'graph' or 'digraph'
900 suppress_disconnected:
901 defaults to False, which will remove from the
902 graph any disconnected nodes.
903 simplify:
904 if True it will avoid displaying equal edges, i.e.
905 only one edge between two nodes. removing the
906 duplicated ones.
907
908 All the attributes defined in the Graphviz dot language should
909 be supported.
910
911 Attributes can be set through the dynamically generated methods:
912
913 set_[attribute name], i.e. set_size, set_fontname
914
915 or using the instance's attributes:
916
917 Graph.obj_dict['attributes'][attribute name], i.e.
918
919 graph_instance.obj_dict['attributes']['label']
920 graph_instance.obj_dict['attributes']['fontname']
921 """
922
923
924 def __init__(self, graph_name='G', obj_dict=None,
925 graph_type='digraph', strict=False,
926 suppress_disconnected=False, simplify=False, **attrs):
927
928 if obj_dict is not None:
929 self.obj_dict = obj_dict
930
931 else:
932
933 self.obj_dict = dict()
934
935 self.obj_dict['attributes'] = dict(attrs)
936
937 if graph_type not in ['graph', 'digraph']:
938 raise Error((
939 'Invalid type "{t}". '
940 'Accepted graph types are: '
941 'graph, digraph').format(t=graph_type))
942
943
944 self.obj_dict['name'] = quote_if_necessary(graph_name)
945 self.obj_dict['type'] = graph_type
946
947 self.obj_dict['strict'] = strict
948 self.obj_dict['suppress_disconnected'] = suppress_disconnected
949 self.obj_dict['simplify'] = simplify
950
951 self.obj_dict['current_child_sequence'] = 1
952 self.obj_dict['nodes'] = dict()
953 self.obj_dict['edges'] = dict()
954 self.obj_dict['subgraphs'] = dict()
955
956 self.set_parent_graph(self)
957
958
959 self.create_attribute_methods(GRAPH_ATTRIBUTES)
960
961 def __str__(self):
962 return self.to_string()
963
964
965 def get_graph_type(self):
966
967 return self.obj_dict['type']
968
969
970 def get_top_graph_type(self):
971
972 parent = self
973 while True:
974 parent_ = parent.get_parent_graph()
975 if parent_ == parent:
976 break
977 parent = parent_
978
979 return parent.obj_dict['type']
980
981
982 def set_graph_defaults(self, **attrs):
983
984 self.add_node( Node('graph', **attrs) )
985
986
987 def get_graph_defaults(self, **attrs):
988
989 graph_nodes = self.get_node('graph')
990
991 if isinstance( graph_nodes, (list, tuple)):
992 return [ node.get_attributes() for node in graph_nodes ]
993
994 return graph_nodes.get_attributes()
995
996
997
998 def set_node_defaults(self, **attrs):
999 """Define default node attributes.
1000
1001 These attributes only apply to nodes added to the graph after
1002 calling this method.
1003 """
1004 self.add_node( Node('node', **attrs) )
1005
1006
1007 def get_node_defaults(self, **attrs):
1008
1009
1010 graph_nodes = self.get_node('node')
1011
1012 if isinstance( graph_nodes, (list, tuple)):
1013 return [ node.get_attributes() for node in graph_nodes ]
1014
1015 return graph_nodes.get_attributes()
1016
1017
1018 def set_edge_defaults(self, **attrs):
1019
1020 self.add_node( Node('edge', **attrs) )
1021
1022
1023
1024 def get_edge_defaults(self, **attrs):
1025
1026 graph_nodes = self.get_node('edge')
1027
1028 if isinstance( graph_nodes, (list, tuple)):
1029 return [ node.get_attributes() for node in graph_nodes ]
1030
1031 return graph_nodes.get_attributes()
1032
1033
1034
1035 def set_simplify(self, simplify):
1036 """Set whether to simplify or not.
1037
1038 If True it will avoid displaying equal edges, i.e.
1039 only one edge between two nodes. removing the
1040 duplicated ones.
1041 """
1042
1043 self.obj_dict['simplify'] = simplify
1044
1045
1046
1047 def get_simplify(self):
1048 """Get whether to simplify or not.
1049
1050 Refer to set_simplify for more information.
1051 """
1052
1053 return self.obj_dict['simplify']
1054
1055
1056 def set_type(self, graph_type):
1057 """Set the graph's type, 'graph' or 'digraph'."""
1058
1059 self.obj_dict['type'] = graph_type
1060
1061
1062
1063 def get_type(self):
1064 """Get the graph's type, 'graph' or 'digraph'."""
1065
1066 return self.obj_dict['type']
1067
1068
1069
1070 def set_name(self, graph_name):
1071 """Set the graph's name."""
1072
1073 self.obj_dict['name'] = graph_name
1074
1075
1076
1077 def get_name(self):
1078 """Get the graph's name."""
1079
1080 return self.obj_dict['name']
1081
1082
1083
1084 def set_strict(self, val):
1085 """Set graph to 'strict' mode.
1086
1087 This option is only valid for top level graphs.
1088 """
1089
1090 self.obj_dict['strict'] = val
1091
1092
1093
1094 def get_strict(self, val):
1095 """Get graph's 'strict' mode (True, False).
1096
1097 This option is only valid for top level graphs.
1098 """
1099
1100 return self.obj_dict['strict']
1101
1102
1103
1104 def set_suppress_disconnected(self, val):
1105 """Suppress disconnected nodes in the output graph.
1106
1107 This option will skip nodes in
1108 the graph with no incoming or outgoing
1109 edges. This option works also
1110 for subgraphs and has effect only in the
1111 current graph/subgraph.
1112 """
1113
1114 self.obj_dict['suppress_disconnected'] = val
1115
1116
1117
1118 def get_suppress_disconnected(self, val):
1119 """Get if suppress disconnected is set.
1120
1121 Refer to set_suppress_disconnected for more information.
1122 """
1123
1124 return self.obj_dict['suppress_disconnected']
1125
1126
1127 def get_next_sequence_number(self):
1128
1129 seq = self.obj_dict['current_child_sequence']
1130
1131 self.obj_dict['current_child_sequence'] += 1
1132
1133 return seq
1134
1135
1136
1137 def add_node(self, graph_node):
1138 """Adds a node object to the graph.
1139
1140 It takes a node object as its only argument and returns
1141 None.
1142 """
1143
1144 if not isinstance(graph_node, Node):
1145 raise TypeError(
1146 'add_node() received ' +
1147 'a non node class object: ' + str(graph_node))
1148
1149
1150 node = self.get_node(graph_node.get_name())
1151
1152 if not node:
1153
1154 self.obj_dict['nodes'][graph_node.get_name()] = [
1155 graph_node.obj_dict ]
1156
1157 #self.node_dict[graph_node.get_name()] = graph_node.attributes
1158 graph_node.set_parent_graph(self.get_parent_graph())
1159
1160 else:
1161
1162 self.obj_dict['nodes'][graph_node.get_name()].append(
1163 graph_node.obj_dict )
1164
1165 graph_node.set_sequence(self.get_next_sequence_number())
1166
1167
1168
1169 def del_node(self, name, index=None):
1170 """Delete a node from the graph.
1171
1172 Given a node's name all node(s) with that same name
1173 will be deleted if 'index' is not specified or set
1174 to None.
1175 If there are several nodes with that same name and
1176 'index' is given, only the node in that position
1177 will be deleted.
1178
1179 'index' should be an integer specifying the position
1180 of the node to delete. If index is larger than the
1181 number of nodes with that name, no action is taken.
1182
1183 If nodes are deleted it returns True. If no action
1184 is taken it returns False.
1185 """
1186
1187 if isinstance(name, Node):
1188 name = name.get_name()
1189
1190 if name in self.obj_dict['nodes']:
1191
1192 if (index is not None and
1193 index < len(self.obj_dict['nodes'][name])):
1194 del self.obj_dict['nodes'][name][index]
1195 return True
1196 else:
1197 del self.obj_dict['nodes'][name]
1198 return True
1199
1200 return False
1201
1202
1203 def get_node(self, name):
1204 """Retrieve a node from the graph.
1205
1206 Given a node's name the corresponding Node
1207 instance will be returned.
1208
1209 If one or more nodes exist with that name a list of
1210 Node instances is returned.
1211 An empty list is returned otherwise.
1212 """
1213
1214 match = list()
1215
1216 if name in self.obj_dict['nodes']:
1217
1218 match.extend(
1219 [Node(obj_dict=obj_dict)
1220 for obj_dict in self.obj_dict['nodes'][name]])
1221
1222 return match
1223
1224
1225 def get_nodes(self):
1226 """Get the list of Node instances."""
1227
1228 return self.get_node_list()
1229
1230
1231 def get_node_list(self):
1232 """Get the list of Node instances.
1233
1234 This method returns the list of Node instances
1235 composing the graph.
1236 """
1237
1238 node_objs = list()
1239
1240 for node in self.obj_dict['nodes']:
1241 obj_dict_list = self.obj_dict['nodes'][node]
1242 node_objs.extend( [ Node( obj_dict = obj_d )
1243 for obj_d in obj_dict_list ] )
1244
1245 return node_objs
1246
1247
1248
1249 def add_edge(self, graph_edge):
1250 """Adds an edge object to the graph.
1251
1252 It takes a edge object as its only argument and returns
1253 None.
1254 """
1255
1256 if not isinstance(graph_edge, Edge):
1257 raise TypeError(
1258 'add_edge() received a non edge class object: ' +
1259 str(graph_edge))
1260
1261 edge_points = ( graph_edge.get_source(),
1262 graph_edge.get_destination() )
1263
1264 if edge_points in self.obj_dict['edges']:
1265
1266 edge_list = self.obj_dict['edges'][edge_points]
1267 edge_list.append(graph_edge.obj_dict)
1268
1269 else:
1270
1271 self.obj_dict['edges'][edge_points] = [ graph_edge.obj_dict ]
1272
1273
1274 graph_edge.set_sequence( self.get_next_sequence_number() )
1275
1276 graph_edge.set_parent_graph( self.get_parent_graph() )
1277
1278
1279
1280 def del_edge(self, src_or_list, dst=None, index=None):
1281 """Delete an edge from the graph.
1282
1283 Given an edge's (source, destination) node names all
1284 matching edges(s) will be deleted if 'index' is not
1285 specified or set to None.
1286 If there are several matching edges and 'index' is
1287 given, only the edge in that position will be deleted.
1288
1289 'index' should be an integer specifying the position
1290 of the edge to delete. If index is larger than the
1291 number of matching edges, no action is taken.
1292
1293 If edges are deleted it returns True. If no action
1294 is taken it returns False.
1295 """
1296
1297 if isinstance( src_or_list, (list, tuple)):
1298 if dst is not None and isinstance(dst, int):
1299 index = dst
1300 src, dst = src_or_list
1301 else:
1302 src, dst = src_or_list, dst
1303
1304 if isinstance(src, Node):
1305 src = src.get_name()
1306
1307 if isinstance(dst, Node):
1308 dst = dst.get_name()
1309
1310 if (src, dst) in self.obj_dict['edges']:
1311
1312 if (index is not None and
1313 index < len(self.obj_dict['edges'][(src, dst)])):
1314 del self.obj_dict['edges'][(src, dst)][index]
1315 return True
1316 else:
1317 del self.obj_dict['edges'][(src, dst)]
1318 return True
1319
1320 return False
1321
1322
1323 def get_edge(self, src_or_list, dst=None):
1324 """Retrieved an edge from the graph.
1325
1326 Given an edge's source and destination the corresponding
1327 Edge instance(s) will be returned.
1328
1329 If one or more edges exist with that source and destination
1330 a list of Edge instances is returned.
1331 An empty list is returned otherwise.
1332 """
1333
1334 if isinstance( src_or_list, (list, tuple)) and dst is None:
1335 edge_points = tuple(src_or_list)
1336 edge_points_reverse = (edge_points[1], edge_points[0])
1337 else:
1338 edge_points = (src_or_list, dst)
1339 edge_points_reverse = (dst, src_or_list)
1340
1341 match = list()
1342
1343 if edge_points in self.obj_dict['edges'] or (
1344 self.get_top_graph_type() == 'graph' and
1345 edge_points_reverse in self.obj_dict['edges']):
1346
1347 edges_obj_dict = self.obj_dict['edges'].get(
1348 edge_points,
1349 self.obj_dict['edges'].get( edge_points_reverse, None ))
1350
1351 for edge_obj_dict in edges_obj_dict:
1352 match.append(
1353 Edge(edge_points[0],
1354 edge_points[1],
1355 obj_dict=edge_obj_dict))
1356
1357 return match
1358
1359
1360 def get_edges(self):
1361 return self.get_edge_list()
1362
1363
1364 def get_edge_list(self):
1365 """Get the list of Edge instances.
1366
1367 This method returns the list of Edge instances
1368 composing the graph.
1369 """
1370
1371 edge_objs = list()
1372
1373 for edge in self.obj_dict['edges']:
1374 obj_dict_list = self.obj_dict['edges'][edge]
1375 edge_objs.extend(
1376 [Edge(obj_dict=obj_d)
1377 for obj_d in obj_dict_list])
1378
1379 return edge_objs
1380
1381
1382
1383 def add_subgraph(self, sgraph):
1384 """Adds an subgraph object to the graph.
1385
1386 It takes a subgraph object as its only argument and returns
1387 None.
1388 """
1389
1390 if (not isinstance(sgraph, Subgraph) and
1391 not isinstance(sgraph, Cluster)):
1392 raise TypeError(
1393 'add_subgraph() received a non subgraph class object:' +
1394 str(sgraph))
1395
1396 if sgraph.get_name() in self.obj_dict['subgraphs']:
1397
1398 sgraph_list = self.obj_dict['subgraphs'][ sgraph.get_name() ]
1399 sgraph_list.append( sgraph.obj_dict )
1400
1401 else:
1402 self.obj_dict['subgraphs'][sgraph.get_name()] = [
1403 sgraph.obj_dict]
1404
1405 sgraph.set_sequence( self.get_next_sequence_number() )
1406
1407 sgraph.set_parent_graph( self.get_parent_graph() )
1408
1409
1410
1411
1412 def get_subgraph(self, name):
1413 """Retrieved a subgraph from the graph.
1414
1415 Given a subgraph's name the corresponding
1416 Subgraph instance will be returned.
1417
1418 If one or more subgraphs exist with the same name, a list of
1419 Subgraph instances is returned.
1420 An empty list is returned otherwise.
1421 """
1422
1423 match = list()
1424
1425 if name in self.obj_dict['subgraphs']:
1426
1427 sgraphs_obj_dict = self.obj_dict['subgraphs'].get( name )
1428
1429 for obj_dict_list in sgraphs_obj_dict:
1430 #match.extend( Subgraph( obj_dict = obj_d )
1431 # for obj_d in obj_dict_list )
1432 match.append( Subgraph( obj_dict = obj_dict_list ) )
1433
1434 return match
1435
1436
1437 def get_subgraphs(self):
1438
1439 return self.get_subgraph_list()
1440
1441
1442 def get_subgraph_list(self):
1443 """Get the list of Subgraph instances.
1444
1445 This method returns the list of Subgraph instances
1446 in the graph.
1447 """
1448
1449 sgraph_objs = list()
1450
1451 for sgraph in self.obj_dict['subgraphs']:
1452 obj_dict_list = self.obj_dict['subgraphs'][sgraph]
1453 sgraph_objs.extend(
1454 [Subgraph(obj_dict=obj_d)
1455 for obj_d in obj_dict_list])
1456
1457 return sgraph_objs
1458
1459
1460
1461 def set_parent_graph(self, parent_graph):
1462
1463 self.obj_dict['parent_graph'] = parent_graph
1464
1465 for k in self.obj_dict['nodes']:
1466 obj_list = self.obj_dict['nodes'][k]
1467 for obj in obj_list:
1468 obj['parent_graph'] = parent_graph
1469
1470 for k in self.obj_dict['edges']:
1471 obj_list = self.obj_dict['edges'][k]
1472 for obj in obj_list:
1473 obj['parent_graph'] = parent_graph
1474
1475 for k in self.obj_dict['subgraphs']:
1476 obj_list = self.obj_dict['subgraphs'][k]
1477 for obj in obj_list:
1478 Graph(obj_dict=obj).set_parent_graph(parent_graph)
1479
1480
1481
1482 def to_string(self):
1483 """Return string representation of graph in DOT language.
1484
1485 @return: graph and subelements
1486 @rtype: `str`
1487 """
1488
1489
1490 graph = list()
1491
1492 if self.obj_dict.get('strict', None) is not None:
1493
1494 if (self == self.get_parent_graph() and
1495 self.obj_dict['strict']):
1496
1497 graph.append('strict ')
1498
1499 graph_type = self.obj_dict['type']
1500 if (graph_type == 'subgraph' and
1501 not self.obj_dict.get('show_keyword', True)):
1502 graph_type = ''
1503 s = '{type} {name} {{\n'.format(
1504 type=graph_type,
1505 name=self.obj_dict['name'])
1506 graph.append(s)
1507
1508 for attr in sorted(self.obj_dict['attributes']):
1509
1510 if self.obj_dict['attributes'].get(attr, None) is not None:
1511
1512 val = self.obj_dict['attributes'].get(attr)
1513 if val == '':
1514 val = '""'
1515 if val is not None:
1516 graph.append('%s=%s' %
1517 (attr, quote_if_necessary(val)))
1518 else:
1519 graph.append( attr )
1520
1521 graph.append( ';\n' )
1522
1523
1524 edges_done = set()
1525
1526 edge_obj_dicts = list()
1527 for k in self.obj_dict['edges']:
1528 edge_obj_dicts.extend(self.obj_dict['edges'][k])
1529
1530 if edge_obj_dicts:
1531 edge_src_set, edge_dst_set = list(zip(
1532 *[obj['points'] for obj in edge_obj_dicts]))
1533 edge_src_set, edge_dst_set = set(edge_src_set), set(edge_dst_set)
1534 else:
1535 edge_src_set, edge_dst_set = set(), set()
1536
1537 node_obj_dicts = list()
1538 for k in self.obj_dict['nodes']:
1539 node_obj_dicts.extend(self.obj_dict['nodes'][k])
1540
1541 sgraph_obj_dicts = list()
1542 for k in self.obj_dict['subgraphs']:
1543 sgraph_obj_dicts.extend(self.obj_dict['subgraphs'][k])
1544
1545
1546 obj_list = [(obj['sequence'], obj)
1547 for obj in (edge_obj_dicts +
1548 node_obj_dicts + sgraph_obj_dicts) ]
1549 obj_list.sort(key=lambda x: x[0])
1550
1551 for idx, obj in obj_list:
1552
1553 if obj['type'] == 'node':
1554
1555 node = Node(obj_dict=obj)
1556
1557 if self.obj_dict.get('suppress_disconnected', False):
1558
1559 if (node.get_name() not in edge_src_set and
1560 node.get_name() not in edge_dst_set):
1561
1562 continue
1563
1564 graph.append( node.to_string()+'\n' )
1565
1566 elif obj['type'] == 'edge':
1567
1568 edge = Edge(obj_dict=obj)
1569
1570 if (self.obj_dict.get('simplify', False) and
1571 edge in edges_done):
1572 continue
1573
1574 graph.append( edge.to_string() + '\n' )
1575 edges_done.add(edge)
1576
1577 else:
1578
1579 sgraph = Subgraph(obj_dict=obj)
1580
1581 graph.append( sgraph.to_string()+'\n' )
1582
1583 graph.append( '}\n' )
1584
1585 return ''.join(graph)
1586
1587
1588
1589 class Subgraph(Graph):
1590
1591 """Class representing a subgraph in Graphviz's dot language.
1592
1593 This class implements the methods to work on a representation
1594 of a subgraph in Graphviz's dot language.
1595
1596 subgraph(graph_name='subG',
1597 suppress_disconnected=False,
1598 attribute=value,
1599 ...)
1600
1601 graph_name:
1602 the subgraph's name
1603 suppress_disconnected:
1604 defaults to false, which will remove from the
1605 subgraph any disconnected nodes.
1606 All the attributes defined in the Graphviz dot language should
1607 be supported.
1608
1609 Attributes can be set through the dynamically generated methods:
1610
1611 set_[attribute name], i.e. set_size, set_fontname
1612
1613 or using the instance's attributes:
1614
1615 Subgraph.obj_dict['attributes'][attribute name], i.e.
1616
1617 subgraph_instance.obj_dict['attributes']['label']
1618 subgraph_instance.obj_dict['attributes']['fontname']
1619 """
1620
1621
1622 # RMF: subgraph should have all the
1623 # attributes of graph so it can be passed
1624 # as a graph to all methods
1625 #
1626 def __init__(self, graph_name='',
1627 obj_dict=None, suppress_disconnected=False,
1628 simplify=False, **attrs):
1629
1630
1631 Graph.__init__(
1632 self, graph_name=graph_name, obj_dict=obj_dict,
1633 suppress_disconnected=suppress_disconnected,
1634 simplify=simplify, **attrs)
1635
1636 if obj_dict is None:
1637
1638 self.obj_dict['type'] = 'subgraph'
1639
1640
1641
1642
1643 class Cluster(Graph):
1644
1645 """Class representing a cluster in Graphviz's dot language.
1646
1647 This class implements the methods to work on a representation
1648 of a cluster in Graphviz's dot language.
1649
1650 cluster(graph_name='subG',
1651 suppress_disconnected=False,
1652 attribute=value,
1653 ...)
1654
1655 graph_name:
1656 the cluster's name
1657 (the string 'cluster' will be always prepended)
1658 suppress_disconnected:
1659 defaults to false, which will remove from the
1660 cluster any disconnected nodes.
1661 All the attributes defined in the Graphviz dot language should
1662 be supported.
1663
1664 Attributes can be set through the dynamically generated methods:
1665
1666 set_[attribute name], i.e. set_color, set_fontname
1667
1668 or using the instance's attributes:
1669
1670 Cluster.obj_dict['attributes'][attribute name], i.e.
1671
1672 cluster_instance.obj_dict['attributes']['label']
1673 cluster_instance.obj_dict['attributes']['fontname']
1674 """
1675
1676
1677 def __init__(self, graph_name='subG',
1678 obj_dict=None, suppress_disconnected=False,
1679 simplify=False, **attrs):
1680
1681 Graph.__init__(
1682 self, graph_name=graph_name, obj_dict=obj_dict,
1683 suppress_disconnected=suppress_disconnected,
1684 simplify=simplify, **attrs)
1685
1686 if obj_dict is None:
1687
1688 self.obj_dict['type'] = 'subgraph'
1689 self.obj_dict['name'] = quote_if_necessary('cluster_'+graph_name)
1690
1691 self.create_attribute_methods(CLUSTER_ATTRIBUTES)
1692
1693
1694
1695
1696
1697
1698 class Dot(Graph):
1699 """A container for handling a dot language file.
1700
1701 This class implements methods to write and process
1702 a dot language file. It is a derived class of
1703 the base class 'Graph'.
1704 """
1705
1706
1707
1708 def __init__(self, *argsl, **argsd):
1709 Graph.__init__(self, *argsl, **argsd)
1710
1711 self.shape_files = list()
1712 self.formats = [
1713 'canon', 'cmap', 'cmapx',
1714 'cmapx_np', 'dia', 'dot',
1715 'fig', 'gd', 'gd2', 'gif',
1716 'hpgl', 'imap', 'imap_np', 'ismap',
1717 'jpe', 'jpeg', 'jpg', 'mif',
1718 'mp', 'pcl', 'pdf', 'pic', 'plain',
1719 'plain-ext', 'png', 'ps', 'ps2',
1720 'svg', 'svgz', 'vml', 'vmlz',
1721 'vrml', 'vtx', 'wbmp', 'xdot', 'xlib']
1722
1723 self.prog = 'dot'
1724
1725 # Automatically creates all
1726 # the methods enabling the creation
1727 # of output in any of the supported formats.
1728 for frmt in self.formats:
1729 def new_method(
1730 f=frmt, prog=self.prog,
1731 encoding=None):
1732 """Refer to docstring of method `create`."""
1733 return self.create(
1734 format=f, prog=prog, encoding=encoding)
1735 name = 'create_{fmt}'.format(fmt=frmt)
1736 self.__setattr__(name, new_method)
1737
1738 for frmt in self.formats+['raw']:
1739 def new_method(
1740 path, f=frmt, prog=self.prog,
1741 encoding=None):
1742 """Refer to docstring of method `write.`"""
1743 self.write(
1744 path, format=f, prog=prog,
1745 encoding=encoding)
1746 name = 'write_{fmt}'.format(fmt=frmt)
1747 self.__setattr__(name, new_method)
1748
1749 def __getstate__(self):
1750
1751 dict = copy.copy(self.obj_dict)
1752
1753 return dict
1754
1755 def __setstate__(self, state):
1756
1757 self.obj_dict = state
1758
1759
1760 def set_shape_files(self, file_paths):
1761 """Add the paths of the required image files.
1762
1763 If the graph needs graphic objects to
1764 be used as shapes or otherwise
1765 those need to be in the same folder as
1766 the graph is going to be rendered
1767 from. Alternatively the absolute path to
1768 the files can be specified when
1769 including the graphics in the graph.
1770
1771 The files in the location pointed to by
1772 the path(s) specified as arguments
1773 to this method will be copied to
1774 the same temporary location where the
1775 graph is going to be rendered.
1776 """
1777
1778 if isinstance( file_paths, str_type):
1779 self.shape_files.append( file_paths )
1780
1781 if isinstance( file_paths, (list, tuple) ):
1782 self.shape_files.extend( file_paths )
1783
1784
1785 def set_prog(self, prog):
1786 """Sets the default program.
1787
1788 Sets the default program in charge of processing
1789 the dot file into a graph.
1790 """
1791 self.prog = prog
1792
1793
1794 def write(self, path, prog=None, format='raw', encoding=None):
1795 """Writes a graph to a file.
1796
1797 Given a filename 'path' it will open/create and truncate
1798 such file and write on it a representation of the graph
1799 defined by the dot object in the format specified by
1800 'format' and using the encoding specified by `encoding` for text.
1801 The format 'raw' is used to dump the string representation
1802 of the Dot object, without further processing.
1803 The output can be processed by any of graphviz tools, defined
1804 in 'prog', which defaults to 'dot'
1805 Returns True or False according to the success of the write
1806 operation.
1807
1808 There's also the preferred possibility of using:
1809
1810 write_'format'(path, prog='program')
1811
1812 which are automatically defined for all the supported formats.
1813 [write_ps(), write_gif(), write_dia(), ...]
1814
1815 The encoding is passed to `open` [1].
1816
1817 [1] https://docs.python.org/3/library/functions.html#open
1818 """
1819 if prog is None:
1820 prog = self.prog
1821 if format == 'raw':
1822 s = self.to_string()
1823 if not PY3:
1824 s = unicode(s)
1825 with io.open(path, mode='wt', encoding=encoding) as f:
1826 f.write(s)
1827 else:
1828 s = self.create(prog, format, encoding=encoding)
1829 with io.open(path, mode='wb') as f:
1830 f.write(s)
1831 return True
1832
1833 def create(self, prog=None, format='ps', encoding=None):
1834 """Creates and returns a binary image for the graph.
1835
1836 create will write the graph to a temporary dot file in the
1837 encoding specified by `encoding` and process it with the
1838 program given by 'prog' (which defaults to 'twopi'), reading
1839 the binary image output and return it as:
1840
1841 - `str` of bytes in Python 2
1842 - `bytes` in Python 3
1843
1844 There's also the preferred possibility of using:
1845
1846 create_'format'(prog='program')
1847
1848 which are automatically defined for all the supported formats,
1849 for example:
1850
1851 - `create_ps()`
1852 - `create_gif()`
1853 - `create_dia()`
1854
1855 If 'prog' is a list, instead of a string,
1856 then the fist item is expected to be the program name,
1857 followed by any optional command-line arguments for it:
1858
1859 [ 'twopi', '-Tdot', '-s10' ]
1860
1861
1862 @param prog: either:
1863
1864 - name of GraphViz executable that
1865 can be found in the `$PATH`, or
1866
1867 - absolute path to GraphViz executable.
1868
1869 If you have added GraphViz to the `$PATH` and
1870 use its executables as installed
1871 (without renaming any of them)
1872 then their names are:
1873
1874 - `'dot'`
1875 - `'twopi'`
1876 - `'neato'`
1877 - `'circo'`
1878 - `'fdp'`
1879 - `'sfdp'`
1880
1881 On Windows, these have the notorious ".exe" extension that,
1882 only for the above strings, will be added automatically.
1883
1884 The `$PATH` is inherited from `os.env['PATH']` and
1885 passed to `subprocess.Popen` using the `env` argument.
1886
1887 If you haven't added GraphViz to your `$PATH` on Windows,
1888 then you may want to give the absolute path to the
1889 executable (for example, to `dot.exe`) in `prog`.
1890 """
1891
1892 if prog is None:
1893 prog = self.prog
1894
1895 assert prog is not None
1896
1897 if isinstance(prog, (list, tuple)):
1898 prog, args = prog[0], prog[1:]
1899 else:
1900 args = []
1901
1902 # temp file
1903 tmp_fd, tmp_name = tempfile.mkstemp()
1904 os.close(tmp_fd)
1905 self.write(tmp_name, encoding=encoding)
1906 tmp_dir = os.path.dirname(tmp_name)
1907
1908 # For each of the image files...
1909 for img in self.shape_files:
1910 # Get its data
1911 f = open(img, 'rb')
1912 f_data = f.read()
1913 f.close()
1914 # And copy it under a file with the same name in
1915 # the temporary directory
1916 f = open(os.path.join(tmp_dir, os.path.basename(img)), 'wb')
1917 f.write(f_data)
1918 f.close()
1919
1920 arguments = ['-T{}'.format(format), ] + args + [tmp_name]
1921
1922 try:
1923 stdout_data, stderr_data, process = call_graphviz(
1924 program=prog,
1925 arguments=arguments,
1926 working_dir=tmp_dir,
1927 )
1928 except OSError as e:
1929 if e.errno == errno.ENOENT:
1930 args = list(e.args)
1931 args[1] = '"{prog}" not found in path.'.format(
1932 prog=prog)
1933 raise OSError(*args)
1934 else:
1935 raise
1936
1937 # clean file litter
1938 for img in self.shape_files:
1939 os.unlink(os.path.join(tmp_dir, os.path.basename(img)))
1940
1941 os.unlink(tmp_name)
1942
1943 if process.returncode != 0:
1944 message = (
1945 '"{prog}" with args {arguments} returned code: {code}\n\n'
1946 'stdout, stderr:\n {out}\n{err}\n'
1947 ).format(
1948 prog=prog,
1949 arguments=arguments,
1950 code=process.returncode,
1951 out=stdout_data,
1952 err=stderr_data,
1953 )
1954 print(message)
1955
1956 assert process.returncode == 0, (
1957 '"{prog}" with args {arguments} returned code: {code}'.format(
1958 prog=prog,
1959 arguments=arguments,
1960 code=process.returncode,
1961 )
1962 )
1963
1964 return stdout_data