12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964 |
- """An interface to GraphViz."""
- from __future__ import division
- from __future__ import print_function
- import copy
- import io
- import errno
- import os
- import re
- import subprocess
- import sys
- import tempfile
- import warnings
- try:
- import dot_parser
- except Exception as e:
- warnings.warn(
- "`pydot` could not import `dot_parser`, "
- "so `pydot` will be unable to parse DOT files. "
- "The error was: {e}".format(e=e))
- __author__ = 'Ero Carrera'
- __version__ = '1.4.2'
- __license__ = 'MIT'
- PY3 = sys.version_info >= (3, 0, 0)
- if PY3:
- str_type = str
- else:
- str_type = basestring
- GRAPH_ATTRIBUTES = { 'Damping', 'K', 'URL', 'aspect', 'bb', 'bgcolor',
- 'center', 'charset', 'clusterrank', 'colorscheme', 'comment', 'compound',
- 'concentrate', 'defaultdist', 'dim', 'dimen', 'diredgeconstraints',
- 'dpi', 'epsilon', 'esep', 'fontcolor', 'fontname', 'fontnames',
- 'fontpath', 'fontsize', 'id', 'label', 'labeljust', 'labelloc',
- 'landscape', 'layers', 'layersep', 'layout', 'levels', 'levelsgap',
- 'lheight', 'lp', 'lwidth', 'margin', 'maxiter', 'mclimit', 'mindist',
- 'mode', 'model', 'mosek', 'nodesep', 'nojustify', 'normalize', 'nslimit',
- 'nslimit1', 'ordering', 'orientation', 'outputorder', 'overlap',
- 'overlap_scaling', 'pack', 'packmode', 'pad', 'page', 'pagedir',
- 'quadtree', 'quantum', 'rankdir', 'ranksep', 'ratio', 'remincross',
- 'repulsiveforce', 'resolution', 'root', 'rotate', 'searchsize', 'sep',
- 'showboxes', 'size', 'smoothing', 'sortv', 'splines', 'start',
- 'stylesheet', 'target', 'truecolor', 'viewport', 'voro_margin',
- # for subgraphs
- 'rank' }
- EDGE_ATTRIBUTES = { 'URL', 'arrowhead', 'arrowsize', 'arrowtail',
- 'color', 'colorscheme', 'comment', 'constraint', 'decorate', 'dir',
- 'edgeURL', 'edgehref', 'edgetarget', 'edgetooltip', 'fontcolor',
- 'fontname', 'fontsize', 'headURL', 'headclip', 'headhref', 'headlabel',
- 'headport', 'headtarget', 'headtooltip', 'href', 'id', 'label',
- 'labelURL', 'labelangle', 'labeldistance', 'labelfloat', 'labelfontcolor',
- 'labelfontname', 'labelfontsize', 'labelhref', 'labeltarget',
- 'labeltooltip', 'layer', 'len', 'lhead', 'lp', 'ltail', 'minlen',
- 'nojustify', 'penwidth', 'pos', 'samehead', 'sametail', 'showboxes',
- 'style', 'tailURL', 'tailclip', 'tailhref', 'taillabel', 'tailport',
- 'tailtarget', 'tailtooltip', 'target', 'tooltip', 'weight',
- 'rank' }
- NODE_ATTRIBUTES = { 'URL', 'color', 'colorscheme', 'comment',
- 'distortion', 'fillcolor', 'fixedsize', 'fontcolor', 'fontname',
- 'fontsize', 'group', 'height', 'id', 'image', 'imagescale', 'label',
- 'labelloc', 'layer', 'margin', 'nojustify', 'orientation', 'penwidth',
- 'peripheries', 'pin', 'pos', 'rects', 'regular', 'root', 'samplepoints',
- 'shape', 'shapefile', 'showboxes', 'sides', 'skew', 'sortv', 'style',
- 'target', 'tooltip', 'vertices', 'width', 'z',
- # The following are attributes dot2tex
- 'texlbl', 'texmode' }
- CLUSTER_ATTRIBUTES = { 'K', 'URL', 'bgcolor', 'color', 'colorscheme',
- 'fillcolor', 'fontcolor', 'fontname', 'fontsize', 'label', 'labeljust',
- 'labelloc', 'lheight', 'lp', 'lwidth', 'nojustify', 'pencolor',
- 'penwidth', 'peripheries', 'sortv', 'style', 'target', 'tooltip' }
- DEFAULT_PROGRAMS = {
- 'dot',
- 'twopi',
- 'neato',
- 'circo',
- 'fdp',
- 'sfdp',
- }
- def is_windows():
- # type: () -> bool
- return os.name == 'nt'
- def is_anaconda():
- # type: () -> bool
- import glob
- return glob.glob(os.path.join(sys.prefix, 'conda-meta\\graphviz*.json')) != []
- def get_executable_extension():
- # type: () -> str
- if is_windows():
- return '.bat' if is_anaconda() else '.exe'
- else:
- return ''
- def call_graphviz(program, arguments, working_dir, **kwargs):
- # explicitly inherit `$PATH`, on Windows too,
- # with `shell=False`
- if program in DEFAULT_PROGRAMS:
- extension = get_executable_extension()
- program += extension
- if arguments is None:
- arguments = []
- env = {
- 'PATH': os.environ.get('PATH', ''),
- 'LD_LIBRARY_PATH': os.environ.get('LD_LIBRARY_PATH', ''),
- 'SYSTEMROOT': os.environ.get('SYSTEMROOT', ''),
- }
- program_with_args = [program, ] + arguments
- process = subprocess.Popen(
- program_with_args,
- env=env,
- cwd=working_dir,
- shell=False,
- stderr=subprocess.PIPE,
- stdout=subprocess.PIPE,
- **kwargs
- )
- stdout_data, stderr_data = process.communicate()
- return stdout_data, stderr_data, process
- #
- # Extended version of ASPN's Python Cookbook Recipe:
- # Frozen dictionaries.
- # https://code.activestate.com/recipes/414283/
- #
- # This version freezes dictionaries used as values within dictionaries.
- #
- class frozendict(dict):
- def _blocked_attribute(obj):
- raise AttributeError('A frozendict cannot be modified.')
- _blocked_attribute = property(_blocked_attribute)
- __delitem__ = __setitem__ = clear = _blocked_attribute
- pop = popitem = setdefault = update = _blocked_attribute
- def __new__(cls, *args, **kw):
- new = dict.__new__(cls)
- args_ = []
- for arg in args:
- if isinstance(arg, dict):
- arg = copy.copy(arg)
- for k in arg:
- v = arg[k]
- if isinstance(v, frozendict):
- arg[k] = v
- elif isinstance(v, dict):
- arg[k] = frozendict(v)
- elif isinstance(v, list):
- v_ = list()
- for elm in v:
- if isinstance(elm, dict):
- v_.append( frozendict(elm) )
- else:
- v_.append( elm )
- arg[k] = tuple(v_)
- args_.append( arg )
- else:
- args_.append( arg )
- dict.__init__(new, *args_, **kw)
- return new
- def __init__(self, *args, **kw):
- pass
- def __hash__(self):
- try:
- return self._cached_hash
- except AttributeError:
- h = self._cached_hash = hash(tuple(sorted(self.items())))
- return h
- def __repr__(self):
- return "frozendict(%s)" % dict.__repr__(self)
- dot_keywords = ['graph', 'subgraph', 'digraph', 'node', 'edge', 'strict']
- id_re_alpha_nums = re.compile('^[_a-zA-Z][a-zA-Z0-9_,]*$', re.UNICODE)
- id_re_alpha_nums_with_ports = re.compile(
- '^[_a-zA-Z][a-zA-Z0-9_,:\"]*[a-zA-Z0-9_,\"]+$', re.UNICODE)
- id_re_num = re.compile('^[0-9,]+$', re.UNICODE)
- id_re_with_port = re.compile('^([^:]*):([^:]*)$', re.UNICODE)
- id_re_dbl_quoted = re.compile('^\".*\"$', re.S|re.UNICODE)
- id_re_html = re.compile('^<.*>$', re.S|re.UNICODE)
- def needs_quotes( s ):
- """Checks whether a string is a dot language ID.
- It will check whether the string is solely composed
- by the characters allowed in an ID or not.
- If the string is one of the reserved keywords it will
- need quotes too but the user will need to add them
- manually.
- """
- # If the name is a reserved keyword it will need quotes but pydot
- # can't tell when it's being used as a keyword or when it's simply
- # a name. Hence the user needs to supply the quotes when an element
- # would use a reserved keyword as name. This function will return
- # false indicating that a keyword string, if provided as-is, won't
- # need quotes.
- if s in dot_keywords:
- return False
- chars = [ord(c) for c in s if ord(c)>0x7f or ord(c)==0]
- if chars and not id_re_dbl_quoted.match(s) and not id_re_html.match(s):
- return True
- for test_re in [id_re_alpha_nums, id_re_num,
- id_re_dbl_quoted, id_re_html,
- id_re_alpha_nums_with_ports]:
- if test_re.match(s):
- return False
- m = id_re_with_port.match(s)
- if m:
- return needs_quotes(m.group(1)) or needs_quotes(m.group(2))
- return True
- def quote_if_necessary(s):
- """Enclose attribute value in quotes, if needed."""
- if isinstance(s, bool):
- if s is True:
- return 'True'
- return 'False'
- if not isinstance( s, str_type):
- return s
- if not s:
- return s
- if needs_quotes(s):
- replace = {'"' : r'\"',
- "\n" : r'\n',
- "\r" : r'\r'}
- for (a,b) in replace.items():
- s = s.replace(a, b)
- return '"' + s + '"'
- return s
- def graph_from_dot_data(s):
- """Load graphs from DOT description in string `s`.
- @param s: string in [DOT language](
- https://en.wikipedia.org/wiki/DOT_(graph_description_language))
- @return: Graphs that result from parsing.
- @rtype: `list` of `pydot.Dot`
- """
- return dot_parser.parse_dot_data(s)
- def graph_from_dot_file(path, encoding=None):
- """Load graphs from DOT file at `path`.
- @param path: to DOT file
- @param encoding: as passed to `io.open`.
- For example, `'utf-8'`.
- @return: Graphs that result from parsing.
- @rtype: `list` of `pydot.Dot`
- """
- with io.open(path, 'rt', encoding=encoding) as f:
- s = f.read()
- if not PY3:
- s = unicode(s)
- graphs = graph_from_dot_data(s)
- return graphs
- def graph_from_edges(edge_list, node_prefix='', directed=False):
- """Creates a basic graph out of an edge list.
- The edge list has to be a list of tuples representing
- the nodes connected by the edge.
- The values can be anything: bool, int, float, str.
- If the graph is undirected by default, it is only
- calculated from one of the symmetric halves of the matrix.
- """
- if directed:
- graph = Dot(graph_type='digraph')
- else:
- graph = Dot(graph_type='graph')
- for edge in edge_list:
- if isinstance(edge[0], str):
- src = node_prefix + edge[0]
- else:
- src = node_prefix + str(edge[0])
- if isinstance(edge[1], str):
- dst = node_prefix + edge[1]
- else:
- dst = node_prefix + str(edge[1])
- e = Edge( src, dst )
- graph.add_edge(e)
- return graph
- def graph_from_adjacency_matrix(matrix, node_prefix= u'', directed=False):
- """Creates a basic graph out of an adjacency matrix.
- The matrix has to be a list of rows of values
- representing an adjacency matrix.
- The values can be anything: bool, int, float, as long
- as they can evaluate to True or False.
- """
- node_orig = 1
- if directed:
- graph = Dot(graph_type='digraph')
- else:
- graph = Dot(graph_type='graph')
- for row in matrix:
- if not directed:
- skip = matrix.index(row)
- r = row[skip:]
- else:
- skip = 0
- r = row
- node_dest = skip+1
- for e in r:
- if e:
- graph.add_edge(
- Edge('%s%s' % (node_prefix, node_orig),
- '%s%s' % (node_prefix, node_dest)))
- node_dest += 1
- node_orig += 1
- return graph
- def graph_from_incidence_matrix(matrix, node_prefix='', directed=False):
- """Creates a basic graph out of an incidence matrix.
- The matrix has to be a list of rows of values
- representing an incidence matrix.
- The values can be anything: bool, int, float, as long
- as they can evaluate to True or False.
- """
- node_orig = 1
- if directed:
- graph = Dot(graph_type='digraph')
- else:
- graph = Dot(graph_type='graph')
- for row in matrix:
- nodes = []
- c = 1
- for node in row:
- if node:
- nodes.append(c*node)
- c += 1
- nodes.sort()
- if len(nodes) == 2:
- graph.add_edge(
- Edge('%s%s' % (node_prefix, abs(nodes[0])),
- '%s%s' % (node_prefix, nodes[1])))
- if not directed:
- graph.set_simplify(True)
- return graph
- class Common(object):
- """Common information to several classes.
- Should not be directly used, several classes are derived from
- this one.
- """
- def __getstate__(self):
- dict = copy.copy(self.obj_dict)
- return dict
- def __setstate__(self, state):
- self.obj_dict = state
- def __get_attribute__(self, attr):
- """Look for default attributes for this node"""
- attr_val = self.obj_dict['attributes'].get(attr, None)
- if attr_val is None:
- # get the defaults for nodes/edges
- default_node_name = self.obj_dict['type']
- # The defaults for graphs are set on a node named 'graph'
- if default_node_name in ('subgraph', 'digraph', 'cluster'):
- default_node_name = 'graph'
- g = self.get_parent_graph()
- if g is not None:
- defaults = g.get_node( default_node_name )
- else:
- return None
- # Multiple defaults could be set by having repeated 'graph [...]'
- # 'node [...]', 'edge [...]' statements. In such case, if the
- # same attribute is set in different statements, only the first
- # will be returned. In order to get all, one would call the
- # get_*_defaults() methods and handle those. Or go node by node
- # (of the ones specifying defaults) and modify the attributes
- # individually.
- #
- if not isinstance(defaults, (list, tuple)):
- defaults = [defaults]
- for default in defaults:
- attr_val = default.obj_dict['attributes'].get(attr, None)
- if attr_val:
- return attr_val
- else:
- return attr_val
- return None
- def set_parent_graph(self, parent_graph):
- self.obj_dict['parent_graph'] = parent_graph
- def get_parent_graph(self):
- return self.obj_dict.get('parent_graph', None)
- def set(self, name, value):
- """Set an attribute value by name.
- Given an attribute 'name' it will set its value to 'value'.
- There's always the possibility of using the methods:
- set_'name'(value)
- which are defined for all the existing attributes.
- """
- self.obj_dict['attributes'][name] = value
- def get(self, name):
- """Get an attribute value by name.
- Given an attribute 'name' it will get its value.
- There's always the possibility of using the methods:
- get_'name'()
- which are defined for all the existing attributes.
- """
- return self.obj_dict['attributes'].get(name, None)
- def get_attributes(self):
- """"""
- return self.obj_dict['attributes']
- def set_sequence(self, seq):
- self.obj_dict['sequence'] = seq
- def get_sequence(self):
- return self.obj_dict['sequence']
- def create_attribute_methods(self, obj_attributes):
- #for attr in self.obj_dict['attributes']:
- for attr in obj_attributes:
- # Generate all the Setter methods.
- #
- self.__setattr__(
- 'set_'+attr,
- lambda x, a=attr :
- self.obj_dict['attributes'].__setitem__(a, x) )
- # Generate all the Getter methods.
- #
- self.__setattr__(
- 'get_'+attr, lambda a=attr : self.__get_attribute__(a))
- class Error(Exception):
- """General error handling class.
- """
- def __init__(self, value):
- self.value = value
- def __str__(self):
- return self.value
- class InvocationException(Exception):
- """Indicate problem while running any GraphViz executable.
- """
- def __init__(self, value):
- self.value = value
- def __str__(self):
- return self.value
- class Node(Common):
- """A graph node.
- This class represents a graph's node with all its attributes.
- node(name, attribute=value, ...)
- name: node's name
- All the attributes defined in the Graphviz dot language should
- be supported.
- """
- def __init__(self, name = '', obj_dict = None, **attrs):
- #
- # Nodes will take attributes of
- # all other types because the defaults
- # for any GraphViz object are dealt with
- # as if they were Node definitions
- #
- if obj_dict is not None:
- self.obj_dict = obj_dict
- else:
- self.obj_dict = dict()
- # Copy the attributes
- #
- self.obj_dict[ 'attributes' ] = dict( attrs )
- self.obj_dict[ 'type' ] = 'node'
- self.obj_dict[ 'parent_graph' ] = None
- self.obj_dict[ 'parent_node_list' ] = None
- self.obj_dict[ 'sequence' ] = None
- # Remove the compass point
- #
- port = None
- if isinstance(name, str_type) and not name.startswith('"'):
- idx = name.find(':')
- if idx > 0 and idx+1 < len(name):
- name, port = name[:idx], name[idx:]
- if isinstance(name, int):
- name = str(name)
- self.obj_dict['name'] = quote_if_necessary(name)
- self.obj_dict['port'] = port
- self.create_attribute_methods(NODE_ATTRIBUTES)
- def __str__(self):
- return self.to_string()
- def set_name(self, node_name):
- """Set the node's name."""
- self.obj_dict['name'] = node_name
- def get_name(self):
- """Get the node's name."""
- return self.obj_dict['name']
- def get_port(self):
- """Get the node's port."""
- return self.obj_dict['port']
- def add_style(self, style):
- styles = self.obj_dict['attributes'].get('style', None)
- if not styles and style:
- styles = [ style ]
- else:
- styles = styles.split(',')
- styles.append( style )
- self.obj_dict['attributes']['style'] = ','.join( styles )
- def to_string(self):
- """Return string representation of node in DOT language."""
- # RMF: special case defaults for node, edge and graph properties.
- #
- node = quote_if_necessary(self.obj_dict['name'])
- node_attr = list()
- for attr in sorted(self.obj_dict['attributes']):
- value = self.obj_dict['attributes'][attr]
- if value == '':
- value = '""'
- if value is not None:
- node_attr.append(
- '%s=%s' % (attr, quote_if_necessary(value) ) )
- else:
- node_attr.append( attr )
- # No point in having nodes setting any defaults if the don't set
- # any attributes...
- #
- if node in ('graph', 'node', 'edge') and len(node_attr) == 0:
- return ''
- node_attr = ', '.join(node_attr)
- if node_attr:
- node += ' [' + node_attr + ']'
- return node + ';'
- class Edge(Common):
- """A graph edge.
- This class represents a graph's edge with all its attributes.
- edge(src, dst, attribute=value, ...)
- src: source node, subgraph or cluster
- dst: destination node, subgraph or cluster
- `src` and `dst` can be specified as a `Node`, `Subgraph` or
- `Cluster` object, or as the name string of such a component.
- All the attributes defined in the Graphviz dot language should
- be supported.
- Attributes can be set through the dynamically generated methods:
- set_[attribute name], i.e. set_label, set_fontname
- or directly by using the instance's special dictionary:
- Edge.obj_dict['attributes'][attribute name], i.e.
- edge_instance.obj_dict['attributes']['label']
- edge_instance.obj_dict['attributes']['fontname']
- """
- def __init__(self, src='', dst='', obj_dict=None, **attrs):
- self.obj_dict = dict()
- if isinstance(src, (Node, Subgraph, Cluster)):
- src = src.get_name()
- if isinstance(dst, (Node, Subgraph, Cluster)):
- dst = dst.get_name()
- points = (quote_if_necessary(src),
- quote_if_necessary(dst))
- self.obj_dict['points'] = points
- if obj_dict is None:
- # Copy the attributes
- self.obj_dict[ 'attributes' ] = dict( attrs )
- self.obj_dict[ 'type' ] = 'edge'
- self.obj_dict[ 'parent_graph' ] = None
- self.obj_dict[ 'parent_edge_list' ] = None
- self.obj_dict[ 'sequence' ] = None
- else:
- self.obj_dict = obj_dict
- self.create_attribute_methods(EDGE_ATTRIBUTES)
- def __str__(self):
- return self.to_string()
- def get_source(self):
- """Get the edges source node name."""
- return self.obj_dict['points'][0]
- def get_destination(self):
- """Get the edge's destination node name."""
- return self.obj_dict['points'][1]
- def __hash__(self):
- return hash( hash(self.get_source()) +
- hash(self.get_destination()) )
- def __eq__(self, edge):
- """Compare two edges.
- If the parent graph is directed, arcs linking
- node A to B are considered equal and A->B != B->A
- If the parent graph is undirected, any edge
- connecting two nodes is equal to any other
- edge connecting the same nodes, A->B == B->A
- """
- if not isinstance(edge, Edge):
- raise Error('Can not compare and '
- 'edge to a non-edge object.')
- if self.get_parent_graph().get_top_graph_type() == 'graph':
- # If the graph is undirected, the edge has neither
- # source nor destination.
- #
- if ( ( self.get_source() == edge.get_source() and
- self.get_destination() == edge.get_destination() ) or
- ( edge.get_source() == self.get_destination() and
- edge.get_destination() == self.get_source() ) ):
- return True
- else:
- if (self.get_source()==edge.get_source() and
- self.get_destination()==edge.get_destination()):
- return True
- return False
- if not PY3:
- def __ne__(self, other):
- result = self.__eq__(other)
- if result is NotImplemented:
- return NotImplemented
- return not result
- def parse_node_ref(self, node_str):
- if not isinstance(node_str, str):
- return node_str
- if node_str.startswith('"') and node_str.endswith('"'):
- return node_str
- node_port_idx = node_str.rfind(':')
- if (node_port_idx>0 and node_str[0]=='"' and
- node_str[node_port_idx-1]=='"'):
- return node_str
- if node_port_idx>0:
- a = node_str[:node_port_idx]
- b = node_str[node_port_idx+1:]
- node = quote_if_necessary(a)
- node += ':'+quote_if_necessary(b)
- return node
- return node_str
- def to_string(self):
- """Return string representation of edge in DOT language."""
- src = self.parse_node_ref( self.get_source() )
- dst = self.parse_node_ref( self.get_destination() )
- if isinstance(src, frozendict):
- edge = [ Subgraph(obj_dict=src).to_string() ]
- elif isinstance(src, int):
- edge = [ str(src) ]
- else:
- edge = [ src ]
- if (self.get_parent_graph() and
- self.get_parent_graph().get_top_graph_type() and
- self.get_parent_graph().get_top_graph_type() == 'digraph' ):
- edge.append( '->' )
- else:
- edge.append( '--' )
- if isinstance(dst, frozendict):
- edge.append( Subgraph(obj_dict=dst).to_string() )
- elif isinstance(dst, int):
- edge.append( str(dst) )
- else:
- edge.append( dst )
- edge_attr = list()
- for attr in sorted(self.obj_dict['attributes']):
- value = self.obj_dict['attributes'][attr]
- if value == '':
- value = '""'
- if value is not None:
- edge_attr.append(
- '%s=%s' % (attr, quote_if_necessary(value) ) )
- else:
- edge_attr.append( attr )
- edge_attr = ', '.join(edge_attr)
- if edge_attr:
- edge.append( ' [' + edge_attr + ']' )
- return ' '.join(edge) + ';'
- class Graph(Common):
- """Class representing a graph in Graphviz's dot language.
- This class implements the methods to work on a representation
- of a graph in Graphviz's dot language.
- graph( graph_name='G', graph_type='digraph',
- strict=False, suppress_disconnected=False, attribute=value, ...)
- graph_name:
- the graph's name
- graph_type:
- can be 'graph' or 'digraph'
- suppress_disconnected:
- defaults to False, which will remove from the
- graph any disconnected nodes.
- simplify:
- if True it will avoid displaying equal edges, i.e.
- only one edge between two nodes. removing the
- duplicated ones.
- All the attributes defined in the Graphviz dot language should
- be supported.
- Attributes can be set through the dynamically generated methods:
- set_[attribute name], i.e. set_size, set_fontname
- or using the instance's attributes:
- Graph.obj_dict['attributes'][attribute name], i.e.
- graph_instance.obj_dict['attributes']['label']
- graph_instance.obj_dict['attributes']['fontname']
- """
- def __init__(self, graph_name='G', obj_dict=None,
- graph_type='digraph', strict=False,
- suppress_disconnected=False, simplify=False, **attrs):
- if obj_dict is not None:
- self.obj_dict = obj_dict
- else:
- self.obj_dict = dict()
- self.obj_dict['attributes'] = dict(attrs)
- if graph_type not in ['graph', 'digraph']:
- raise Error((
- 'Invalid type "{t}". '
- 'Accepted graph types are: '
- 'graph, digraph').format(t=graph_type))
- self.obj_dict['name'] = quote_if_necessary(graph_name)
- self.obj_dict['type'] = graph_type
- self.obj_dict['strict'] = strict
- self.obj_dict['suppress_disconnected'] = suppress_disconnected
- self.obj_dict['simplify'] = simplify
- self.obj_dict['current_child_sequence'] = 1
- self.obj_dict['nodes'] = dict()
- self.obj_dict['edges'] = dict()
- self.obj_dict['subgraphs'] = dict()
- self.set_parent_graph(self)
- self.create_attribute_methods(GRAPH_ATTRIBUTES)
- def __str__(self):
- return self.to_string()
- def get_graph_type(self):
- return self.obj_dict['type']
- def get_top_graph_type(self):
- parent = self
- while True:
- parent_ = parent.get_parent_graph()
- if parent_ == parent:
- break
- parent = parent_
- return parent.obj_dict['type']
- def set_graph_defaults(self, **attrs):
- self.add_node( Node('graph', **attrs) )
- def get_graph_defaults(self, **attrs):
- graph_nodes = self.get_node('graph')
- if isinstance( graph_nodes, (list, tuple)):
- return [ node.get_attributes() for node in graph_nodes ]
- return graph_nodes.get_attributes()
- def set_node_defaults(self, **attrs):
- """Define default node attributes.
- These attributes only apply to nodes added to the graph after
- calling this method.
- """
- self.add_node( Node('node', **attrs) )
- def get_node_defaults(self, **attrs):
- graph_nodes = self.get_node('node')
- if isinstance( graph_nodes, (list, tuple)):
- return [ node.get_attributes() for node in graph_nodes ]
- return graph_nodes.get_attributes()
- def set_edge_defaults(self, **attrs):
- self.add_node( Node('edge', **attrs) )
- def get_edge_defaults(self, **attrs):
- graph_nodes = self.get_node('edge')
- if isinstance( graph_nodes, (list, tuple)):
- return [ node.get_attributes() for node in graph_nodes ]
- return graph_nodes.get_attributes()
- def set_simplify(self, simplify):
- """Set whether to simplify or not.
- If True it will avoid displaying equal edges, i.e.
- only one edge between two nodes. removing the
- duplicated ones.
- """
- self.obj_dict['simplify'] = simplify
- def get_simplify(self):
- """Get whether to simplify or not.
- Refer to set_simplify for more information.
- """
- return self.obj_dict['simplify']
- def set_type(self, graph_type):
- """Set the graph's type, 'graph' or 'digraph'."""
- self.obj_dict['type'] = graph_type
- def get_type(self):
- """Get the graph's type, 'graph' or 'digraph'."""
- return self.obj_dict['type']
- def set_name(self, graph_name):
- """Set the graph's name."""
- self.obj_dict['name'] = graph_name
- def get_name(self):
- """Get the graph's name."""
- return self.obj_dict['name']
- def set_strict(self, val):
- """Set graph to 'strict' mode.
- This option is only valid for top level graphs.
- """
- self.obj_dict['strict'] = val
- def get_strict(self, val):
- """Get graph's 'strict' mode (True, False).
- This option is only valid for top level graphs.
- """
- return self.obj_dict['strict']
- def set_suppress_disconnected(self, val):
- """Suppress disconnected nodes in the output graph.
- This option will skip nodes in
- the graph with no incoming or outgoing
- edges. This option works also
- for subgraphs and has effect only in the
- current graph/subgraph.
- """
- self.obj_dict['suppress_disconnected'] = val
- def get_suppress_disconnected(self, val):
- """Get if suppress disconnected is set.
- Refer to set_suppress_disconnected for more information.
- """
- return self.obj_dict['suppress_disconnected']
- def get_next_sequence_number(self):
- seq = self.obj_dict['current_child_sequence']
- self.obj_dict['current_child_sequence'] += 1
- return seq
- def add_node(self, graph_node):
- """Adds a node object to the graph.
- It takes a node object as its only argument and returns
- None.
- """
- if not isinstance(graph_node, Node):
- raise TypeError(
- 'add_node() received ' +
- 'a non node class object: ' + str(graph_node))
- node = self.get_node(graph_node.get_name())
- if not node:
- self.obj_dict['nodes'][graph_node.get_name()] = [
- graph_node.obj_dict ]
- #self.node_dict[graph_node.get_name()] = graph_node.attributes
- graph_node.set_parent_graph(self.get_parent_graph())
- else:
- self.obj_dict['nodes'][graph_node.get_name()].append(
- graph_node.obj_dict )
- graph_node.set_sequence(self.get_next_sequence_number())
- def del_node(self, name, index=None):
- """Delete a node from the graph.
- Given a node's name all node(s) with that same name
- will be deleted if 'index' is not specified or set
- to None.
- If there are several nodes with that same name and
- 'index' is given, only the node in that position
- will be deleted.
- 'index' should be an integer specifying the position
- of the node to delete. If index is larger than the
- number of nodes with that name, no action is taken.
- If nodes are deleted it returns True. If no action
- is taken it returns False.
- """
- if isinstance(name, Node):
- name = name.get_name()
- if name in self.obj_dict['nodes']:
- if (index is not None and
- index < len(self.obj_dict['nodes'][name])):
- del self.obj_dict['nodes'][name][index]
- return True
- else:
- del self.obj_dict['nodes'][name]
- return True
- return False
- def get_node(self, name):
- """Retrieve a node from the graph.
- Given a node's name the corresponding Node
- instance will be returned.
- If one or more nodes exist with that name a list of
- Node instances is returned.
- An empty list is returned otherwise.
- """
- match = list()
- if name in self.obj_dict['nodes']:
- match.extend(
- [Node(obj_dict=obj_dict)
- for obj_dict in self.obj_dict['nodes'][name]])
- return match
- def get_nodes(self):
- """Get the list of Node instances."""
- return self.get_node_list()
- def get_node_list(self):
- """Get the list of Node instances.
- This method returns the list of Node instances
- composing the graph.
- """
- node_objs = list()
- for node in self.obj_dict['nodes']:
- obj_dict_list = self.obj_dict['nodes'][node]
- node_objs.extend( [ Node( obj_dict = obj_d )
- for obj_d in obj_dict_list ] )
- return node_objs
- def add_edge(self, graph_edge):
- """Adds an edge object to the graph.
- It takes a edge object as its only argument and returns
- None.
- """
- if not isinstance(graph_edge, Edge):
- raise TypeError(
- 'add_edge() received a non edge class object: ' +
- str(graph_edge))
- edge_points = ( graph_edge.get_source(),
- graph_edge.get_destination() )
- if edge_points in self.obj_dict['edges']:
- edge_list = self.obj_dict['edges'][edge_points]
- edge_list.append(graph_edge.obj_dict)
- else:
- self.obj_dict['edges'][edge_points] = [ graph_edge.obj_dict ]
- graph_edge.set_sequence( self.get_next_sequence_number() )
- graph_edge.set_parent_graph( self.get_parent_graph() )
- def del_edge(self, src_or_list, dst=None, index=None):
- """Delete an edge from the graph.
- Given an edge's (source, destination) node names all
- matching edges(s) will be deleted if 'index' is not
- specified or set to None.
- If there are several matching edges and 'index' is
- given, only the edge in that position will be deleted.
- 'index' should be an integer specifying the position
- of the edge to delete. If index is larger than the
- number of matching edges, no action is taken.
- If edges are deleted it returns True. If no action
- is taken it returns False.
- """
- if isinstance( src_or_list, (list, tuple)):
- if dst is not None and isinstance(dst, int):
- index = dst
- src, dst = src_or_list
- else:
- src, dst = src_or_list, dst
- if isinstance(src, Node):
- src = src.get_name()
- if isinstance(dst, Node):
- dst = dst.get_name()
- if (src, dst) in self.obj_dict['edges']:
- if (index is not None and
- index < len(self.obj_dict['edges'][(src, dst)])):
- del self.obj_dict['edges'][(src, dst)][index]
- return True
- else:
- del self.obj_dict['edges'][(src, dst)]
- return True
- return False
- def get_edge(self, src_or_list, dst=None):
- """Retrieved an edge from the graph.
- Given an edge's source and destination the corresponding
- Edge instance(s) will be returned.
- If one or more edges exist with that source and destination
- a list of Edge instances is returned.
- An empty list is returned otherwise.
- """
- if isinstance( src_or_list, (list, tuple)) and dst is None:
- edge_points = tuple(src_or_list)
- edge_points_reverse = (edge_points[1], edge_points[0])
- else:
- edge_points = (src_or_list, dst)
- edge_points_reverse = (dst, src_or_list)
- match = list()
- if edge_points in self.obj_dict['edges'] or (
- self.get_top_graph_type() == 'graph' and
- edge_points_reverse in self.obj_dict['edges']):
- edges_obj_dict = self.obj_dict['edges'].get(
- edge_points,
- self.obj_dict['edges'].get( edge_points_reverse, None ))
- for edge_obj_dict in edges_obj_dict:
- match.append(
- Edge(edge_points[0],
- edge_points[1],
- obj_dict=edge_obj_dict))
- return match
- def get_edges(self):
- return self.get_edge_list()
- def get_edge_list(self):
- """Get the list of Edge instances.
- This method returns the list of Edge instances
- composing the graph.
- """
- edge_objs = list()
- for edge in self.obj_dict['edges']:
- obj_dict_list = self.obj_dict['edges'][edge]
- edge_objs.extend(
- [Edge(obj_dict=obj_d)
- for obj_d in obj_dict_list])
- return edge_objs
- def add_subgraph(self, sgraph):
- """Adds an subgraph object to the graph.
- It takes a subgraph object as its only argument and returns
- None.
- """
- if (not isinstance(sgraph, Subgraph) and
- not isinstance(sgraph, Cluster)):
- raise TypeError(
- 'add_subgraph() received a non subgraph class object:' +
- str(sgraph))
- if sgraph.get_name() in self.obj_dict['subgraphs']:
- sgraph_list = self.obj_dict['subgraphs'][ sgraph.get_name() ]
- sgraph_list.append( sgraph.obj_dict )
- else:
- self.obj_dict['subgraphs'][sgraph.get_name()] = [
- sgraph.obj_dict]
- sgraph.set_sequence( self.get_next_sequence_number() )
- sgraph.set_parent_graph( self.get_parent_graph() )
- def get_subgraph(self, name):
- """Retrieved a subgraph from the graph.
- Given a subgraph's name the corresponding
- Subgraph instance will be returned.
- If one or more subgraphs exist with the same name, a list of
- Subgraph instances is returned.
- An empty list is returned otherwise.
- """
- match = list()
- if name in self.obj_dict['subgraphs']:
- sgraphs_obj_dict = self.obj_dict['subgraphs'].get( name )
- for obj_dict_list in sgraphs_obj_dict:
- #match.extend( Subgraph( obj_dict = obj_d )
- # for obj_d in obj_dict_list )
- match.append( Subgraph( obj_dict = obj_dict_list ) )
- return match
- def get_subgraphs(self):
- return self.get_subgraph_list()
- def get_subgraph_list(self):
- """Get the list of Subgraph instances.
- This method returns the list of Subgraph instances
- in the graph.
- """
- sgraph_objs = list()
- for sgraph in self.obj_dict['subgraphs']:
- obj_dict_list = self.obj_dict['subgraphs'][sgraph]
- sgraph_objs.extend(
- [Subgraph(obj_dict=obj_d)
- for obj_d in obj_dict_list])
- return sgraph_objs
- def set_parent_graph(self, parent_graph):
- self.obj_dict['parent_graph'] = parent_graph
- for k in self.obj_dict['nodes']:
- obj_list = self.obj_dict['nodes'][k]
- for obj in obj_list:
- obj['parent_graph'] = parent_graph
- for k in self.obj_dict['edges']:
- obj_list = self.obj_dict['edges'][k]
- for obj in obj_list:
- obj['parent_graph'] = parent_graph
- for k in self.obj_dict['subgraphs']:
- obj_list = self.obj_dict['subgraphs'][k]
- for obj in obj_list:
- Graph(obj_dict=obj).set_parent_graph(parent_graph)
- def to_string(self):
- """Return string representation of graph in DOT language.
- @return: graph and subelements
- @rtype: `str`
- """
- graph = list()
- if self.obj_dict.get('strict', None) is not None:
- if (self == self.get_parent_graph() and
- self.obj_dict['strict']):
- graph.append('strict ')
- graph_type = self.obj_dict['type']
- if (graph_type == 'subgraph' and
- not self.obj_dict.get('show_keyword', True)):
- graph_type = ''
- s = '{type} {name} {{\n'.format(
- type=graph_type,
- name=self.obj_dict['name'])
- graph.append(s)
- for attr in sorted(self.obj_dict['attributes']):
- if self.obj_dict['attributes'].get(attr, None) is not None:
- val = self.obj_dict['attributes'].get(attr)
- if val == '':
- val = '""'
- if val is not None:
- graph.append('%s=%s' %
- (attr, quote_if_necessary(val)))
- else:
- graph.append( attr )
- graph.append( ';\n' )
- edges_done = set()
- edge_obj_dicts = list()
- for k in self.obj_dict['edges']:
- edge_obj_dicts.extend(self.obj_dict['edges'][k])
- if edge_obj_dicts:
- edge_src_set, edge_dst_set = list(zip(
- *[obj['points'] for obj in edge_obj_dicts]))
- edge_src_set, edge_dst_set = set(edge_src_set), set(edge_dst_set)
- else:
- edge_src_set, edge_dst_set = set(), set()
- node_obj_dicts = list()
- for k in self.obj_dict['nodes']:
- node_obj_dicts.extend(self.obj_dict['nodes'][k])
- sgraph_obj_dicts = list()
- for k in self.obj_dict['subgraphs']:
- sgraph_obj_dicts.extend(self.obj_dict['subgraphs'][k])
- obj_list = [(obj['sequence'], obj)
- for obj in (edge_obj_dicts +
- node_obj_dicts + sgraph_obj_dicts) ]
- obj_list.sort(key=lambda x: x[0])
- for idx, obj in obj_list:
- if obj['type'] == 'node':
- node = Node(obj_dict=obj)
- if self.obj_dict.get('suppress_disconnected', False):
- if (node.get_name() not in edge_src_set and
- node.get_name() not in edge_dst_set):
- continue
- graph.append( node.to_string()+'\n' )
- elif obj['type'] == 'edge':
- edge = Edge(obj_dict=obj)
- if (self.obj_dict.get('simplify', False) and
- edge in edges_done):
- continue
- graph.append( edge.to_string() + '\n' )
- edges_done.add(edge)
- else:
- sgraph = Subgraph(obj_dict=obj)
- graph.append( sgraph.to_string()+'\n' )
- graph.append( '}\n' )
- return ''.join(graph)
- class Subgraph(Graph):
- """Class representing a subgraph in Graphviz's dot language.
- This class implements the methods to work on a representation
- of a subgraph in Graphviz's dot language.
- subgraph(graph_name='subG',
- suppress_disconnected=False,
- attribute=value,
- ...)
- graph_name:
- the subgraph's name
- suppress_disconnected:
- defaults to false, which will remove from the
- subgraph any disconnected nodes.
- All the attributes defined in the Graphviz dot language should
- be supported.
- Attributes can be set through the dynamically generated methods:
- set_[attribute name], i.e. set_size, set_fontname
- or using the instance's attributes:
- Subgraph.obj_dict['attributes'][attribute name], i.e.
- subgraph_instance.obj_dict['attributes']['label']
- subgraph_instance.obj_dict['attributes']['fontname']
- """
- # RMF: subgraph should have all the
- # attributes of graph so it can be passed
- # as a graph to all methods
- #
- def __init__(self, graph_name='',
- obj_dict=None, suppress_disconnected=False,
- simplify=False, **attrs):
- Graph.__init__(
- self, graph_name=graph_name, obj_dict=obj_dict,
- suppress_disconnected=suppress_disconnected,
- simplify=simplify, **attrs)
- if obj_dict is None:
- self.obj_dict['type'] = 'subgraph'
- class Cluster(Graph):
- """Class representing a cluster in Graphviz's dot language.
- This class implements the methods to work on a representation
- of a cluster in Graphviz's dot language.
- cluster(graph_name='subG',
- suppress_disconnected=False,
- attribute=value,
- ...)
- graph_name:
- the cluster's name
- (the string 'cluster' will be always prepended)
- suppress_disconnected:
- defaults to false, which will remove from the
- cluster any disconnected nodes.
- All the attributes defined in the Graphviz dot language should
- be supported.
- Attributes can be set through the dynamically generated methods:
- set_[attribute name], i.e. set_color, set_fontname
- or using the instance's attributes:
- Cluster.obj_dict['attributes'][attribute name], i.e.
- cluster_instance.obj_dict['attributes']['label']
- cluster_instance.obj_dict['attributes']['fontname']
- """
- def __init__(self, graph_name='subG',
- obj_dict=None, suppress_disconnected=False,
- simplify=False, **attrs):
- Graph.__init__(
- self, graph_name=graph_name, obj_dict=obj_dict,
- suppress_disconnected=suppress_disconnected,
- simplify=simplify, **attrs)
- if obj_dict is None:
- self.obj_dict['type'] = 'subgraph'
- self.obj_dict['name'] = quote_if_necessary('cluster_'+graph_name)
- self.create_attribute_methods(CLUSTER_ATTRIBUTES)
- class Dot(Graph):
- """A container for handling a dot language file.
- This class implements methods to write and process
- a dot language file. It is a derived class of
- the base class 'Graph'.
- """
- def __init__(self, *argsl, **argsd):
- Graph.__init__(self, *argsl, **argsd)
- self.shape_files = list()
- self.formats = [
- 'canon', 'cmap', 'cmapx',
- 'cmapx_np', 'dia', 'dot',
- 'fig', 'gd', 'gd2', 'gif',
- 'hpgl', 'imap', 'imap_np', 'ismap',
- 'jpe', 'jpeg', 'jpg', 'mif',
- 'mp', 'pcl', 'pdf', 'pic', 'plain',
- 'plain-ext', 'png', 'ps', 'ps2',
- 'svg', 'svgz', 'vml', 'vmlz',
- 'vrml', 'vtx', 'wbmp', 'xdot', 'xlib']
- self.prog = 'dot'
- # Automatically creates all
- # the methods enabling the creation
- # of output in any of the supported formats.
- for frmt in self.formats:
- def new_method(
- f=frmt, prog=self.prog,
- encoding=None):
- """Refer to docstring of method `create`."""
- return self.create(
- format=f, prog=prog, encoding=encoding)
- name = 'create_{fmt}'.format(fmt=frmt)
- self.__setattr__(name, new_method)
- for frmt in self.formats+['raw']:
- def new_method(
- path, f=frmt, prog=self.prog,
- encoding=None):
- """Refer to docstring of method `write.`"""
- self.write(
- path, format=f, prog=prog,
- encoding=encoding)
- name = 'write_{fmt}'.format(fmt=frmt)
- self.__setattr__(name, new_method)
- def __getstate__(self):
- dict = copy.copy(self.obj_dict)
- return dict
- def __setstate__(self, state):
- self.obj_dict = state
- def set_shape_files(self, file_paths):
- """Add the paths of the required image files.
- If the graph needs graphic objects to
- be used as shapes or otherwise
- those need to be in the same folder as
- the graph is going to be rendered
- from. Alternatively the absolute path to
- the files can be specified when
- including the graphics in the graph.
- The files in the location pointed to by
- the path(s) specified as arguments
- to this method will be copied to
- the same temporary location where the
- graph is going to be rendered.
- """
- if isinstance( file_paths, str_type):
- self.shape_files.append( file_paths )
- if isinstance( file_paths, (list, tuple) ):
- self.shape_files.extend( file_paths )
- def set_prog(self, prog):
- """Sets the default program.
- Sets the default program in charge of processing
- the dot file into a graph.
- """
- self.prog = prog
- def write(self, path, prog=None, format='raw', encoding=None):
- """Writes a graph to a file.
- Given a filename 'path' it will open/create and truncate
- such file and write on it a representation of the graph
- defined by the dot object in the format specified by
- 'format' and using the encoding specified by `encoding` for text.
- The format 'raw' is used to dump the string representation
- of the Dot object, without further processing.
- The output can be processed by any of graphviz tools, defined
- in 'prog', which defaults to 'dot'
- Returns True or False according to the success of the write
- operation.
- There's also the preferred possibility of using:
- write_'format'(path, prog='program')
- which are automatically defined for all the supported formats.
- [write_ps(), write_gif(), write_dia(), ...]
- The encoding is passed to `open` [1].
- [1] https://docs.python.org/3/library/functions.html#open
- """
- if prog is None:
- prog = self.prog
- if format == 'raw':
- s = self.to_string()
- if not PY3:
- s = unicode(s)
- with io.open(path, mode='wt', encoding=encoding) as f:
- f.write(s)
- else:
- s = self.create(prog, format, encoding=encoding)
- with io.open(path, mode='wb') as f:
- f.write(s)
- return True
- def create(self, prog=None, format='ps', encoding=None):
- """Creates and returns a binary image for the graph.
- create will write the graph to a temporary dot file in the
- encoding specified by `encoding` and process it with the
- program given by 'prog' (which defaults to 'twopi'), reading
- the binary image output and return it as:
- - `str` of bytes in Python 2
- - `bytes` in Python 3
- There's also the preferred possibility of using:
- create_'format'(prog='program')
- which are automatically defined for all the supported formats,
- for example:
- - `create_ps()`
- - `create_gif()`
- - `create_dia()`
- If 'prog' is a list, instead of a string,
- then the fist item is expected to be the program name,
- followed by any optional command-line arguments for it:
- [ 'twopi', '-Tdot', '-s10' ]
- @param prog: either:
- - name of GraphViz executable that
- can be found in the `$PATH`, or
- - absolute path to GraphViz executable.
- If you have added GraphViz to the `$PATH` and
- use its executables as installed
- (without renaming any of them)
- then their names are:
- - `'dot'`
- - `'twopi'`
- - `'neato'`
- - `'circo'`
- - `'fdp'`
- - `'sfdp'`
- On Windows, these have the notorious ".exe" extension that,
- only for the above strings, will be added automatically.
- The `$PATH` is inherited from `os.env['PATH']` and
- passed to `subprocess.Popen` using the `env` argument.
- If you haven't added GraphViz to your `$PATH` on Windows,
- then you may want to give the absolute path to the
- executable (for example, to `dot.exe`) in `prog`.
- """
- if prog is None:
- prog = self.prog
- assert prog is not None
- if isinstance(prog, (list, tuple)):
- prog, args = prog[0], prog[1:]
- else:
- args = []
- # temp file
- tmp_fd, tmp_name = tempfile.mkstemp()
- os.close(tmp_fd)
- self.write(tmp_name, encoding=encoding)
- tmp_dir = os.path.dirname(tmp_name)
- # For each of the image files...
- for img in self.shape_files:
- # Get its data
- f = open(img, 'rb')
- f_data = f.read()
- f.close()
- # And copy it under a file with the same name in
- # the temporary directory
- f = open(os.path.join(tmp_dir, os.path.basename(img)), 'wb')
- f.write(f_data)
- f.close()
- arguments = ['-T{}'.format(format), ] + args + [tmp_name]
- try:
- stdout_data, stderr_data, process = call_graphviz(
- program=prog,
- arguments=arguments,
- working_dir=tmp_dir,
- )
- except OSError as e:
- if e.errno == errno.ENOENT:
- args = list(e.args)
- args[1] = '"{prog}" not found in path.'.format(
- prog=prog)
- raise OSError(*args)
- else:
- raise
- # clean file litter
- for img in self.shape_files:
- os.unlink(os.path.join(tmp_dir, os.path.basename(img)))
- os.unlink(tmp_name)
- if process.returncode != 0:
- message = (
- '"{prog}" with args {arguments} returned code: {code}\n\n'
- 'stdout, stderr:\n {out}\n{err}\n'
- ).format(
- prog=prog,
- arguments=arguments,
- code=process.returncode,
- out=stdout_data,
- err=stderr_data,
- )
- print(message)
- assert process.returncode == 0, (
- '"{prog}" with args {arguments} returned code: {code}'.format(
- prog=prog,
- arguments=arguments,
- code=process.returncode,
- )
- )
- return stdout_data
|