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 |
