pydot.py 54 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964
  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. try:
  14. import dot_parser
  15. except Exception as e:
  16. warnings.warn(
  17. "`pydot` could not import `dot_parser`, "
  18. "so `pydot` will be unable to parse DOT files. "
  19. "The error was: {e}".format(e=e))
  20. __author__ = 'Ero Carrera'
  21. __version__ = '1.4.2'
  22. __license__ = 'MIT'
  23. PY3 = sys.version_info >= (3, 0, 0)
  24. if PY3:
  25. str_type = str
  26. else:
  27. str_type = basestring
  28. GRAPH_ATTRIBUTES = { 'Damping', 'K', 'URL', 'aspect', 'bb', 'bgcolor',
  29. 'center', 'charset', 'clusterrank', 'colorscheme', 'comment', 'compound',
  30. 'concentrate', 'defaultdist', 'dim', 'dimen', 'diredgeconstraints',
  31. 'dpi', 'epsilon', 'esep', 'fontcolor', 'fontname', 'fontnames',
  32. 'fontpath', 'fontsize', 'id', 'label', 'labeljust', 'labelloc',
  33. 'landscape', 'layers', 'layersep', 'layout', 'levels', 'levelsgap',
  34. 'lheight', 'lp', 'lwidth', 'margin', 'maxiter', 'mclimit', 'mindist',
  35. 'mode', 'model', 'mosek', 'nodesep', 'nojustify', 'normalize', 'nslimit',
  36. 'nslimit1', 'ordering', 'orientation', 'outputorder', 'overlap',
  37. 'overlap_scaling', 'pack', 'packmode', 'pad', 'page', 'pagedir',
  38. 'quadtree', 'quantum', 'rankdir', 'ranksep', 'ratio', 'remincross',
  39. 'repulsiveforce', 'resolution', 'root', 'rotate', 'searchsize', 'sep',
  40. 'showboxes', 'size', 'smoothing', 'sortv', 'splines', 'start',
  41. 'stylesheet', 'target', 'truecolor', 'viewport', 'voro_margin',
  42. # for subgraphs
  43. 'rank' }
  44. EDGE_ATTRIBUTES = { 'URL', 'arrowhead', 'arrowsize', 'arrowtail',
  45. 'color', 'colorscheme', 'comment', 'constraint', 'decorate', 'dir',
  46. 'edgeURL', 'edgehref', 'edgetarget', 'edgetooltip', 'fontcolor',
  47. 'fontname', 'fontsize', 'headURL', 'headclip', 'headhref', 'headlabel',
  48. 'headport', 'headtarget', 'headtooltip', 'href', 'id', 'label',
  49. 'labelURL', 'labelangle', 'labeldistance', 'labelfloat', 'labelfontcolor',
  50. 'labelfontname', 'labelfontsize', 'labelhref', 'labeltarget',
  51. 'labeltooltip', 'layer', 'len', 'lhead', 'lp', 'ltail', 'minlen',
  52. 'nojustify', 'penwidth', 'pos', 'samehead', 'sametail', 'showboxes',
  53. 'style', 'tailURL', 'tailclip', 'tailhref', 'taillabel', 'tailport',
  54. 'tailtarget', 'tailtooltip', 'target', 'tooltip', 'weight',
  55. 'rank' }
  56. NODE_ATTRIBUTES = { 'URL', 'color', 'colorscheme', 'comment',
  57. 'distortion', 'fillcolor', 'fixedsize', 'fontcolor', 'fontname',
  58. 'fontsize', 'group', 'height', 'id', 'image', 'imagescale', 'label',
  59. 'labelloc', 'layer', 'margin', 'nojustify', 'orientation', 'penwidth',
  60. 'peripheries', 'pin', 'pos', 'rects', 'regular', 'root', 'samplepoints',
  61. 'shape', 'shapefile', 'showboxes', 'sides', 'skew', 'sortv', 'style',
  62. 'target', 'tooltip', 'vertices', 'width', 'z',
  63. # The following are attributes dot2tex
  64. 'texlbl', 'texmode' }
  65. CLUSTER_ATTRIBUTES = { 'K', 'URL', 'bgcolor', 'color', 'colorscheme',
  66. 'fillcolor', 'fontcolor', 'fontname', 'fontsize', 'label', 'labeljust',
  67. 'labelloc', 'lheight', 'lp', 'lwidth', 'nojustify', 'pencolor',
  68. 'penwidth', 'peripheries', 'sortv', 'style', 'target', 'tooltip' }
  69. DEFAULT_PROGRAMS = {
  70. 'dot',
  71. 'twopi',
  72. 'neato',
  73. 'circo',
  74. 'fdp',
  75. 'sfdp',
  76. }
  77. def is_windows():
  78. # type: () -> bool
  79. return os.name == 'nt'
  80. def is_anaconda():
  81. # type: () -> bool
  82. import glob
  83. return glob.glob(os.path.join(sys.prefix, 'conda-meta\\graphviz*.json')) != []
  84. def get_executable_extension():
  85. # type: () -> str
  86. if is_windows():
  87. return '.bat' if is_anaconda() else '.exe'
  88. else:
  89. return ''
  90. def call_graphviz(program, arguments, working_dir, **kwargs):
  91. # explicitly inherit `$PATH`, on Windows too,
  92. # with `shell=False`
  93. if program in DEFAULT_PROGRAMS:
  94. extension = get_executable_extension()
  95. program += extension
  96. if arguments is None:
  97. arguments = []
  98. env = {
  99. 'PATH': os.environ.get('PATH', ''),
  100. 'LD_LIBRARY_PATH': os.environ.get('LD_LIBRARY_PATH', ''),
  101. 'SYSTEMROOT': os.environ.get('SYSTEMROOT', ''),
  102. }
  103. program_with_args = [program, ] + arguments
  104. process = subprocess.Popen(
  105. program_with_args,
  106. env=env,
  107. cwd=working_dir,
  108. shell=False,
  109. stderr=subprocess.PIPE,
  110. stdout=subprocess.PIPE,
  111. **kwargs
  112. )
  113. stdout_data, stderr_data = process.communicate()
  114. return stdout_data, stderr_data, process
  115. #
  116. # Extended version of ASPN's Python Cookbook Recipe:
  117. # Frozen dictionaries.
  118. # https://code.activestate.com/recipes/414283/
  119. #
  120. # This version freezes dictionaries used as values within dictionaries.
  121. #
  122. class frozendict(dict):
  123. def _blocked_attribute(obj):
  124. raise AttributeError('A frozendict cannot be modified.')
  125. _blocked_attribute = property(_blocked_attribute)
  126. __delitem__ = __setitem__ = clear = _blocked_attribute
  127. pop = popitem = setdefault = update = _blocked_attribute
  128. def __new__(cls, *args, **kw):
  129. new = dict.__new__(cls)
  130. args_ = []
  131. for arg in args:
  132. if isinstance(arg, dict):
  133. arg = copy.copy(arg)
  134. for k in arg:
  135. v = arg[k]
  136. if isinstance(v, frozendict):
  137. arg[k] = v
  138. elif isinstance(v, dict):
  139. arg[k] = frozendict(v)
  140. elif isinstance(v, list):
  141. v_ = list()
  142. for elm in v:
  143. if isinstance(elm, dict):
  144. v_.append( frozendict(elm) )
  145. else:
  146. v_.append( elm )
  147. arg[k] = tuple(v_)
  148. args_.append( arg )
  149. else:
  150. args_.append( arg )
  151. dict.__init__(new, *args_, **kw)
  152. return new
  153. def __init__(self, *args, **kw):
  154. pass
  155. def __hash__(self):
  156. try:
  157. return self._cached_hash
  158. except AttributeError:
  159. h = self._cached_hash = hash(tuple(sorted(self.items())))
  160. return h
  161. def __repr__(self):
  162. return "frozendict(%s)" % dict.__repr__(self)
  163. dot_keywords = ['graph', 'subgraph', 'digraph', 'node', 'edge', 'strict']
  164. id_re_alpha_nums = re.compile('^[_a-zA-Z][a-zA-Z0-9_,]*$', re.UNICODE)
  165. id_re_alpha_nums_with_ports = re.compile(
  166. '^[_a-zA-Z][a-zA-Z0-9_,:\"]*[a-zA-Z0-9_,\"]+$', re.UNICODE)
  167. id_re_num = re.compile('^[0-9,]+$', re.UNICODE)
  168. id_re_with_port = re.compile('^([^:]*):([^:]*)$', re.UNICODE)
  169. id_re_dbl_quoted = re.compile('^\".*\"$', re.S|re.UNICODE)
  170. id_re_html = re.compile('^<.*>$', re.S|re.UNICODE)
  171. def needs_quotes( s ):
  172. """Checks whether a string is a dot language ID.
  173. It will check whether the string is solely composed
  174. by the characters allowed in an ID or not.
  175. If the string is one of the reserved keywords it will
  176. need quotes too but the user will need to add them
  177. manually.
  178. """
  179. # If the name is a reserved keyword it will need quotes but pydot
  180. # can't tell when it's being used as a keyword or when it's simply
  181. # a name. Hence the user needs to supply the quotes when an element
  182. # would use a reserved keyword as name. This function will return
  183. # false indicating that a keyword string, if provided as-is, won't
  184. # need quotes.
  185. if s in dot_keywords:
  186. return False
  187. chars = [ord(c) for c in s if ord(c)>0x7f or ord(c)==0]
  188. if chars and not id_re_dbl_quoted.match(s) and not id_re_html.match(s):
  189. return True
  190. for test_re in [id_re_alpha_nums, id_re_num,
  191. id_re_dbl_quoted, id_re_html,
  192. id_re_alpha_nums_with_ports]:
  193. if test_re.match(s):
  194. return False
  195. m = id_re_with_port.match(s)
  196. if m:
  197. return needs_quotes(m.group(1)) or needs_quotes(m.group(2))
  198. return True
  199. def quote_if_necessary(s):
  200. """Enclose attribute value in quotes, if needed."""
  201. if isinstance(s, bool):
  202. if s is True:
  203. return 'True'
  204. return 'False'
  205. if not isinstance( s, str_type):
  206. return s
  207. if not s:
  208. return s
  209. if needs_quotes(s):
  210. replace = {'"' : r'\"',
  211. "\n" : r'\n',
  212. "\r" : r'\r'}
  213. for (a,b) in replace.items():
  214. s = s.replace(a, b)
  215. return '"' + s + '"'
  216. return s
  217. def graph_from_dot_data(s):
  218. """Load graphs from DOT description in string `s`.
  219. @param s: string in [DOT language](
  220. https://en.wikipedia.org/wiki/DOT_(graph_description_language))
  221. @return: Graphs that result from parsing.
  222. @rtype: `list` of `pydot.Dot`
  223. """
  224. return dot_parser.parse_dot_data(s)
  225. def graph_from_dot_file(path, encoding=None):
  226. """Load graphs from DOT file at `path`.
  227. @param path: to DOT file
  228. @param encoding: as passed to `io.open`.
  229. For example, `'utf-8'`.
  230. @return: Graphs that result from parsing.
  231. @rtype: `list` of `pydot.Dot`
  232. """
  233. with io.open(path, 'rt', encoding=encoding) as f:
  234. s = f.read()
  235. if not PY3:
  236. s = unicode(s)
  237. graphs = graph_from_dot_data(s)
  238. return graphs
  239. def graph_from_edges(edge_list, node_prefix='', directed=False):
  240. """Creates a basic graph out of an edge list.
  241. The edge list has to be a list of tuples representing
  242. the nodes connected by the edge.
  243. The values can be anything: bool, int, float, str.
  244. If the graph is undirected by default, it is only
  245. calculated from one of the symmetric halves of the matrix.
  246. """
  247. if directed:
  248. graph = Dot(graph_type='digraph')
  249. else:
  250. graph = Dot(graph_type='graph')
  251. for edge in edge_list:
  252. if isinstance(edge[0], str):
  253. src = node_prefix + edge[0]
  254. else:
  255. src = node_prefix + str(edge[0])
  256. if isinstance(edge[1], str):
  257. dst = node_prefix + edge[1]
  258. else:
  259. dst = node_prefix + str(edge[1])
  260. e = Edge( src, dst )
  261. graph.add_edge(e)
  262. return graph
  263. def graph_from_adjacency_matrix(matrix, node_prefix= u'', directed=False):
  264. """Creates a basic graph out of an adjacency matrix.
  265. The matrix has to be a list of rows of values
  266. representing an adjacency matrix.
  267. The values can be anything: bool, int, float, as long
  268. as they can evaluate to True or False.
  269. """
  270. node_orig = 1
  271. if directed:
  272. graph = Dot(graph_type='digraph')
  273. else:
  274. graph = Dot(graph_type='graph')
  275. for row in matrix:
  276. if not directed:
  277. skip = matrix.index(row)
  278. r = row[skip:]
  279. else:
  280. skip = 0
  281. r = row
  282. node_dest = skip+1
  283. for e in r:
  284. if e:
  285. graph.add_edge(
  286. Edge('%s%s' % (node_prefix, node_orig),
  287. '%s%s' % (node_prefix, node_dest)))
  288. node_dest += 1
  289. node_orig += 1
  290. return graph
  291. def graph_from_incidence_matrix(matrix, node_prefix='', directed=False):
  292. """Creates a basic graph out of an incidence matrix.
  293. The matrix has to be a list of rows of values
  294. representing an incidence matrix.
  295. The values can be anything: bool, int, float, as long
  296. as they can evaluate to True or False.
  297. """
  298. node_orig = 1
  299. if directed:
  300. graph = Dot(graph_type='digraph')
  301. else:
  302. graph = Dot(graph_type='graph')
  303. for row in matrix:
  304. nodes = []
  305. c = 1
  306. for node in row:
  307. if node:
  308. nodes.append(c*node)
  309. c += 1
  310. nodes.sort()
  311. if len(nodes) == 2:
  312. graph.add_edge(
  313. Edge('%s%s' % (node_prefix, abs(nodes[0])),
  314. '%s%s' % (node_prefix, nodes[1])))
  315. if not directed:
  316. graph.set_simplify(True)
  317. return graph
  318. class Common(object):
  319. """Common information to several classes.
  320. Should not be directly used, several classes are derived from
  321. this one.
  322. """
  323. def __getstate__(self):
  324. dict = copy.copy(self.obj_dict)
  325. return dict
  326. def __setstate__(self, state):
  327. self.obj_dict = state
  328. def __get_attribute__(self, attr):
  329. """Look for default attributes for this node"""
  330. attr_val = self.obj_dict['attributes'].get(attr, None)
  331. if attr_val is None:
  332. # get the defaults for nodes/edges
  333. default_node_name = self.obj_dict['type']
  334. # The defaults for graphs are set on a node named 'graph'
  335. if default_node_name in ('subgraph', 'digraph', 'cluster'):
  336. default_node_name = 'graph'
  337. g = self.get_parent_graph()
  338. if g is not None:
  339. defaults = g.get_node( default_node_name )
  340. else:
  341. return None
  342. # Multiple defaults could be set by having repeated 'graph [...]'
  343. # 'node [...]', 'edge [...]' statements. In such case, if the
  344. # same attribute is set in different statements, only the first
  345. # will be returned. In order to get all, one would call the
  346. # get_*_defaults() methods and handle those. Or go node by node
  347. # (of the ones specifying defaults) and modify the attributes
  348. # individually.
  349. #
  350. if not isinstance(defaults, (list, tuple)):
  351. defaults = [defaults]
  352. for default in defaults:
  353. attr_val = default.obj_dict['attributes'].get(attr, None)
  354. if attr_val:
  355. return attr_val
  356. else:
  357. return attr_val
  358. return None
  359. def set_parent_graph(self, parent_graph):
  360. self.obj_dict['parent_graph'] = parent_graph
  361. def get_parent_graph(self):
  362. return self.obj_dict.get('parent_graph', None)
  363. def set(self, name, value):
  364. """Set an attribute value by name.
  365. Given an attribute 'name' it will set its value to 'value'.
  366. There's always the possibility of using the methods:
  367. set_'name'(value)
  368. which are defined for all the existing attributes.
  369. """
  370. self.obj_dict['attributes'][name] = value
  371. def get(self, name):
  372. """Get an attribute value by name.
  373. Given an attribute 'name' it will get its value.
  374. There's always the possibility of using the methods:
  375. get_'name'()
  376. which are defined for all the existing attributes.
  377. """
  378. return self.obj_dict['attributes'].get(name, None)
  379. def get_attributes(self):
  380. """"""
  381. return self.obj_dict['attributes']
  382. def set_sequence(self, seq):
  383. self.obj_dict['sequence'] = seq
  384. def get_sequence(self):
  385. return self.obj_dict['sequence']
  386. def create_attribute_methods(self, obj_attributes):
  387. #for attr in self.obj_dict['attributes']:
  388. for attr in obj_attributes:
  389. # Generate all the Setter methods.
  390. #
  391. self.__setattr__(
  392. 'set_'+attr,
  393. lambda x, a=attr :
  394. self.obj_dict['attributes'].__setitem__(a, x) )
  395. # Generate all the Getter methods.
  396. #
  397. self.__setattr__(
  398. 'get_'+attr, lambda a=attr : self.__get_attribute__(a))
  399. class Error(Exception):
  400. """General error handling class.
  401. """
  402. def __init__(self, value):
  403. self.value = value
  404. def __str__(self):
  405. return self.value
  406. class InvocationException(Exception):
  407. """Indicate problem while running any GraphViz executable.
  408. """
  409. def __init__(self, value):
  410. self.value = value
  411. def __str__(self):
  412. return self.value
  413. class Node(Common):
  414. """A graph node.
  415. This class represents a graph's node with all its attributes.
  416. node(name, attribute=value, ...)
  417. name: node's name
  418. All the attributes defined in the Graphviz dot language should
  419. be supported.
  420. """
  421. def __init__(self, name = '', obj_dict = None, **attrs):
  422. #
  423. # Nodes will take attributes of
  424. # all other types because the defaults
  425. # for any GraphViz object are dealt with
  426. # as if they were Node definitions
  427. #
  428. if obj_dict is not None:
  429. self.obj_dict = obj_dict
  430. else:
  431. self.obj_dict = dict()
  432. # Copy the attributes
  433. #
  434. self.obj_dict[ 'attributes' ] = dict( attrs )
  435. self.obj_dict[ 'type' ] = 'node'
  436. self.obj_dict[ 'parent_graph' ] = None
  437. self.obj_dict[ 'parent_node_list' ] = None
  438. self.obj_dict[ 'sequence' ] = None
  439. # Remove the compass point
  440. #
  441. port = None
  442. if isinstance(name, str_type) and not name.startswith('"'):
  443. idx = name.find(':')
  444. if idx > 0 and idx+1 < len(name):
  445. name, port = name[:idx], name[idx:]
  446. if isinstance(name, int):
  447. name = str(name)
  448. self.obj_dict['name'] = quote_if_necessary(name)
  449. self.obj_dict['port'] = port
  450. self.create_attribute_methods(NODE_ATTRIBUTES)
  451. def __str__(self):
  452. return self.to_string()
  453. def set_name(self, node_name):
  454. """Set the node's name."""
  455. self.obj_dict['name'] = node_name
  456. def get_name(self):
  457. """Get the node's name."""
  458. return self.obj_dict['name']
  459. def get_port(self):
  460. """Get the node's port."""
  461. return self.obj_dict['port']
  462. def add_style(self, style):
  463. styles = self.obj_dict['attributes'].get('style', None)
  464. if not styles and style:
  465. styles = [ style ]
  466. else:
  467. styles = styles.split(',')
  468. styles.append( style )
  469. self.obj_dict['attributes']['style'] = ','.join( styles )
  470. def to_string(self):
  471. """Return string representation of node in DOT language."""
  472. # RMF: special case defaults for node, edge and graph properties.
  473. #
  474. node = quote_if_necessary(self.obj_dict['name'])
  475. node_attr = list()
  476. for attr in sorted(self.obj_dict['attributes']):
  477. value = self.obj_dict['attributes'][attr]
  478. if value == '':
  479. value = '""'
  480. if value is not None:
  481. node_attr.append(
  482. '%s=%s' % (attr, quote_if_necessary(value) ) )
  483. else:
  484. node_attr.append( attr )
  485. # No point in having nodes setting any defaults if the don't set
  486. # any attributes...
  487. #
  488. if node in ('graph', 'node', 'edge') and len(node_attr) == 0:
  489. return ''
  490. node_attr = ', '.join(node_attr)
  491. if node_attr:
  492. node += ' [' + node_attr + ']'
  493. return node + ';'
  494. class Edge(Common):
  495. """A graph edge.
  496. This class represents a graph's edge with all its attributes.
  497. edge(src, dst, attribute=value, ...)
  498. src: source node, subgraph or cluster
  499. dst: destination node, subgraph or cluster
  500. `src` and `dst` can be specified as a `Node`, `Subgraph` or
  501. `Cluster` object, or as the name string of such a component.
  502. All the attributes defined in the Graphviz dot language should
  503. be supported.
  504. Attributes can be set through the dynamically generated methods:
  505. set_[attribute name], i.e. set_label, set_fontname
  506. or directly by using the instance's special dictionary:
  507. Edge.obj_dict['attributes'][attribute name], i.e.
  508. edge_instance.obj_dict['attributes']['label']
  509. edge_instance.obj_dict['attributes']['fontname']
  510. """
  511. def __init__(self, src='', dst='', obj_dict=None, **attrs):
  512. self.obj_dict = dict()
  513. if isinstance(src, (Node, Subgraph, Cluster)):
  514. src = src.get_name()
  515. if isinstance(dst, (Node, Subgraph, Cluster)):
  516. dst = dst.get_name()
  517. points = (quote_if_necessary(src),
  518. quote_if_necessary(dst))
  519. self.obj_dict['points'] = points
  520. if obj_dict is None:
  521. # Copy the attributes
  522. self.obj_dict[ 'attributes' ] = dict( attrs )
  523. self.obj_dict[ 'type' ] = 'edge'
  524. self.obj_dict[ 'parent_graph' ] = None
  525. self.obj_dict[ 'parent_edge_list' ] = None
  526. self.obj_dict[ 'sequence' ] = None
  527. else:
  528. self.obj_dict = obj_dict
  529. self.create_attribute_methods(EDGE_ATTRIBUTES)
  530. def __str__(self):
  531. return self.to_string()
  532. def get_source(self):
  533. """Get the edges source node name."""
  534. return self.obj_dict['points'][0]
  535. def get_destination(self):
  536. """Get the edge's destination node name."""
  537. return self.obj_dict['points'][1]
  538. def __hash__(self):
  539. return hash( hash(self.get_source()) +
  540. hash(self.get_destination()) )
  541. def __eq__(self, edge):
  542. """Compare two edges.
  543. If the parent graph is directed, arcs linking
  544. node A to B are considered equal and A->B != B->A
  545. If the parent graph is undirected, any edge
  546. connecting two nodes is equal to any other
  547. edge connecting the same nodes, A->B == B->A
  548. """
  549. if not isinstance(edge, Edge):
  550. raise Error('Can not compare and '
  551. 'edge to a non-edge object.')
  552. if self.get_parent_graph().get_top_graph_type() == 'graph':
  553. # If the graph is undirected, the edge has neither
  554. # source nor destination.
  555. #
  556. if ( ( self.get_source() == edge.get_source() and
  557. self.get_destination() == edge.get_destination() ) or
  558. ( edge.get_source() == self.get_destination() and
  559. edge.get_destination() == self.get_source() ) ):
  560. return True
  561. else:
  562. if (self.get_source()==edge.get_source() and
  563. self.get_destination()==edge.get_destination()):
  564. return True
  565. return False
  566. if not PY3:
  567. def __ne__(self, other):
  568. result = self.__eq__(other)
  569. if result is NotImplemented:
  570. return NotImplemented
  571. return not result
  572. def parse_node_ref(self, node_str):
  573. if not isinstance(node_str, str):
  574. return node_str
  575. if node_str.startswith('"') and node_str.endswith('"'):
  576. return node_str
  577. node_port_idx = node_str.rfind(':')
  578. if (node_port_idx>0 and node_str[0]=='"' and
  579. node_str[node_port_idx-1]=='"'):
  580. return node_str
  581. if node_port_idx>0:
  582. a = node_str[:node_port_idx]
  583. b = node_str[node_port_idx+1:]
  584. node = quote_if_necessary(a)
  585. node += ':'+quote_if_necessary(b)
  586. return node
  587. return node_str
  588. def to_string(self):
  589. """Return string representation of edge in DOT language."""
  590. src = self.parse_node_ref( self.get_source() )
  591. dst = self.parse_node_ref( self.get_destination() )
  592. if isinstance(src, frozendict):
  593. edge = [ Subgraph(obj_dict=src).to_string() ]
  594. elif isinstance(src, int):
  595. edge = [ str(src) ]
  596. else:
  597. edge = [ src ]
  598. if (self.get_parent_graph() and
  599. self.get_parent_graph().get_top_graph_type() and
  600. self.get_parent_graph().get_top_graph_type() == 'digraph' ):
  601. edge.append( '->' )
  602. else:
  603. edge.append( '--' )
  604. if isinstance(dst, frozendict):
  605. edge.append( Subgraph(obj_dict=dst).to_string() )
  606. elif isinstance(dst, int):
  607. edge.append( str(dst) )
  608. else:
  609. edge.append( dst )
  610. edge_attr = list()
  611. for attr in sorted(self.obj_dict['attributes']):
  612. value = self.obj_dict['attributes'][attr]
  613. if value == '':
  614. value = '""'
  615. if value is not None:
  616. edge_attr.append(
  617. '%s=%s' % (attr, quote_if_necessary(value) ) )
  618. else:
  619. edge_attr.append( attr )
  620. edge_attr = ', '.join(edge_attr)
  621. if edge_attr:
  622. edge.append( ' [' + edge_attr + ']' )
  623. return ' '.join(edge) + ';'
  624. class Graph(Common):
  625. """Class representing a graph in Graphviz's dot language.
  626. This class implements the methods to work on a representation
  627. of a graph in Graphviz's dot language.
  628. graph( graph_name='G', graph_type='digraph',
  629. strict=False, suppress_disconnected=False, attribute=value, ...)
  630. graph_name:
  631. the graph's name
  632. graph_type:
  633. can be 'graph' or 'digraph'
  634. suppress_disconnected:
  635. defaults to False, which will remove from the
  636. graph any disconnected nodes.
  637. simplify:
  638. if True it will avoid displaying equal edges, i.e.
  639. only one edge between two nodes. removing the
  640. duplicated ones.
  641. All the attributes defined in the Graphviz dot language should
  642. be supported.
  643. Attributes can be set through the dynamically generated methods:
  644. set_[attribute name], i.e. set_size, set_fontname
  645. or using the instance's attributes:
  646. Graph.obj_dict['attributes'][attribute name], i.e.
  647. graph_instance.obj_dict['attributes']['label']
  648. graph_instance.obj_dict['attributes']['fontname']
  649. """
  650. def __init__(self, graph_name='G', obj_dict=None,
  651. graph_type='digraph', strict=False,
  652. suppress_disconnected=False, simplify=False, **attrs):
  653. if obj_dict is not None:
  654. self.obj_dict = obj_dict
  655. else:
  656. self.obj_dict = dict()
  657. self.obj_dict['attributes'] = dict(attrs)
  658. if graph_type not in ['graph', 'digraph']:
  659. raise Error((
  660. 'Invalid type "{t}". '
  661. 'Accepted graph types are: '
  662. 'graph, digraph').format(t=graph_type))
  663. self.obj_dict['name'] = quote_if_necessary(graph_name)
  664. self.obj_dict['type'] = graph_type
  665. self.obj_dict['strict'] = strict
  666. self.obj_dict['suppress_disconnected'] = suppress_disconnected
  667. self.obj_dict['simplify'] = simplify
  668. self.obj_dict['current_child_sequence'] = 1
  669. self.obj_dict['nodes'] = dict()
  670. self.obj_dict['edges'] = dict()
  671. self.obj_dict['subgraphs'] = dict()
  672. self.set_parent_graph(self)
  673. self.create_attribute_methods(GRAPH_ATTRIBUTES)
  674. def __str__(self):
  675. return self.to_string()
  676. def get_graph_type(self):
  677. return self.obj_dict['type']
  678. def get_top_graph_type(self):
  679. parent = self
  680. while True:
  681. parent_ = parent.get_parent_graph()
  682. if parent_ == parent:
  683. break
  684. parent = parent_
  685. return parent.obj_dict['type']
  686. def set_graph_defaults(self, **attrs):
  687. self.add_node( Node('graph', **attrs) )
  688. def get_graph_defaults(self, **attrs):
  689. graph_nodes = self.get_node('graph')
  690. if isinstance( graph_nodes, (list, tuple)):
  691. return [ node.get_attributes() for node in graph_nodes ]
  692. return graph_nodes.get_attributes()
  693. def set_node_defaults(self, **attrs):
  694. """Define default node attributes.
  695. These attributes only apply to nodes added to the graph after
  696. calling this method.
  697. """
  698. self.add_node( Node('node', **attrs) )
  699. def get_node_defaults(self, **attrs):
  700. graph_nodes = self.get_node('node')
  701. if isinstance( graph_nodes, (list, tuple)):
  702. return [ node.get_attributes() for node in graph_nodes ]
  703. return graph_nodes.get_attributes()
  704. def set_edge_defaults(self, **attrs):
  705. self.add_node( Node('edge', **attrs) )
  706. def get_edge_defaults(self, **attrs):
  707. graph_nodes = self.get_node('edge')
  708. if isinstance( graph_nodes, (list, tuple)):
  709. return [ node.get_attributes() for node in graph_nodes ]
  710. return graph_nodes.get_attributes()
  711. def set_simplify(self, simplify):
  712. """Set whether to simplify or not.
  713. If True it will avoid displaying equal edges, i.e.
  714. only one edge between two nodes. removing the
  715. duplicated ones.
  716. """
  717. self.obj_dict['simplify'] = simplify
  718. def get_simplify(self):
  719. """Get whether to simplify or not.
  720. Refer to set_simplify for more information.
  721. """
  722. return self.obj_dict['simplify']
  723. def set_type(self, graph_type):
  724. """Set the graph's type, 'graph' or 'digraph'."""
  725. self.obj_dict['type'] = graph_type
  726. def get_type(self):
  727. """Get the graph's type, 'graph' or 'digraph'."""
  728. return self.obj_dict['type']
  729. def set_name(self, graph_name):
  730. """Set the graph's name."""
  731. self.obj_dict['name'] = graph_name
  732. def get_name(self):
  733. """Get the graph's name."""
  734. return self.obj_dict['name']
  735. def set_strict(self, val):
  736. """Set graph to 'strict' mode.
  737. This option is only valid for top level graphs.
  738. """
  739. self.obj_dict['strict'] = val
  740. def get_strict(self, val):
  741. """Get graph's 'strict' mode (True, False).
  742. This option is only valid for top level graphs.
  743. """
  744. return self.obj_dict['strict']
  745. def set_suppress_disconnected(self, val):
  746. """Suppress disconnected nodes in the output graph.
  747. This option will skip nodes in
  748. the graph with no incoming or outgoing
  749. edges. This option works also
  750. for subgraphs and has effect only in the
  751. current graph/subgraph.
  752. """
  753. self.obj_dict['suppress_disconnected'] = val
  754. def get_suppress_disconnected(self, val):
  755. """Get if suppress disconnected is set.
  756. Refer to set_suppress_disconnected for more information.
  757. """
  758. return self.obj_dict['suppress_disconnected']
  759. def get_next_sequence_number(self):
  760. seq = self.obj_dict['current_child_sequence']
  761. self.obj_dict['current_child_sequence'] += 1
  762. return seq
  763. def add_node(self, graph_node):
  764. """Adds a node object to the graph.
  765. It takes a node object as its only argument and returns
  766. None.
  767. """
  768. if not isinstance(graph_node, Node):
  769. raise TypeError(
  770. 'add_node() received ' +
  771. 'a non node class object: ' + str(graph_node))
  772. node = self.get_node(graph_node.get_name())
  773. if not node:
  774. self.obj_dict['nodes'][graph_node.get_name()] = [
  775. graph_node.obj_dict ]
  776. #self.node_dict[graph_node.get_name()] = graph_node.attributes
  777. graph_node.set_parent_graph(self.get_parent_graph())
  778. else:
  779. self.obj_dict['nodes'][graph_node.get_name()].append(
  780. graph_node.obj_dict )
  781. graph_node.set_sequence(self.get_next_sequence_number())
  782. def del_node(self, name, index=None):
  783. """Delete a node from the graph.
  784. Given a node's name all node(s) with that same name
  785. will be deleted if 'index' is not specified or set
  786. to None.
  787. If there are several nodes with that same name and
  788. 'index' is given, only the node in that position
  789. will be deleted.
  790. 'index' should be an integer specifying the position
  791. of the node to delete. If index is larger than the
  792. number of nodes with that name, no action is taken.
  793. If nodes are deleted it returns True. If no action
  794. is taken it returns False.
  795. """
  796. if isinstance(name, Node):
  797. name = name.get_name()
  798. if name in self.obj_dict['nodes']:
  799. if (index is not None and
  800. index < len(self.obj_dict['nodes'][name])):
  801. del self.obj_dict['nodes'][name][index]
  802. return True
  803. else:
  804. del self.obj_dict['nodes'][name]
  805. return True
  806. return False
  807. def get_node(self, name):
  808. """Retrieve a node from the graph.
  809. Given a node's name the corresponding Node
  810. instance will be returned.
  811. If one or more nodes exist with that name a list of
  812. Node instances is returned.
  813. An empty list is returned otherwise.
  814. """
  815. match = list()
  816. if name in self.obj_dict['nodes']:
  817. match.extend(
  818. [Node(obj_dict=obj_dict)
  819. for obj_dict in self.obj_dict['nodes'][name]])
  820. return match
  821. def get_nodes(self):
  822. """Get the list of Node instances."""
  823. return self.get_node_list()
  824. def get_node_list(self):
  825. """Get the list of Node instances.
  826. This method returns the list of Node instances
  827. composing the graph.
  828. """
  829. node_objs = list()
  830. for node in self.obj_dict['nodes']:
  831. obj_dict_list = self.obj_dict['nodes'][node]
  832. node_objs.extend( [ Node( obj_dict = obj_d )
  833. for obj_d in obj_dict_list ] )
  834. return node_objs
  835. def add_edge(self, graph_edge):
  836. """Adds an edge object to the graph.
  837. It takes a edge object as its only argument and returns
  838. None.
  839. """
  840. if not isinstance(graph_edge, Edge):
  841. raise TypeError(
  842. 'add_edge() received a non edge class object: ' +
  843. str(graph_edge))
  844. edge_points = ( graph_edge.get_source(),
  845. graph_edge.get_destination() )
  846. if edge_points in self.obj_dict['edges']:
  847. edge_list = self.obj_dict['edges'][edge_points]
  848. edge_list.append(graph_edge.obj_dict)
  849. else:
  850. self.obj_dict['edges'][edge_points] = [ graph_edge.obj_dict ]
  851. graph_edge.set_sequence( self.get_next_sequence_number() )
  852. graph_edge.set_parent_graph( self.get_parent_graph() )
  853. def del_edge(self, src_or_list, dst=None, index=None):
  854. """Delete an edge from the graph.
  855. Given an edge's (source, destination) node names all
  856. matching edges(s) will be deleted if 'index' is not
  857. specified or set to None.
  858. If there are several matching edges and 'index' is
  859. given, only the edge in that position will be deleted.
  860. 'index' should be an integer specifying the position
  861. of the edge to delete. If index is larger than the
  862. number of matching edges, no action is taken.
  863. If edges are deleted it returns True. If no action
  864. is taken it returns False.
  865. """
  866. if isinstance( src_or_list, (list, tuple)):
  867. if dst is not None and isinstance(dst, int):
  868. index = dst
  869. src, dst = src_or_list
  870. else:
  871. src, dst = src_or_list, dst
  872. if isinstance(src, Node):
  873. src = src.get_name()
  874. if isinstance(dst, Node):
  875. dst = dst.get_name()
  876. if (src, dst) in self.obj_dict['edges']:
  877. if (index is not None and
  878. index < len(self.obj_dict['edges'][(src, dst)])):
  879. del self.obj_dict['edges'][(src, dst)][index]
  880. return True
  881. else:
  882. del self.obj_dict['edges'][(src, dst)]
  883. return True
  884. return False
  885. def get_edge(self, src_or_list, dst=None):
  886. """Retrieved an edge from the graph.
  887. Given an edge's source and destination the corresponding
  888. Edge instance(s) will be returned.
  889. If one or more edges exist with that source and destination
  890. a list of Edge instances is returned.
  891. An empty list is returned otherwise.
  892. """
  893. if isinstance( src_or_list, (list, tuple)) and dst is None:
  894. edge_points = tuple(src_or_list)
  895. edge_points_reverse = (edge_points[1], edge_points[0])
  896. else:
  897. edge_points = (src_or_list, dst)
  898. edge_points_reverse = (dst, src_or_list)
  899. match = list()
  900. if edge_points in self.obj_dict['edges'] or (
  901. self.get_top_graph_type() == 'graph' and
  902. edge_points_reverse in self.obj_dict['edges']):
  903. edges_obj_dict = self.obj_dict['edges'].get(
  904. edge_points,
  905. self.obj_dict['edges'].get( edge_points_reverse, None ))
  906. for edge_obj_dict in edges_obj_dict:
  907. match.append(
  908. Edge(edge_points[0],
  909. edge_points[1],
  910. obj_dict=edge_obj_dict))
  911. return match
  912. def get_edges(self):
  913. return self.get_edge_list()
  914. def get_edge_list(self):
  915. """Get the list of Edge instances.
  916. This method returns the list of Edge instances
  917. composing the graph.
  918. """
  919. edge_objs = list()
  920. for edge in self.obj_dict['edges']:
  921. obj_dict_list = self.obj_dict['edges'][edge]
  922. edge_objs.extend(
  923. [Edge(obj_dict=obj_d)
  924. for obj_d in obj_dict_list])
  925. return edge_objs
  926. def add_subgraph(self, sgraph):
  927. """Adds an subgraph object to the graph.
  928. It takes a subgraph object as its only argument and returns
  929. None.
  930. """
  931. if (not isinstance(sgraph, Subgraph) and
  932. not isinstance(sgraph, Cluster)):
  933. raise TypeError(
  934. 'add_subgraph() received a non subgraph class object:' +
  935. str(sgraph))
  936. if sgraph.get_name() in self.obj_dict['subgraphs']:
  937. sgraph_list = self.obj_dict['subgraphs'][ sgraph.get_name() ]
  938. sgraph_list.append( sgraph.obj_dict )
  939. else:
  940. self.obj_dict['subgraphs'][sgraph.get_name()] = [
  941. sgraph.obj_dict]
  942. sgraph.set_sequence( self.get_next_sequence_number() )
  943. sgraph.set_parent_graph( self.get_parent_graph() )
  944. def get_subgraph(self, name):
  945. """Retrieved a subgraph from the graph.
  946. Given a subgraph's name the corresponding
  947. Subgraph instance will be returned.
  948. If one or more subgraphs exist with the same name, a list of
  949. Subgraph instances is returned.
  950. An empty list is returned otherwise.
  951. """
  952. match = list()
  953. if name in self.obj_dict['subgraphs']:
  954. sgraphs_obj_dict = self.obj_dict['subgraphs'].get( name )
  955. for obj_dict_list in sgraphs_obj_dict:
  956. #match.extend( Subgraph( obj_dict = obj_d )
  957. # for obj_d in obj_dict_list )
  958. match.append( Subgraph( obj_dict = obj_dict_list ) )
  959. return match
  960. def get_subgraphs(self):
  961. return self.get_subgraph_list()
  962. def get_subgraph_list(self):
  963. """Get the list of Subgraph instances.
  964. This method returns the list of Subgraph instances
  965. in the graph.
  966. """
  967. sgraph_objs = list()
  968. for sgraph in self.obj_dict['subgraphs']:
  969. obj_dict_list = self.obj_dict['subgraphs'][sgraph]
  970. sgraph_objs.extend(
  971. [Subgraph(obj_dict=obj_d)
  972. for obj_d in obj_dict_list])
  973. return sgraph_objs
  974. def set_parent_graph(self, parent_graph):
  975. self.obj_dict['parent_graph'] = parent_graph
  976. for k in self.obj_dict['nodes']:
  977. obj_list = self.obj_dict['nodes'][k]
  978. for obj in obj_list:
  979. obj['parent_graph'] = parent_graph
  980. for k in self.obj_dict['edges']:
  981. obj_list = self.obj_dict['edges'][k]
  982. for obj in obj_list:
  983. obj['parent_graph'] = parent_graph
  984. for k in self.obj_dict['subgraphs']:
  985. obj_list = self.obj_dict['subgraphs'][k]
  986. for obj in obj_list:
  987. Graph(obj_dict=obj).set_parent_graph(parent_graph)
  988. def to_string(self):
  989. """Return string representation of graph in DOT language.
  990. @return: graph and subelements
  991. @rtype: `str`
  992. """
  993. graph = list()
  994. if self.obj_dict.get('strict', None) is not None:
  995. if (self == self.get_parent_graph() and
  996. self.obj_dict['strict']):
  997. graph.append('strict ')
  998. graph_type = self.obj_dict['type']
  999. if (graph_type == 'subgraph' and
  1000. not self.obj_dict.get('show_keyword', True)):
  1001. graph_type = ''
  1002. s = '{type} {name} {{\n'.format(
  1003. type=graph_type,
  1004. name=self.obj_dict['name'])
  1005. graph.append(s)
  1006. for attr in sorted(self.obj_dict['attributes']):
  1007. if self.obj_dict['attributes'].get(attr, None) is not None:
  1008. val = self.obj_dict['attributes'].get(attr)
  1009. if val == '':
  1010. val = '""'
  1011. if val is not None:
  1012. graph.append('%s=%s' %
  1013. (attr, quote_if_necessary(val)))
  1014. else:
  1015. graph.append( attr )
  1016. graph.append( ';\n' )
  1017. edges_done = set()
  1018. edge_obj_dicts = list()
  1019. for k in self.obj_dict['edges']:
  1020. edge_obj_dicts.extend(self.obj_dict['edges'][k])
  1021. if edge_obj_dicts:
  1022. edge_src_set, edge_dst_set = list(zip(
  1023. *[obj['points'] for obj in edge_obj_dicts]))
  1024. edge_src_set, edge_dst_set = set(edge_src_set), set(edge_dst_set)
  1025. else:
  1026. edge_src_set, edge_dst_set = set(), set()
  1027. node_obj_dicts = list()
  1028. for k in self.obj_dict['nodes']:
  1029. node_obj_dicts.extend(self.obj_dict['nodes'][k])
  1030. sgraph_obj_dicts = list()
  1031. for k in self.obj_dict['subgraphs']:
  1032. sgraph_obj_dicts.extend(self.obj_dict['subgraphs'][k])
  1033. obj_list = [(obj['sequence'], obj)
  1034. for obj in (edge_obj_dicts +
  1035. node_obj_dicts + sgraph_obj_dicts) ]
  1036. obj_list.sort(key=lambda x: x[0])
  1037. for idx, obj in obj_list:
  1038. if obj['type'] == 'node':
  1039. node = Node(obj_dict=obj)
  1040. if self.obj_dict.get('suppress_disconnected', False):
  1041. if (node.get_name() not in edge_src_set and
  1042. node.get_name() not in edge_dst_set):
  1043. continue
  1044. graph.append( node.to_string()+'\n' )
  1045. elif obj['type'] == 'edge':
  1046. edge = Edge(obj_dict=obj)
  1047. if (self.obj_dict.get('simplify', False) and
  1048. edge in edges_done):
  1049. continue
  1050. graph.append( edge.to_string() + '\n' )
  1051. edges_done.add(edge)
  1052. else:
  1053. sgraph = Subgraph(obj_dict=obj)
  1054. graph.append( sgraph.to_string()+'\n' )
  1055. graph.append( '}\n' )
  1056. return ''.join(graph)
  1057. class Subgraph(Graph):
  1058. """Class representing a subgraph in Graphviz's dot language.
  1059. This class implements the methods to work on a representation
  1060. of a subgraph in Graphviz's dot language.
  1061. subgraph(graph_name='subG',
  1062. suppress_disconnected=False,
  1063. attribute=value,
  1064. ...)
  1065. graph_name:
  1066. the subgraph's name
  1067. suppress_disconnected:
  1068. defaults to false, which will remove from the
  1069. subgraph any disconnected nodes.
  1070. All the attributes defined in the Graphviz dot language should
  1071. be supported.
  1072. Attributes can be set through the dynamically generated methods:
  1073. set_[attribute name], i.e. set_size, set_fontname
  1074. or using the instance's attributes:
  1075. Subgraph.obj_dict['attributes'][attribute name], i.e.
  1076. subgraph_instance.obj_dict['attributes']['label']
  1077. subgraph_instance.obj_dict['attributes']['fontname']
  1078. """
  1079. # RMF: subgraph should have all the
  1080. # attributes of graph so it can be passed
  1081. # as a graph to all methods
  1082. #
  1083. def __init__(self, graph_name='',
  1084. obj_dict=None, suppress_disconnected=False,
  1085. simplify=False, **attrs):
  1086. Graph.__init__(
  1087. self, graph_name=graph_name, obj_dict=obj_dict,
  1088. suppress_disconnected=suppress_disconnected,
  1089. simplify=simplify, **attrs)
  1090. if obj_dict is None:
  1091. self.obj_dict['type'] = 'subgraph'
  1092. class Cluster(Graph):
  1093. """Class representing a cluster in Graphviz's dot language.
  1094. This class implements the methods to work on a representation
  1095. of a cluster in Graphviz's dot language.
  1096. cluster(graph_name='subG',
  1097. suppress_disconnected=False,
  1098. attribute=value,
  1099. ...)
  1100. graph_name:
  1101. the cluster's name
  1102. (the string 'cluster' will be always prepended)
  1103. suppress_disconnected:
  1104. defaults to false, which will remove from the
  1105. cluster any disconnected nodes.
  1106. All the attributes defined in the Graphviz dot language should
  1107. be supported.
  1108. Attributes can be set through the dynamically generated methods:
  1109. set_[attribute name], i.e. set_color, set_fontname
  1110. or using the instance's attributes:
  1111. Cluster.obj_dict['attributes'][attribute name], i.e.
  1112. cluster_instance.obj_dict['attributes']['label']
  1113. cluster_instance.obj_dict['attributes']['fontname']
  1114. """
  1115. def __init__(self, graph_name='subG',
  1116. obj_dict=None, suppress_disconnected=False,
  1117. simplify=False, **attrs):
  1118. Graph.__init__(
  1119. self, graph_name=graph_name, obj_dict=obj_dict,
  1120. suppress_disconnected=suppress_disconnected,
  1121. simplify=simplify, **attrs)
  1122. if obj_dict is None:
  1123. self.obj_dict['type'] = 'subgraph'
  1124. self.obj_dict['name'] = quote_if_necessary('cluster_'+graph_name)
  1125. self.create_attribute_methods(CLUSTER_ATTRIBUTES)
  1126. class Dot(Graph):
  1127. """A container for handling a dot language file.
  1128. This class implements methods to write and process
  1129. a dot language file. It is a derived class of
  1130. the base class 'Graph'.
  1131. """
  1132. def __init__(self, *argsl, **argsd):
  1133. Graph.__init__(self, *argsl, **argsd)
  1134. self.shape_files = list()
  1135. self.formats = [
  1136. 'canon', 'cmap', 'cmapx',
  1137. 'cmapx_np', 'dia', 'dot',
  1138. 'fig', 'gd', 'gd2', 'gif',
  1139. 'hpgl', 'imap', 'imap_np', 'ismap',
  1140. 'jpe', 'jpeg', 'jpg', 'mif',
  1141. 'mp', 'pcl', 'pdf', 'pic', 'plain',
  1142. 'plain-ext', 'png', 'ps', 'ps2',
  1143. 'svg', 'svgz', 'vml', 'vmlz',
  1144. 'vrml', 'vtx', 'wbmp', 'xdot', 'xlib']
  1145. self.prog = 'dot'
  1146. # Automatically creates all
  1147. # the methods enabling the creation
  1148. # of output in any of the supported formats.
  1149. for frmt in self.formats:
  1150. def new_method(
  1151. f=frmt, prog=self.prog,
  1152. encoding=None):
  1153. """Refer to docstring of method `create`."""
  1154. return self.create(
  1155. format=f, prog=prog, encoding=encoding)
  1156. name = 'create_{fmt}'.format(fmt=frmt)
  1157. self.__setattr__(name, new_method)
  1158. for frmt in self.formats+['raw']:
  1159. def new_method(
  1160. path, f=frmt, prog=self.prog,
  1161. encoding=None):
  1162. """Refer to docstring of method `write.`"""
  1163. self.write(
  1164. path, format=f, prog=prog,
  1165. encoding=encoding)
  1166. name = 'write_{fmt}'.format(fmt=frmt)
  1167. self.__setattr__(name, new_method)
  1168. def __getstate__(self):
  1169. dict = copy.copy(self.obj_dict)
  1170. return dict
  1171. def __setstate__(self, state):
  1172. self.obj_dict = state
  1173. def set_shape_files(self, file_paths):
  1174. """Add the paths of the required image files.
  1175. If the graph needs graphic objects to
  1176. be used as shapes or otherwise
  1177. those need to be in the same folder as
  1178. the graph is going to be rendered
  1179. from. Alternatively the absolute path to
  1180. the files can be specified when
  1181. including the graphics in the graph.
  1182. The files in the location pointed to by
  1183. the path(s) specified as arguments
  1184. to this method will be copied to
  1185. the same temporary location where the
  1186. graph is going to be rendered.
  1187. """
  1188. if isinstance( file_paths, str_type):
  1189. self.shape_files.append( file_paths )
  1190. if isinstance( file_paths, (list, tuple) ):
  1191. self.shape_files.extend( file_paths )
  1192. def set_prog(self, prog):
  1193. """Sets the default program.
  1194. Sets the default program in charge of processing
  1195. the dot file into a graph.
  1196. """
  1197. self.prog = prog
  1198. def write(self, path, prog=None, format='raw', encoding=None):
  1199. """Writes a graph to a file.
  1200. Given a filename 'path' it will open/create and truncate
  1201. such file and write on it a representation of the graph
  1202. defined by the dot object in the format specified by
  1203. 'format' and using the encoding specified by `encoding` for text.
  1204. The format 'raw' is used to dump the string representation
  1205. of the Dot object, without further processing.
  1206. The output can be processed by any of graphviz tools, defined
  1207. in 'prog', which defaults to 'dot'
  1208. Returns True or False according to the success of the write
  1209. operation.
  1210. There's also the preferred possibility of using:
  1211. write_'format'(path, prog='program')
  1212. which are automatically defined for all the supported formats.
  1213. [write_ps(), write_gif(), write_dia(), ...]
  1214. The encoding is passed to `open` [1].
  1215. [1] https://docs.python.org/3/library/functions.html#open
  1216. """
  1217. if prog is None:
  1218. prog = self.prog
  1219. if format == 'raw':
  1220. s = self.to_string()
  1221. if not PY3:
  1222. s = unicode(s)
  1223. with io.open(path, mode='wt', encoding=encoding) as f:
  1224. f.write(s)
  1225. else:
  1226. s = self.create(prog, format, encoding=encoding)
  1227. with io.open(path, mode='wb') as f:
  1228. f.write(s)
  1229. return True
  1230. def create(self, prog=None, format='ps', encoding=None):
  1231. """Creates and returns a binary image for the graph.
  1232. create will write the graph to a temporary dot file in the
  1233. encoding specified by `encoding` and process it with the
  1234. program given by 'prog' (which defaults to 'twopi'), reading
  1235. the binary image output and return it as:
  1236. - `str` of bytes in Python 2
  1237. - `bytes` in Python 3
  1238. There's also the preferred possibility of using:
  1239. create_'format'(prog='program')
  1240. which are automatically defined for all the supported formats,
  1241. for example:
  1242. - `create_ps()`
  1243. - `create_gif()`
  1244. - `create_dia()`
  1245. If 'prog' is a list, instead of a string,
  1246. then the fist item is expected to be the program name,
  1247. followed by any optional command-line arguments for it:
  1248. [ 'twopi', '-Tdot', '-s10' ]
  1249. @param prog: either:
  1250. - name of GraphViz executable that
  1251. can be found in the `$PATH`, or
  1252. - absolute path to GraphViz executable.
  1253. If you have added GraphViz to the `$PATH` and
  1254. use its executables as installed
  1255. (without renaming any of them)
  1256. then their names are:
  1257. - `'dot'`
  1258. - `'twopi'`
  1259. - `'neato'`
  1260. - `'circo'`
  1261. - `'fdp'`
  1262. - `'sfdp'`
  1263. On Windows, these have the notorious ".exe" extension that,
  1264. only for the above strings, will be added automatically.
  1265. The `$PATH` is inherited from `os.env['PATH']` and
  1266. passed to `subprocess.Popen` using the `env` argument.
  1267. If you haven't added GraphViz to your `$PATH` on Windows,
  1268. then you may want to give the absolute path to the
  1269. executable (for example, to `dot.exe`) in `prog`.
  1270. """
  1271. if prog is None:
  1272. prog = self.prog
  1273. assert prog is not None
  1274. if isinstance(prog, (list, tuple)):
  1275. prog, args = prog[0], prog[1:]
  1276. else:
  1277. args = []
  1278. # temp file
  1279. tmp_fd, tmp_name = tempfile.mkstemp()
  1280. os.close(tmp_fd)
  1281. self.write(tmp_name, encoding=encoding)
  1282. tmp_dir = os.path.dirname(tmp_name)
  1283. # For each of the image files...
  1284. for img in self.shape_files:
  1285. # Get its data
  1286. f = open(img, 'rb')
  1287. f_data = f.read()
  1288. f.close()
  1289. # And copy it under a file with the same name in
  1290. # the temporary directory
  1291. f = open(os.path.join(tmp_dir, os.path.basename(img)), 'wb')
  1292. f.write(f_data)
  1293. f.close()
  1294. arguments = ['-T{}'.format(format), ] + args + [tmp_name]
  1295. try:
  1296. stdout_data, stderr_data, process = call_graphviz(
  1297. program=prog,
  1298. arguments=arguments,
  1299. working_dir=tmp_dir,
  1300. )
  1301. except OSError as e:
  1302. if e.errno == errno.ENOENT:
  1303. args = list(e.args)
  1304. args[1] = '"{prog}" not found in path.'.format(
  1305. prog=prog)
  1306. raise OSError(*args)
  1307. else:
  1308. raise
  1309. # clean file litter
  1310. for img in self.shape_files:
  1311. os.unlink(os.path.join(tmp_dir, os.path.basename(img)))
  1312. os.unlink(tmp_name)
  1313. if process.returncode != 0:
  1314. message = (
  1315. '"{prog}" with args {arguments} returned code: {code}\n\n'
  1316. 'stdout, stderr:\n {out}\n{err}\n'
  1317. ).format(
  1318. prog=prog,
  1319. arguments=arguments,
  1320. code=process.returncode,
  1321. out=stdout_data,
  1322. err=stderr_data,
  1323. )
  1324. print(message)
  1325. assert process.returncode == 0, (
  1326. '"{prog}" with args {arguments} returned code: {code}'.format(
  1327. prog=prog,
  1328. arguments=arguments,
  1329. code=process.returncode,
  1330. )
  1331. )
  1332. return stdout_data