Mercurial > repos > shellac > sam_consensus_v3
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 |