Merge lp:~veger/ubuntu/trusty/pydot/fix-for-987934 into lp:ubuntu/trusty/pydot

Proposed by Maarten Bezemer
Status: Needs review
Proposed branch: lp:~veger/ubuntu/trusty/pydot/fix-for-987934
Merge into: lp:ubuntu/trusty/pydot
Diff against target: 1828 lines (+632/-405)
21 files modified
ChangeLog (+0/-64)
PKG-INFO (+9/-8)
debian/changelog (+29/-0)
debian/control (+6/-7)
debian/patches/01-setup-py-nodata.patch (+6/-5)
debian/patches/02-use-setuptools.patch (+20/-0)
debian/patches/03-support-pyparsing-v2.patch (+18/-0)
debian/patches/series (+3/-0)
debian/pycompat (+0/-1)
debian/pyversions (+0/-1)
debian/rules (+0/-3)
debian/source/format (+1/-0)
dot_parser.py (+82/-33)
pydot.egg-info/PKG-INFO (+0/-35)
pydot.egg-info/SOURCES.txt (+0/-11)
pydot.egg-info/dependency_links.txt (+0/-1)
pydot.egg-info/requires.txt (+0/-2)
pydot.egg-info/top_level.txt (+0/-2)
pydot.py (+451/-224)
setup.cfg (+0/-5)
setup.py (+7/-3)
To merge this branch: bzr merge lp:~veger/ubuntu/trusty/pydot/fix-for-987934
Reviewer Review Type Date Requested Status
Daniel Holbach (community) Approve
Review via email: mp+206584@code.launchpad.net

Description of the change

I have updated the pydot package to version 1.0.28, as the current version is quite outdated (1.0.2).

The update fixes the linked bug reports and additionally it fixes #490015 bug 2. (I do not know what is causing bug 1, but this update still shows the same behavior as described by the reporter)

As the package is old, I had to update the debian/* files quite heavily as well.
I have followed the debian packaging and python library packaging guidelines, so everything should be fine. (But then again, I am not very experienced, so I might have missed something).

To post a comment you must log in.
Revision history for this message
Daniel Holbach (dholbach) wrote :

Thanks a lot for your work on this. Uploaded with two small modifications:
 - changed version number to 1.0.28-0ubuntu1 (1.0.28-1 would indicate that it was in Debian already)
 - ran the "update-maintainer" script to adhere to https://wiki.ubuntu.com/DebianMaintainerField

Can you please forward your changes to Debian as well?

review: Approve
Revision history for this message
Maarten Bezemer (veger) wrote :

I just have forwarded the (slightly modified) patch to Debian: https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=638869

Unmerged revisions

4. By Maarten Bezemer

* New upstream release. (LP: #574428, #987934)
* debian/control updated to newest standards version:
  - Removed DM-Upload-Allowed, obsolete
  - Updated Vsc-Browser field
  - Depend on python-setuptools
  - Depend on pyparsing >= 2.0.1 to prevent incompability problems with
    debian/patches/03-support-pyparsing-v2.patch
  - Add ${misc:Depends} to binary package Depends
  - Support dh-python
    o Bumped cdbs version to 0.4.90
    o Bumped python version to 2.6.6.3
    o Removed XB-Python-Version field
    o Added X-Python-Version field
    o Removed ${python:Depends} from Depends
* debian/rules:
  - do not include simple-patchsys.mk
  - Support dh-python: Remove DEB_PYTHON_SYSTEM
* debian/pyversions: not required for dh-python
* debian/pycompat: not required for dh-python
* debian/source/format: added and set to 3.0 (quilt)
* debian/patches/02-use-setuptools.patch: patched setup.py to use setuptools
  to prevent "Unknown distribution option: 'install_requires'" warning
* debian/patches/03-support-pyparsing-v2.patch: add support for pyparsing
  version 2.X, fixes "Couldn't import dot_parser" error

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== removed file '._dot_parser.py'
2Binary files ._dot_parser.py 2008-05-26 15:35:04 +0000 and ._dot_parser.py 1970-01-01 00:00:00 +0000 differ
3=== removed file '._pydot.py'
4Binary files ._pydot.py 2008-05-26 15:35:04 +0000 and ._pydot.py 1970-01-01 00:00:00 +0000 differ
5=== removed file '._setup.py'
6Binary files ._setup.py 2008-05-26 15:35:04 +0000 and ._setup.py 1970-01-01 00:00:00 +0000 differ
7=== removed file 'ChangeLog'
8--- ChangeLog 2008-05-26 15:35:04 +0000
9+++ ChangeLog 1970-01-01 00:00:00 +0000
10@@ -1,64 +0,0 @@
11-2004-04-28 21:50 carrer
12-
13- * pydot.py: Some of the changes already made should allow pydot to
14- run on OSX. Bumped version to 0.9.2
15-
16-2004-04-24 17:52 carrer
17-
18- * setup.py: Added more metainformation to the distribution.
19-
20-2004-04-24 17:51 carrer
21-
22- * pydot.py: Added support for circo and fdp. Fixed piping mechanism
23- to not to capture stderr.
24-
25-2004-04-24 13:26 carrer
26-
27- * ChangeLog, LICENSE, MANIFEST, README, setup.py: Adding
28- suplementary files to the distribution to the CVS.
29-
30-2004-04-24 12:57 carrer
31-
32- * pydot.py: Bumped version to 0.9.1
33-
34-2004-04-24 01:36 carrer
35-
36- * pydot.py: Implemented tweaks suggested by John B. Cole to handle
37- non-str nodes, converting them to strings.
38-
39-2004-04-24 01:10 carrer
40-
41- * pydot.py: Applied patch for Windows support by Kent Johnson.
42-
43-2004-04-24 01:05 carrer
44-
45- * pydot.py: Fixed to properly handle unicode strings in attributes.
46-
47-2004-04-20 00:06 carrer
48-
49- * pydot.py:
50- Fixed silly error in graph_from_edges. When pasting the function
51- into the code, the references to the pydot module were not
52- removed, which are no longer needed since we now are _in_ the
53- module.
54-
55-2004-04-19 23:33 carrer
56-
57- * pydot.py:
58- Added support to write files with the desired output format with
59- write_[format]
60-
61-2004-04-19 22:53 carrer
62-
63- * pydot.py:
64- Done some clean up, no major changes.
65-
66-2004-04-08 00:22 carrer
67-
68- * pydot.py:
69- Initial revision.
70-
71-2004-04-08 00:22 carrer
72-
73- * pydot.py: Initial revision
74-
75
76=== modified file 'PKG-INFO'
77--- PKG-INFO 2008-05-26 15:35:04 +0000
78+++ PKG-INFO 2014-02-15 16:49:34 +0000
79@@ -1,12 +1,12 @@
80 Metadata-Version: 1.0
81 Name: pydot
82-Version: 1.0.2
83+Version: 1.0.28
84 Summary: Python interface to Graphviz's Dot
85 Home-page: http://code.google.com/p/pydot/
86 Author: Ero Carrera
87 Author-email: ero@dkbza.org
88 License: MIT
89-Download-URL: http://pydot.googlecode.com/files/pydot-1.0.2.tar.gz
90+Download-URL: http://pydot.googlecode.com/files/pydot-1.0.28.tar.gz
91 Description: Graphviz's dot language Python interface.
92
93 This module provides with a full interface to create handle modify
94@@ -14,12 +14,13 @@
95
96 References:
97
98- pydot Homepage: http://www.dkbza.org/pydot.html
99- Graphviz: http://www.research.att.com/sw/tools/graphviz/
100- DOT Language: http://www.research.att.com/~erg/graphviz/info/lang.html
101-
102- Programmed and tested with Graphviz 1.16 and Python 2.3.4 on GNU/Linux
103- by Ero Carrera (c) 2004 [ero@dkbza.org]
104+ pydot Homepage: http://code.google.com/p/pydot/
105+ Graphviz: http://www.graphviz.org/
106+ DOT Language: http://www.graphviz.org/doc/info/lang.html
107+
108+ Programmed and tested with Graphviz 2.26.3 and Python 2.6 on OSX 10.6.4
109+
110+ Copyright (c) 2005-2011 Ero Carrera <ero.carrera@gmail.com>
111
112 Distributed under MIT license [http://opensource.org/licenses/mit-license.html].
113
114
115=== modified file 'debian/changelog'
116--- debian/changelog 2008-05-26 15:35:04 +0000
117+++ debian/changelog 2014-02-15 16:49:34 +0000
118@@ -1,3 +1,32 @@
119+pydot (1.0.28-1) trusty; urgency=low
120+
121+ * New upstream release. (LP: #574428, #987934)
122+ * debian/control updated to newest standards version:
123+ - Removed DM-Upload-Allowed, obsolete
124+ - Updated Vsc-Browser field
125+ - Depend on python-setuptools
126+ - Depend on pyparsing >= 2.0.1 to prevent incompability problems with
127+ debian/patches/03-support-pyparsing-v2.patch
128+ - Add ${misc:Depends} to binary package Depends
129+ - Support dh-python
130+ o Bumped cdbs version to 0.4.90
131+ o Bumped python version to 2.6.6.3
132+ o Removed XB-Python-Version field
133+ o Added X-Python-Version field
134+ o Removed ${python:Depends} from Depends
135+ * debian/rules:
136+ - do not include simple-patchsys.mk
137+ - Support dh-python: Remove DEB_PYTHON_SYSTEM
138+ * debian/pyversions: not required for dh-python
139+ * debian/pycompat: not required for dh-python
140+ * debian/source/format: added and set to 3.0 (quilt)
141+ * debian/patches/02-use-setuptools.patch: patched setup.py to use setuptools
142+ to prevent "Unknown distribution option: 'install_requires'" warning
143+ * debian/patches/03-support-pyparsing-v2.patch: add support for pyparsing
144+ version 2.X, fixes "Couldn't import dot_parser" error
145+
146+ -- Maarten Bezemer <maarten.bezemer@gmail.com> Sat, 15 Feb 2014 14:01:16 +0100
147+
148 pydot (1.0.2-1) unstable; urgency=low
149
150 * New upstream release.
151
152=== modified file 'debian/control'
153--- debian/control 2008-05-26 15:35:04 +0000
154+++ debian/control 2014-02-15 16:49:34 +0000
155@@ -3,19 +3,18 @@
156 Priority: optional
157 Maintainer: Peter Collingbourne <pcc03@doc.ic.ac.uk>
158 Uploaders: Debian Python Modules Team <python-modules-team@lists.alioth.debian.org>
159-Build-Depends: cdbs (>= 0.4.49), debhelper (>= 5.0.38), python (>= 2.4~), python-support (>= 0.5.3)
160-Build-Depends-Indep: python-pyparsing (>= 1.4.10)
161-Standards-Version: 3.7.3
162+Build-Depends: cdbs (>= 0.4.90~), debhelper (>= 5.0.38), python (>= 2.6.6.3~), dh-python, python-setuptools
163+Build-Depends-Indep: python-pyparsing (>= 2.0.1)
164+Standards-Version: 3.9.5
165 Vcs-Svn: svn://svn.debian.org/python-modules/packages/pydot/trunk
166-Vcs-Browser: http://svn.debian.org/wsvn/python-modules/packages/pydot/trunk/?op=log
167-DM-Upload-Allowed: yes
168+Vcs-Browser: http://svn.debian.org/wsvn/python-modules/packages/pydot/trunk
169 Homepage: http://dkbza.org/pydot.html
170+X-Python-Version: >= 2.4
171
172 Package: python-pydot
173 Architecture: all
174-Depends: ${python:Depends}, python-pyparsing (>= 1.4.10), graphviz
175+Depends: ${misc:Depends}, ${python:Depends}, python-pyparsing (>= 2.0.1), graphviz
176 Conflicts: dot2tex (<< 2.8.0)
177-XB-Python-Version: ${python:Versions}
178 Description: Python interface to Graphviz's dot
179 This package provides you with a full Python interface for creating,
180 handling, modifying and processing graphs in Graphviz's dot language.
181
182=== modified file 'debian/patches/01-setup-py-nodata.patch'
183--- debian/patches/01-setup-py-nodata.patch 2008-05-26 15:35:04 +0000
184+++ debian/patches/01-setup-py-nodata.patch 2014-02-15 16:49:34 +0000
185@@ -1,10 +1,11 @@
186-diff -Nur pydot-1.0.2/setup.py pydot-1.0.2.new/setup.py
187---- pydot-1.0.2/setup.py 2008-02-14 20:48:02.000000000 +0000
188-+++ pydot-1.0.2.new/setup.py 2008-02-22 03:08:11.000000000 +0000
189-@@ -27,5 +27,4 @@
190+Index: pydot/setup.py
191+===================================================================
192+--- pydot.orig/setup.py 2014-02-15 14:10:12.113725634 +0100
193++++ pydot/setup.py 2014-02-15 14:10:12.105725635 +0100
194+@@ -31,5 +31,4 @@
195 'Topic :: Software Development :: Libraries :: Python Modules'],
196 long_description = "\n".join(pydot.__doc__.split('\n')),
197 py_modules = ['pydot', 'dot_parser'],
198 - install_requires = ['pyparsing', 'setuptools'],
199-- data_files = [('.', ['ChangeLog', 'LICENSE', 'README'])] )
200+- data_files = [('.', ['LICENSE', 'README'])] )
201 + install_requires = ['pyparsing', 'setuptools'] )
202
203=== added file 'debian/patches/02-use-setuptools.patch'
204--- debian/patches/02-use-setuptools.patch 1970-01-01 00:00:00 +0000
205+++ debian/patches/02-use-setuptools.patch 2014-02-15 16:49:34 +0000
206@@ -0,0 +1,20 @@
207+Fix Unknown distribution option: 'install_requires', information from: http://stackoverflow.com/q/9810603/246263
208+Author: Maarten Bezemer <maarten.bezemer@gmail.com>
209+Last-Update: <2014-02-15>
210+
211+Index: pydot/setup.py
212+===================================================================
213+--- pydot.orig/setup.py 2014-02-15 16:47:04.321340325 +0100
214++++ pydot/setup.py 2014-02-15 16:47:04.317340325 +0100
215+@@ -1,9 +1,9 @@
216+ #!/usr/bin/env python
217+
218+ try:
219+- from distutils.core import setup
220+-except ImportError, excp:
221+ from setuptools import setup
222++except ImportError, excp:
223++ from distutils.core import setup
224+
225+ import pydot
226+ import os
227
228=== added file 'debian/patches/03-support-pyparsing-v2.patch'
229--- debian/patches/03-support-pyparsing-v2.patch 1970-01-01 00:00:00 +0000
230+++ debian/patches/03-support-pyparsing-v2.patch 2014-02-15 16:49:34 +0000
231@@ -0,0 +1,18 @@
232+Support pyparsing version 2, information from http://code.google.com/p/pydot/issues/detail?id=81#c9
233+Author: Maarten Bezemer <maarten.bezemer@gmail.com>
234+Last-Update: <2014-02-15>
235+Index: pydot/dot_parser.py
236+===================================================================
237+--- pydot.orig/dot_parser.py 2014-02-15 15:50:30.713479250 +0100
238++++ pydot/dot_parser.py 2014-02-15 15:50:30.709479250 +0100
239+@@ -25,8 +25,9 @@
240+ from pyparsing import ( nestedExpr, Literal, CaselessLiteral, Word, Upcase, OneOrMore, ZeroOrMore,
241+ Forward, NotAny, delimitedList, oneOf, Group, Optional, Combine, alphas, nums,
242+ restOfLine, cStyleComment, nums, alphanums, printables, empty, quotedString,
243+- ParseException, ParseResults, CharsNotIn, _noncomma, dblQuotedString, QuotedString, ParserElement )
244++ ParseException, ParseResults, CharsNotIn, dblQuotedString, QuotedString, ParserElement )
245+
246++_noncomma = "".join( [ c for c in printables if c != "," ] )
247+
248+ class P_AttrList:
249+
250
251=== added file 'debian/patches/series'
252--- debian/patches/series 1970-01-01 00:00:00 +0000
253+++ debian/patches/series 2014-02-15 16:49:34 +0000
254@@ -0,0 +1,3 @@
255+01-setup-py-nodata.patch
256+02-use-setuptools.patch
257+03-support-pyparsing-v2.patch
258
259=== removed file 'debian/pycompat'
260--- debian/pycompat 2006-11-08 15:52:04 +0000
261+++ debian/pycompat 1970-01-01 00:00:00 +0000
262@@ -1,1 +0,0 @@
263-2
264
265=== removed file 'debian/pyversions'
266--- debian/pyversions 2008-05-26 15:35:04 +0000
267+++ debian/pyversions 1970-01-01 00:00:00 +0000
268@@ -1,1 +0,0 @@
269-2.4-
270
271=== modified file 'debian/rules'
272--- debian/rules 2008-05-26 15:35:04 +0000
273+++ debian/rules 2014-02-15 16:49:34 +0000
274@@ -1,7 +1,4 @@
275 #!/usr/bin/make -f
276
277-DEB_PYTHON_SYSTEM = pysupport
278-
279 include /usr/share/cdbs/1/rules/debhelper.mk
280 include /usr/share/cdbs/1/class/python-distutils.mk
281-include /usr/share/cdbs/1/rules/simple-patchsys.mk
282
283=== added directory 'debian/source'
284=== added file 'debian/source/format'
285--- debian/source/format 1970-01-01 00:00:00 +0000
286+++ debian/source/format 2014-02-15 16:49:34 +0000
287@@ -0,0 +1,1 @@
288+3.0 (quilt)
289
290=== modified file 'dot_parser.py'
291--- dot_parser.py 2008-05-26 15:35:04 +0000
292+++ dot_parser.py 2014-02-15 16:49:34 +0000
293@@ -4,9 +4,10 @@
294 The dotparser parses graphviz files in dot and dot files and transforms them
295 into a class representation defined by pydot.
296
297-The module needs pyparsing (tested with version 1.2.2) and pydot (tested with 0.9.10)
298+The module needs pyparsing (tested with version 1.2.2) and pydot
299
300 Author: Michael Krause <michael@krause-software.de>
301+Fixes by: Ero Carrera <ero@dkbza.org>
302 """
303
304 __author__ = ['Michael Krause', 'Ero Carrera']
305@@ -17,6 +18,7 @@
306 import glob
307 import pydot
308 import re
309+import codecs
310
311 from pyparsing import __version__ as pyparsing_version
312
313@@ -26,7 +28,6 @@
314 ParseException, ParseResults, CharsNotIn, _noncomma, dblQuotedString, QuotedString, ParserElement )
315
316
317-
318 class P_AttrList:
319
320 def __init__(self, toks):
321@@ -35,12 +36,17 @@
322 i = 0
323
324 while i < len(toks):
325-
326 attrname = toks[i]
327- attrvalue = toks[i+1]
328+ if i+2 < len(toks) and toks[i+1] == '=':
329+ attrvalue = toks[i+2]
330+ i += 3
331+ else:
332+ attrvalue = None
333+ i += 1
334+
335 self.attrs[attrname] = attrvalue
336- i += 2
337-
338+
339+
340 def __repr__(self):
341
342 return "%s(%r)" % (self.__class__.__name__, self.attrs)
343@@ -107,7 +113,9 @@
344 else:
345 raise ValueError, "Unknown element statement: %r " % element
346
347-
348+
349+ for g in top_graphs:
350+ update_parent_graph_hierarchy(g)
351
352 if len( top_graphs ) == 1:
353 return top_graphs[0]
354@@ -115,6 +123,42 @@
355 return top_graphs
356
357
358+def update_parent_graph_hierarchy(g, parent_graph=None, level=0):
359+
360+
361+ if parent_graph is None:
362+ parent_graph = g
363+
364+ for key_name in ('edges',):
365+
366+ if isinstance(g, pydot.frozendict):
367+ item_dict = g
368+ else:
369+ item_dict = g.obj_dict
370+
371+ if not item_dict.has_key( key_name ):
372+ continue
373+
374+ for key, objs in item_dict[key_name].items():
375+ for obj in objs:
376+ if 'parent_graph' in obj and obj['parent_graph'].get_parent_graph()==g:
377+ if obj['parent_graph'] is g:
378+ pass
379+ else:
380+ obj['parent_graph'].set_parent_graph(parent_graph)
381+
382+ if key_name == 'edges' and len(key) == 2:
383+ for idx, vertex in enumerate( obj['points'] ):
384+ if isinstance( vertex, (pydot.Graph, pydot.Subgraph, pydot.Cluster)):
385+ vertex.set_parent_graph(parent_graph)
386+ if isinstance( vertex, pydot.frozendict):
387+ if vertex['parent_graph'] is g:
388+ pass
389+ else:
390+ vertex['parent_graph'].set_parent_graph(parent_graph)
391+
392+
393+
394 def add_defaults(element, defaults):
395
396 d = element.__dict__
397@@ -134,7 +178,7 @@
398 defaults_edge = {}
399
400 for elm_idx, element in enumerate(toks):
401-
402+
403 if isinstance(element, (pydot.Subgraph, pydot.Cluster)):
404
405 add_defaults(element, defaults_graph)
406@@ -191,14 +235,19 @@
407 return g
408
409
410-def push_subgraph_stmt(str, loc, toks):
411+def push_subgraph_stmt(str, loc, toks):
412
413 g = pydot.Subgraph('')
414-
415 for e in toks:
416 if len(e)==3:
417- g = e[2]
418- g.set_name(e[1])
419+ e[2].set_name(e[1])
420+ if e[0] == 'subgraph':
421+ e[2].obj_dict['show_keyword'] = True
422+ return e[2]
423+ else:
424+ if e[0] == 'subgraph':
425+ e[1].obj_dict['show_keyword'] = True
426+ return e[1]
427
428 return g
429
430@@ -208,7 +257,6 @@
431 # The pydot class instances should be marked as
432 # default statements to be inherited by actual
433 # graphs, nodes and edges.
434- # print "push_default_stmt", toks
435 #
436 default_type = toks[0][0]
437 if len(toks) > 1:
438@@ -362,23 +410,21 @@
439
440 # token definitions
441
442- identifier = Word(alphanums + "_" ).setName("identifier")
443+ identifier = Word(alphanums + "_." ).setName("identifier")
444
445 double_quoted_string = QuotedString('"', multiline=True, unquoteResults=False) # dblQuotedString
446
447- alphastring_ = OneOrMore(CharsNotIn(_noncomma))
448+ alphastring_ = OneOrMore(CharsNotIn(_noncomma + ' '))
449
450 def parse_html(s, loc, toks):
451-
452 return '<%s>' % ''.join(toks[0])
453
454
455 opener = '<'
456 closer = '>'
457 html_text = nestedExpr( opener, closer,
458- ( CharsNotIn(
459- opener + closer ).setParseAction( lambda t:t[0] ))
460- ).setParseAction(parse_html)
461+ ( CharsNotIn( opener + closer ) )
462+ ).setParseAction(parse_html).leaveWhitespace()
463
464 ID = ( identifier | html_text |
465 double_quoted_string | #.setParseAction(strip_quotes) |
466@@ -399,16 +445,16 @@
467 Group(port_angle + Optional(port_location))).setName("port")
468
469 node_id = (ID + Optional(port))
470- a_list = OneOrMore(ID + Optional(equals.suppress() + righthand_id) +
471+ a_list = OneOrMore(ID + Optional(equals + righthand_id) +
472 Optional(comma.suppress())).setName("a_list")
473-
474+
475 attr_list = OneOrMore(lbrack.suppress() + Optional(a_list) +
476 rbrack.suppress()).setName("attr_list")
477-
478+
479 attr_stmt = (Group(graph_ | node_ | edge_) + attr_list).setName("attr_stmt")
480-
481+
482 edgeop = (Literal("--") | Literal("->")).setName("edgeop")
483-
484+
485 stmt_list = Forward()
486 graph_stmt = Group(lbrace.suppress() + Optional(stmt_list) +
487 rbrace.suppress() + Optional(semi.suppress()) ).setName("graph_stmt")
488@@ -421,17 +467,17 @@
489
490 subgraph = Group(subgraph_ + Optional(ID) + graph_stmt).setName("subgraph")
491
492- edge_point << Group(subgraph | graph_stmt | node_id )
493-
494+ edge_point << Group( subgraph | graph_stmt | node_id ).setName('edge_point')
495+
496 node_stmt = (node_id + Optional(attr_list) + Optional(semi.suppress())).setName("node_stmt")
497
498- assignment = (ID + equals.suppress() + righthand_id).setName("assignment")
499+ assignment = (ID + equals + righthand_id).setName("assignment")
500 stmt = (assignment | edge_stmt | attr_stmt | subgraph | graph_stmt | node_stmt).setName("stmt")
501 stmt_list << OneOrMore(stmt + Optional(semi.suppress()))
502-
503- graphparser = OneOrMore( (Optional(strict_) + Group((graph_ | digraph_)) +
504+
505+ graphparser = OneOrMore( (Optional(strict_) + Group((graph_ | digraph_)) +
506 Optional(ID) + graph_stmt).setResultsName("graph") )
507-
508+
509 singleLineComment = Group("//" + restOfLine) | Group("#" + restOfLine)
510
511
512@@ -439,18 +485,18 @@
513
514 graphparser.ignore(singleLineComment)
515 graphparser.ignore(cStyleComment)
516-
517+
518 assignment.setParseAction(push_attr_list)
519 a_list.setParseAction(push_attr_list)
520 edge_stmt.setParseAction(push_edge_stmt)
521 node_stmt.setParseAction(push_node_stmt)
522 attr_stmt.setParseAction(push_default_stmt)
523-
524+
525 subgraph.setParseAction(push_subgraph_stmt)
526 graph_stmt.setParseAction(push_graph_stmt)
527 graphparser.setParseAction(push_top_graph_stmt)
528-
529
530+
531 return graphparser
532
533
534@@ -460,6 +506,9 @@
535
536 top_graphs = list()
537
538+ if data.startswith(codecs.BOM_UTF8):
539+ data = data.decode( 'utf-8' )
540+
541 try:
542
543 graphparser = graph_definition()
544
545=== removed directory 'pydot.egg-info'
546=== removed file 'pydot.egg-info/PKG-INFO'
547--- pydot.egg-info/PKG-INFO 2008-05-26 15:35:04 +0000
548+++ pydot.egg-info/PKG-INFO 1970-01-01 00:00:00 +0000
549@@ -1,35 +0,0 @@
550-Metadata-Version: 1.0
551-Name: pydot
552-Version: 1.0.2
553-Summary: Python interface to Graphviz's Dot
554-Home-page: http://code.google.com/p/pydot/
555-Author: Ero Carrera
556-Author-email: ero@dkbza.org
557-License: MIT
558-Download-URL: http://pydot.googlecode.com/files/pydot-1.0.2.tar.gz
559-Description: Graphviz's dot language Python interface.
560-
561- This module provides with a full interface to create handle modify
562- and process graphs in Graphviz's dot language.
563-
564- References:
565-
566- pydot Homepage: http://www.dkbza.org/pydot.html
567- Graphviz: http://www.research.att.com/sw/tools/graphviz/
568- DOT Language: http://www.research.att.com/~erg/graphviz/info/lang.html
569-
570- Programmed and tested with Graphviz 1.16 and Python 2.3.4 on GNU/Linux
571- by Ero Carrera (c) 2004 [ero@dkbza.org]
572-
573- Distributed under MIT license [http://opensource.org/licenses/mit-license.html].
574-
575-Keywords: graphviz dot graphs visualization
576-Platform: any
577-Classifier: Development Status :: 5 - Production/Stable
578-Classifier: Intended Audience :: Science/Research
579-Classifier: License :: OSI Approved :: MIT License
580-Classifier: Natural Language :: English
581-Classifier: Operating System :: OS Independent
582-Classifier: Programming Language :: Python
583-Classifier: Topic :: Scientific/Engineering :: Visualization
584-Classifier: Topic :: Software Development :: Libraries :: Python Modules
585
586=== removed file 'pydot.egg-info/SOURCES.txt'
587--- pydot.egg-info/SOURCES.txt 2008-05-26 15:35:04 +0000
588+++ pydot.egg-info/SOURCES.txt 1970-01-01 00:00:00 +0000
589@@ -1,11 +0,0 @@
590-ChangeLog
591-LICENSE
592-README
593-dot_parser.py
594-pydot.py
595-setup.py
596-pydot.egg-info/PKG-INFO
597-pydot.egg-info/SOURCES.txt
598-pydot.egg-info/dependency_links.txt
599-pydot.egg-info/requires.txt
600-pydot.egg-info/top_level.txt
601
602=== removed file 'pydot.egg-info/dependency_links.txt'
603--- pydot.egg-info/dependency_links.txt 2008-05-26 15:35:04 +0000
604+++ pydot.egg-info/dependency_links.txt 1970-01-01 00:00:00 +0000
605@@ -1,1 +0,0 @@
606-
607
608=== removed file 'pydot.egg-info/requires.txt'
609--- pydot.egg-info/requires.txt 2008-05-26 15:35:04 +0000
610+++ pydot.egg-info/requires.txt 1970-01-01 00:00:00 +0000
611@@ -1,2 +0,0 @@
612-pyparsing
613-setuptools
614\ No newline at end of file
615
616=== removed file 'pydot.egg-info/top_level.txt'
617--- pydot.egg-info/top_level.txt 2008-05-26 15:35:04 +0000
618+++ pydot.egg-info/top_level.txt 1970-01-01 00:00:00 +0000
619@@ -1,2 +0,0 @@
620-dot_parser
621-pydot
622
623=== modified file 'pydot.py'
624--- pydot.py 2008-05-26 15:35:04 +0000
625+++ pydot.py 2014-02-15 16:49:34 +0000
626@@ -6,18 +6,20 @@
627
628 References:
629
630-pydot Homepage: http://www.dkbza.org/pydot.html
631-Graphviz: http://www.research.att.com/sw/tools/graphviz/
632-DOT Language: http://www.research.att.com/~erg/graphviz/info/lang.html
633-
634-Programmed and tested with Graphviz 1.16 and Python 2.3.4 on GNU/Linux
635-by Ero Carrera (c) 2004 [ero@dkbza.org]
636+pydot Homepage: http://code.google.com/p/pydot/
637+Graphviz: http://www.graphviz.org/
638+DOT Language: http://www.graphviz.org/doc/info/lang.html
639+
640+Programmed and tested with Graphviz 2.26.3 and Python 2.6 on OSX 10.6.4
641+
642+Copyright (c) 2005-2011 Ero Carrera <ero.carrera@gmail.com>
643
644 Distributed under MIT license [http://opensource.org/licenses/mit-license.html].
645 """
646
647+__revision__ = "$LastChangedRevision: 28 $"
648 __author__ = 'Ero Carrera'
649-__version__ = '1.0.2'
650+__version__ = '1.0.%d' % int( __revision__[21:-2] )
651 __license__ = 'MIT'
652
653 import os
654@@ -32,51 +34,53 @@
655
656
657
658-GRAPH_ATTRIBUTES = set( ['Damping', 'K', 'URL', 'bb', 'bgcolor', 'center', 'charset',
659- 'clusterrank', 'colorscheme', 'comment', 'compound', 'concentrate',
660- 'defaultdist', 'dim', 'diredgeconstraints', 'dpi', 'epsilon', 'esep',
661- 'fontcolor', 'fontname', 'fontnames', 'fontpath', 'fontsize', 'label',
662- 'labeljust', 'labelloc', 'landscape', 'layers', 'layersep', 'levelsgap',
663- 'lp', 'margin', 'maxiter', 'mclimit', 'mindist', 'mode', 'model',
664- 'mosek', 'nodesep', 'nojustify', 'normalize', 'nslimit', 'nslimit1', 'ordering',
665- 'orientation', 'outputorder', 'overlap', 'pack', 'packmode', 'pad',
666- 'page', 'pagedir', 'quantum', 'rankdir', 'ranksep', 'ratio', 'remincross',
667- 'resolution', 'root', 'rotate', 'searchsize', 'sep', 'showboxes', 'size',
668- 'splines', 'start', 'stylesheet', 'target', 'truecolor', 'viewport',
669- 'voro_margin'] + ['rank'] )
670-
671-
672-EDGE_ATTRIBUTES = set( ['URL', 'arrowhead', 'arrowsize', 'arrowtail', 'color',
673- 'colorscheme', 'comment', 'constraint', 'decorate', 'dir',
674- 'edgeURL', 'edgehref', 'edgetarget', 'edgetooltip',
675- 'fontcolor', 'fontname', 'fontsize', 'headURL', 'headclip',
676- 'headhref', 'headlabel', 'headport', 'headtarget',
677- 'headtooltip', 'href', 'label', 'labelURL', 'labelangle',
678- 'labeldistance', 'labelfloat', 'labelfontcolor',
679+GRAPH_ATTRIBUTES = set( ['Damping', 'K', 'URL', 'aspect', 'bb', 'bgcolor',
680+ 'center', 'charset', 'clusterrank', 'colorscheme', 'comment', 'compound',
681+ 'concentrate', 'defaultdist', 'dim', 'dimen', 'diredgeconstraints',
682+ 'dpi', 'epsilon', 'esep', 'fontcolor', 'fontname', 'fontnames',
683+ 'fontpath', 'fontsize', 'id', 'label', 'labeljust', 'labelloc',
684+ 'landscape', 'layers', 'layersep', 'layout', 'levels', 'levelsgap',
685+ 'lheight', 'lp', 'lwidth', 'margin', 'maxiter', 'mclimit', 'mindist',
686+ 'mode', 'model', 'mosek', 'nodesep', 'nojustify', 'normalize', 'nslimit',
687+ 'nslimit1', 'ordering', 'orientation', 'outputorder', 'overlap',
688+ 'overlap_scaling', 'pack', 'packmode', 'pad', 'page', 'pagedir',
689+ 'quadtree', 'quantum', 'rankdir', 'ranksep', 'ratio', 'remincross',
690+ 'repulsiveforce', 'resolution', 'root', 'rotate', 'searchsize', 'sep',
691+ 'showboxes', 'size', 'smoothing', 'sortv', 'splines', 'start',
692+ 'stylesheet', 'target', 'truecolor', 'viewport', 'voro_margin',
693+ # for subgraphs
694+ 'rank' ] )
695+
696+
697+EDGE_ATTRIBUTES = set( ['URL', 'arrowhead', 'arrowsize', 'arrowtail',
698+ 'color', 'colorscheme', 'comment', 'constraint', 'decorate', 'dir',
699+ 'edgeURL', 'edgehref', 'edgetarget', 'edgetooltip', 'fontcolor',
700+ 'fontname', 'fontsize', 'headURL', 'headclip', 'headhref', 'headlabel',
701+ 'headport', 'headtarget', 'headtooltip', 'href', 'id', 'label',
702+ 'labelURL', 'labelangle', 'labeldistance', 'labelfloat', 'labelfontcolor',
703 'labelfontname', 'labelfontsize', 'labelhref', 'labeltarget',
704- 'labeltooltip', 'layer', 'len', 'lhead', 'lp', 'ltail',
705- 'minlen', 'nojustify', 'pos', 'samehead', 'sametail',
706- 'showboxes', 'style', 'tailURL', 'tailclip', 'tailhref',
707- 'taillabel', 'tailport', 'tailtarget', 'tailtooltip',
708- 'target', 'tooltip', 'weight'] + ['rank'] )
709+ 'labeltooltip', 'layer', 'len', 'lhead', 'lp', 'ltail', 'minlen',
710+ 'nojustify', 'penwidth', 'pos', 'samehead', 'sametail', 'showboxes',
711+ 'style', 'tailURL', 'tailclip', 'tailhref', 'taillabel', 'tailport',
712+ 'tailtarget', 'tailtooltip', 'target', 'tooltip', 'weight',
713+ 'rank' ] )
714
715
716 NODE_ATTRIBUTES = set( ['URL', 'color', 'colorscheme', 'comment',
717 'distortion', 'fillcolor', 'fixedsize', 'fontcolor', 'fontname',
718- 'fontsize', 'group', 'height', 'image', 'imagescale', 'label',
719- 'layer', 'margin', 'nojustify', 'orientation', 'peripheries',
720- 'pin', 'pos', 'rects', 'regular', 'root', 'samplepoints',
721- 'shape', 'shapefile', 'showboxes', 'sides', 'skew', 'style',
722+ 'fontsize', 'group', 'height', 'id', 'image', 'imagescale', 'label',
723+ 'labelloc', 'layer', 'margin', 'nojustify', 'orientation', 'penwidth',
724+ 'peripheries', 'pin', 'pos', 'rects', 'regular', 'root', 'samplepoints',
725+ 'shape', 'shapefile', 'showboxes', 'sides', 'skew', 'sortv', 'style',
726 'target', 'tooltip', 'vertices', 'width', 'z',
727-
728 # The following are attributes dot2tex
729 'texlbl', 'texmode' ] )
730
731
732-CLUSTER_ATTRIBUTES = set( ['K', 'URL', 'bgcolor', 'color', 'colorscheme', 'fillcolor',
733- 'fontcolor', 'fontname', 'fontsize', 'label', 'labeljust',
734- 'labelloc', 'lp', 'nojustify', 'pencolor', 'peripheries',
735- 'style', 'target', 'tooltip'] )
736+CLUSTER_ATTRIBUTES = set( ['K', 'URL', 'bgcolor', 'color', 'colorscheme',
737+ 'fillcolor', 'fontcolor', 'fontname', 'fontsize', 'label', 'labeljust',
738+ 'labelloc', 'lheight', 'lp', 'lwidth', 'nojustify', 'pencolor',
739+ 'penwidth', 'peripheries', 'sortv', 'style', 'target', 'tooltip'] )
740
741
742 #
743@@ -101,8 +105,10 @@
744 for arg in args:
745 if isinstance(arg, dict):
746 arg = copy.copy(arg)
747- for k, v in arg.items():
748- if isinstance(v, dict):
749+ for k, v in arg.iteritems():
750+ if isinstance(v, frozendict):
751+ arg[k] = v
752+ elif isinstance(v, dict):
753 arg[k] = frozendict(v)
754 elif isinstance(v, list):
755 v_ = list()
756@@ -126,7 +132,7 @@
757 try:
758 return self._cached_hash
759 except AttributeError:
760- h = self._cached_hash = hash(tuple(sorted(self.items())))
761+ h = self._cached_hash = hash(tuple(sorted(self.iteritems())))
762 return h
763
764 def __repr__(self):
765@@ -135,11 +141,12 @@
766
767 dot_keywords = ['graph', 'subgraph', 'digraph', 'node', 'edge', 'strict']
768
769-id_re_alpha_nums = re.compile('^[_a-zA-Z][a-zA-Z0-9_:,]*$')
770-id_re_num = re.compile('^[0-9]+$')
771-id_re_with_port = re.compile('^.*:([^"]+|[^"]*\"[^"]*\"[^"]*)$')
772-id_re_dbl_quoted = re.compile('^\".*\"$', re.S)
773-id_re_html = re.compile('^<.*>$', re.S)
774+id_re_alpha_nums = re.compile('^[_a-zA-Z][a-zA-Z0-9_,]*$', re.UNICODE)
775+id_re_alpha_nums_with_ports = re.compile('^[_a-zA-Z][a-zA-Z0-9_,:\"]*[a-zA-Z0-9_,\"]+$', re.UNICODE)
776+id_re_num = re.compile('^[0-9,]+$', re.UNICODE)
777+id_re_with_port = re.compile('^([^:]*):([^:]*)$', re.UNICODE)
778+id_re_dbl_quoted = re.compile('^\".*\"$', re.S|re.UNICODE)
779+id_re_html = re.compile('^<.*>$', re.S|re.UNICODE)
780
781
782 def needs_quotes( s ):
783@@ -148,40 +155,54 @@
784 It will check whether the string is solely composed
785 by the characters allowed in an ID or not.
786 If the string is one of the reserved keywords it will
787- need quotes too.
788+ need quotes too but the user will need to add them
789+ manually.
790 """
791-
792+
793+ # If the name is a reserved keyword it will need quotes but pydot
794+ # can't tell when it's being used as a keyword or when it's simply
795+ # a name. Hence the user needs to supply the quotes when an element
796+ # would use a reserved keyword as name. This function will return
797+ # false indicating that a keyword string, if provided as-is, won't
798+ # need quotes.
799 if s in dot_keywords:
800 return False
801
802 chars = [ord(c) for c in s if ord(c)>0x7f or ord(c)==0]
803- if chars:
804- return False
805+ if chars and not id_re_dbl_quoted.match(s) and not id_re_html.match(s):
806+ return True
807
808- res = id_re_alpha_nums.match(s)
809- if not res:
810- res = id_re_num.match(s)
811- if not res:
812- res = id_re_dbl_quoted.match(s)
813- if not res:
814- res = id_re_html.match(s)
815- if not res:
816- res = id_re_with_port.match(s)
817-
818- if not res:
819- return True
820-
821- return False
822-
823+ for test_re in [id_re_alpha_nums, id_re_num, id_re_dbl_quoted, id_re_html, id_re_alpha_nums_with_ports]:
824+ if test_re.match(s):
825+ return False
826+
827+ m = id_re_with_port.match(s)
828+ if m:
829+ return needs_quotes(m.group(1)) or needs_quotes(m.group(2))
830+
831+ return True
832
833
834 def quote_if_necessary(s):
835
836+ if isinstance(s, bool):
837+ if s is True:
838+ return 'True'
839+ return 'False'
840+
841 if not isinstance( s, basestring ):
842 return s
843
844+ if not s:
845+ return s
846+
847 if needs_quotes(s):
848-
849+ replace = {'"' : r'\"',
850+ "\n" : r'\n',
851+ "\r" : r'\r'}
852+ for (a,b) in replace.items():
853+ s = s.replace(a, b)
854+
855 return '"' + s + '"'
856
857 return s
858@@ -233,8 +254,18 @@
859 graph = Dot(graph_type='graph')
860
861 for edge in edge_list:
862+
863+ if isinstance(edge[0], str):
864+ src = node_prefix + edge[0]
865+ else:
866+ src = node_prefix + str(edge[0])
867+
868+ if isinstance(edge[1], str):
869+ dst = node_prefix + edge[1]
870+ else:
871+ dst = node_prefix + str(edge[1])
872
873- e = Edge( node_prefix + edge[0], node_prefix + edge[1] )
874+ e = Edge( src, dst )
875 graph.add_edge(e)
876
877 return graph
878@@ -328,7 +359,7 @@
879 """
880
881 success = False
882- progs = {'dot': '', 'twopi': '', 'neato': '', 'circo': '', 'fdp': ''}
883+ progs = {'dot': '', 'twopi': '', 'neato': '', 'circo': '', 'fdp': '', 'sfdp': ''}
884
885 was_quoted = False
886 path = path.strip()
887@@ -338,7 +369,7 @@
888
889 if os.path.isdir(path) :
890
891- for prg in progs.keys():
892+ for prg in progs.iterkeys():
893
894 if progs[prg]:
895 continue
896@@ -399,30 +430,89 @@
897 # Method 1 (Windows only)
898 #
899 if os.sys.platform == 'win32':
900-
901+
902+ HKEY_LOCAL_MACHINE = 0x80000002
903+ KEY_QUERY_VALUE = 0x0001
904+
905+ RegOpenKeyEx = None
906+ RegQueryValueEx = None
907+ RegCloseKey = None
908+
909 try:
910 import win32api, win32con
911+ RegOpenKeyEx = win32api.RegOpenKeyEx
912+ RegQueryValueEx = win32api.RegQueryValueEx
913+ RegCloseKey = win32api.RegCloseKey
914+
915+ except ImportError:
916+ # Print a messaged suggesting they install these?
917+ #
918+ pass
919+
920+ try:
921+ import ctypes
922+
923+ def RegOpenKeyEx(key, subkey, opt, sam):
924+ result = ctypes.c_uint(0)
925+ ctypes.windll.advapi32.RegOpenKeyExA(key, subkey, opt, sam, ctypes.byref(result))
926+ return result.value
927+
928+ def RegQueryValueEx( hkey, valuename ):
929+ data_type = ctypes.c_uint(0)
930+ data_len = ctypes.c_uint(1024)
931+ data = ctypes.create_string_buffer( 1024 )
932+
933+ res = ctypes.windll.advapi32.RegQueryValueExA(hkey, valuename, 0,
934+ ctypes.byref(data_type), data, ctypes.byref(data_len))
935+
936+ return data.value
937+
938+ RegCloseKey = ctypes.windll.advapi32.RegCloseKey
939+
940+ except ImportError:
941+ # Print a messaged suggesting they install these?
942+ #
943+ pass
944+
945+ if RegOpenKeyEx is not None:
946
947 # Get the GraphViz install path from the registry
948 #
949- hkey = win32api.RegOpenKeyEx( win32con.HKEY_LOCAL_MACHINE,
950- "SOFTWARE\ATT\Graphviz", 0, win32con.KEY_QUERY_VALUE )
951-
952- path = win32api.RegQueryValueEx( hkey, "InstallPath" )[0]
953- win32api.RegCloseKey( hkey )
954-
955- # Now append the "bin" subdirectory:
956- #
957- path = os.path.join(path, "bin")
958- progs = __find_executables(path)
959- if progs is not None :
960- #print "Used Windows registry"
961- return progs
962-
963- except ImportError :
964- # Print a messaged suggesting they install these?
965- #
966- pass
967+ hkey = None
968+ potentialKeys = [
969+ "SOFTWARE\\ATT\\Graphviz",
970+ "SOFTWARE\\AT&T Research Labs\\Graphviz",
971+ ]
972+ for potentialKey in potentialKeys:
973+
974+ try:
975+ hkey = RegOpenKeyEx( HKEY_LOCAL_MACHINE,
976+ potentialKey, 0, KEY_QUERY_VALUE )
977+
978+ if hkey is not None:
979+ path = RegQueryValueEx( hkey, "InstallPath" )
980+ RegCloseKey( hkey )
981+
982+ # The regitry variable might exist, left by old installations
983+ # but with no value, in those cases we keep searching...
984+ if not path:
985+ continue
986+
987+ # Now append the "bin" subdirectory:
988+ #
989+ path = os.path.join(path, "bin")
990+ progs = __find_executables(path)
991+ if progs is not None :
992+ #print "Used Windows registry"
993+ return progs
994+
995+ except Exception, excp:
996+ #raise excp
997+ pass
998+ else:
999+ break
1000+
1001+
1002
1003 # Method 2 (Linux, Windows etc)
1004 #
1005@@ -464,6 +554,7 @@
1006
1007 for path in (
1008 '/usr/bin', '/usr/local/bin',
1009+ '/opt/local/bin',
1010 '/opt/bin', '/sw/bin', '/usr/share',
1011 '/Applications/Graphviz.app/Contents/MacOS/' ):
1012
1013@@ -487,20 +578,14 @@
1014
1015 def __getstate__(self):
1016
1017- dict = copy.copy(self.__dict__)
1018- for attr in self.attributes.keys():
1019-
1020- del dict['set_'+attr]
1021- del dict['get_'+attr]
1022+ dict = copy.copy(self.obj_dict)
1023
1024 return dict
1025
1026
1027 def __setstate__(self, state):
1028
1029- for k, v in state.items():
1030-
1031- self.__setattr__(k, v)
1032+ self.obj_dict = state
1033
1034
1035 def __get_attribute__(self, attr):
1036@@ -517,9 +602,25 @@
1037 if default_node_name in ('subgraph', 'digraph', 'cluster'):
1038 default_node_name = 'graph'
1039
1040- defaults = self.get_parent_graph().get_node( default_node_name )
1041- if defaults:
1042- attr_val = defaults.obj_dict['attributes'].get(attr, None)
1043+ g = self.get_parent_graph()
1044+ if g is not None:
1045+ defaults = g.get_node( default_node_name )
1046+ else:
1047+ return None
1048+
1049+ # Multiple defaults could be set by having repeated 'graph [...]'
1050+ # 'node [...]', 'edge [...]' statements. In such case, if the
1051+ # same attribute is set in different statements, only the first
1052+ # will be returned. In order to get all, one would call the
1053+ # get_*_defaults() methods and handle those. Or go node by node
1054+ # (of the ones specifying defaults) and modify the attributes
1055+ # individually.
1056+ #
1057+ if not isinstance(defaults, (list, tuple)):
1058+ defaults = [defaults]
1059+
1060+ for default in defaults:
1061+ attr_val = default.obj_dict['attributes'].get(attr, None)
1062 if attr_val:
1063 return attr_val
1064 else:
1065@@ -549,14 +650,7 @@
1066 which are defined for all the existing attributes.
1067 """
1068
1069- if self.obj_dict['attributes'].has_key(name):
1070- self.obj_dict['attributes'][name] = value
1071- return True
1072-
1073- # Attribute is not known
1074- #
1075- return False
1076-
1077+ self.obj_dict['attributes'][name] = value
1078
1079
1080 def get(self, name):
1081@@ -664,7 +758,7 @@
1082 port = None
1083 if isinstance(name, basestring) and not name.startswith('"'):
1084 idx = name.find(':')
1085- if idx > 0:
1086+ if idx > 0 and idx+1 < len(name):
1087 name, port = name[:idx], name[idx:]
1088
1089 if isinstance(name, (long, int)):
1090@@ -718,8 +812,11 @@
1091
1092 node_attr = list()
1093
1094- for attr, value in self.obj_dict['attributes'].items():
1095- node_attr.append( attr + '=' + quote_if_necessary(value) )
1096+ for attr, value in self.obj_dict['attributes'].iteritems():
1097+ if value is not None:
1098+ node_attr.append( '%s=%s' % (attr, quote_if_necessary(value) ) )
1099+ else:
1100+ node_attr.append( attr )
1101
1102
1103 # No point in having nodes setting any defaults if the don't set
1104@@ -754,15 +851,22 @@
1105
1106 set_[attribute name], i.e. set_label, set_fontname
1107
1108- or using the instance's attributes:
1109+ or directly by using the instance's special dictionary:
1110
1111- Edge.[attribute name], i.e. edge_instance.label, edge_instance.fontname
1112+ Edge.obj_dict['attributes'][attribute name], i.e.
1113+
1114+ edge_instance.obj_dict['attributes']['label']
1115+ edge_instance.obj_dict['attributes']['fontname']
1116+
1117 """
1118
1119
1120
1121 def __init__(self, src='', dst='', obj_dict=None, **attrs):
1122
1123+ if isinstance(src, (list, tuple)) and dst == '':
1124+ src, dst = src
1125+
1126 if obj_dict is not None:
1127
1128 self.obj_dict = obj_dict
1129@@ -804,6 +908,11 @@
1130 return self.obj_dict['points'][1]
1131
1132
1133+ def __hash__(self):
1134+
1135+ return hash( hash(self.get_source()) + hash(self.get_destination()) )
1136+
1137+
1138 def __eq__(self, edge):
1139 """Compare two edges.
1140
1141@@ -823,8 +932,8 @@
1142 # If the graph is undirected, the edge has neither
1143 # source nor destination.
1144 #
1145- if ( ( self.get_source()==edge.get_source() and self.get_destination()==edge.get_destination() ) or
1146- ( edge.get_source() == get_destination() and self.get_destination() == edge.get_source() ) ):
1147+ if ( ( self.get_source() == edge.get_source() and self.get_destination() == edge.get_destination() ) or
1148+ ( edge.get_source() == self.get_destination() and edge.get_destination() == self.get_source() ) ):
1149 return True
1150
1151 else:
1152@@ -841,7 +950,7 @@
1153 if not isinstance(node_str, str):
1154 return node_str
1155
1156- if node_str.startswith('"') and node_str.endswith('"') and node_str.count('"') % 2 != 0:
1157+ if node_str.startswith('"') and node_str.endswith('"'):
1158
1159 return node_str
1160
1161@@ -874,36 +983,42 @@
1162
1163 if isinstance(src, frozendict):
1164 edge = [ Subgraph(obj_dict=src).to_string() ]
1165+ elif isinstance(src, (int, long)):
1166+ edge = [ str(src) ]
1167 else:
1168 edge = [ src ]
1169
1170 if (self.get_parent_graph() and
1171 self.get_parent_graph().get_top_graph_type() and
1172 self.get_parent_graph().get_top_graph_type() == 'digraph' ):
1173-
1174+
1175 edge.append( '->' )
1176
1177 else:
1178-
1179 edge.append( '--' )
1180
1181 if isinstance(dst, frozendict):
1182 edge.append( Subgraph(obj_dict=dst).to_string() )
1183+ elif isinstance(dst, (int, long)):
1184+ edge.append( str(dst) )
1185 else:
1186 edge.append( dst )
1187
1188
1189 edge_attr = list()
1190
1191- for attr, value in self.obj_dict['attributes'].items():
1192+ for attr, value in self.obj_dict['attributes'].iteritems():
1193
1194- edge_attr.append( attr + '=' + quote_if_necessary(value) )
1195+ if value is not None:
1196+ edge_attr.append( '%s=%s' % (attr, quote_if_necessary(value) ) )
1197+ else:
1198+ edge_attr.append( attr )
1199
1200 edge_attr = ', '.join(edge_attr)
1201
1202 if edge_attr:
1203 edge.append( ' [' + edge_attr + ']' )
1204-
1205+
1206 return ' '.join(edge) + ';'
1207
1208
1209@@ -940,7 +1055,10 @@
1210
1211 or using the instance's attributes:
1212
1213- Graph.[attribute name], i.e. graph_instance.label, graph_instance.fontname
1214+ Graph.obj_dict['attributes'][attribute name], i.e.
1215+
1216+ graph_instance.obj_dict['attributes']['label']
1217+ graph_instance.obj_dict['attributes']['fontname']
1218 """
1219
1220
1221@@ -960,7 +1078,7 @@
1222 raise Error, 'Invalid type "%s". Accepted graph types are: graph, digraph, subgraph' % graph_type
1223
1224
1225- self.obj_dict['name'] = graph_name
1226+ self.obj_dict['name'] = quote_if_necessary(graph_name)
1227 self.obj_dict['type'] = graph_type
1228
1229 self.obj_dict['strict'] = strict
1230@@ -1003,8 +1121,12 @@
1231 def get_graph_defaults(self, **attrs):
1232
1233 graph_nodes = self.get_node('graph')
1234-
1235- return [ node.get_attributes() for node in graph_nodes ]
1236+
1237+ if isinstance( graph_nodes, (list, tuple)):
1238+ return [ node.get_attributes() for node in graph_nodes ]
1239+
1240+ return graph_nodes.get_attributes()
1241+
1242
1243
1244 def set_node_defaults(self, **attrs):
1245@@ -1017,7 +1139,10 @@
1246
1247 graph_nodes = self.get_node('node')
1248
1249- return [ node.get_attributes() for node in graph_nodes ]
1250+ if isinstance( graph_nodes, (list, tuple)):
1251+ return [ node.get_attributes() for node in graph_nodes ]
1252+
1253+ return graph_nodes.get_attributes()
1254
1255
1256 def set_edge_defaults(self, **attrs):
1257@@ -1030,7 +1155,10 @@
1258
1259 graph_nodes = self.get_node('edge')
1260
1261- return [ node.get_attributes() for node in graph_nodes ]
1262+ if isinstance( graph_nodes, (list, tuple)):
1263+ return [ node.get_attributes() for node in graph_nodes ]
1264+
1265+ return graph_nodes.get_attributes()
1266
1267
1268
1269@@ -1106,7 +1234,7 @@
1270 def set_suppress_disconnected(self, val):
1271 """Suppress disconnected nodes in the output graph.
1272
1273- This option will skip nodes in the graph with no incoming or outgoing
1274+ This option will skip nodes in the graph with no incoming or outgoing
1275 edges. This option works also for subgraphs and has effect only in the
1276 current graph/subgraph.
1277 """
1278@@ -1133,6 +1261,7 @@
1279 return seq
1280
1281
1282+
1283 def add_node(self, graph_node):
1284 """Adds a node object to the graph.
1285
1286@@ -1141,7 +1270,7 @@
1287 """
1288
1289 if not isinstance(graph_node, Node):
1290- raise TypeError('add_node() received a non node class object')
1291+ raise TypeError('add_node() received a non node class object: ' + str(graph_node))
1292
1293
1294 node = self.get_node(graph_node.get_name())
1295@@ -1161,16 +1290,48 @@
1296
1297
1298
1299+ def del_node(self, name, index=None):
1300+ """Delete a node from the graph.
1301+
1302+ Given a node's name all node(s) with that same name
1303+ will be deleted if 'index' is not specified or set
1304+ to None.
1305+ If there are several nodes with that same name and
1306+ 'index' is given, only the node in that position
1307+ will be deleted.
1308+
1309+ 'index' should be an integer specifying the position
1310+ of the node to delete. If index is larger than the
1311+ number of nodes with that name, no action is taken.
1312+
1313+ If nodes are deleted it returns True. If no action
1314+ is taken it returns False.
1315+ """
1316+
1317+ if isinstance(name, Node):
1318+ name = name.get_name()
1319+
1320+ if self.obj_dict['nodes'].has_key(name):
1321+
1322+ if index is not None and index < len(self.obj_dict['nodes'][name]):
1323+ del self.obj_dict['nodes'][name][index]
1324+ return True
1325+ else:
1326+ del self.obj_dict['nodes'][name]
1327+ return True
1328+
1329+ return False
1330+
1331+
1332 def get_node(self, name):
1333- """Retrieved a node from the graph.
1334+ """Retrieve a node from the graph.
1335
1336 Given a node's name the corresponding Node
1337 instance will be returned.
1338
1339- If multiple nodes exist with that name, a list of
1340+ If one or more nodes exist with that name a list of
1341 Node instances is returned.
1342- If only one node exists, the instance is returned.
1343- None is returned otherwise.
1344+ An empty list is returned otherwise.
1345 """
1346
1347 match = list()
1348@@ -1179,14 +1340,11 @@
1349
1350 match.extend( [ Node( obj_dict = obj_dict ) for obj_dict in self.obj_dict['nodes'][name] ])
1351
1352- if len(match)==1:
1353- return match[0]
1354-
1355 return match
1356
1357
1358 def get_nodes(self):
1359- """Return an iterator."""
1360+ """Get the list of Node instances."""
1361
1362 return self.get_node_list()
1363
1364@@ -1200,7 +1358,7 @@
1365
1366 node_objs = list()
1367
1368- for node, obj_dict_list in self.obj_dict['nodes'].items():
1369+ for node, obj_dict_list in self.obj_dict['nodes'].iteritems():
1370 node_objs.extend( [ Node( obj_dict = obj_d ) for obj_d in obj_dict_list ] )
1371
1372 return node_objs
1373@@ -1215,56 +1373,98 @@
1374 """
1375
1376 if not isinstance(graph_edge, Edge):
1377- raise TypeError('add_edge() received a non edge class object')
1378-
1379+ raise TypeError('add_edge() received a non edge class object: ' + str(graph_edge))
1380+
1381 edge_points = ( graph_edge.get_source(), graph_edge.get_destination() )
1382
1383 if self.obj_dict['edges'].has_key(edge_points):
1384-
1385+
1386 edge_list = self.obj_dict['edges'][edge_points]
1387 edge_list.append(graph_edge.obj_dict)
1388-
1389+
1390 else:
1391-
1392+
1393 self.obj_dict['edges'][edge_points] = [ graph_edge.obj_dict ]
1394-
1395+
1396+
1397 graph_edge.set_sequence( self.get_next_sequence_number() )
1398-
1399+
1400 graph_edge.set_parent_graph( self.get_parent_graph() )
1401-
1402-
1403-
1404-
1405- def get_edge(self, src, dst):
1406+
1407+
1408+
1409+ def del_edge(self, src_or_list, dst=None, index=None):
1410+ """Delete an edge from the graph.
1411+
1412+ Given an edge's (source, destination) node names all
1413+ matching edges(s) will be deleted if 'index' is not
1414+ specified or set to None.
1415+ If there are several matching edges and 'index' is
1416+ given, only the edge in that position will be deleted.
1417+
1418+ 'index' should be an integer specifying the position
1419+ of the edge to delete. If index is larger than the
1420+ number of matching edges, no action is taken.
1421+
1422+ If edges are deleted it returns True. If no action
1423+ is taken it returns False.
1424+ """
1425+
1426+ if isinstance( src_or_list, (list, tuple)):
1427+ if dst is not None and isinstance(dst, (int, long)):
1428+ index = dst
1429+ src, dst = src_or_list
1430+ else:
1431+ src, dst = src_or_list, dst
1432+
1433+ if isinstance(src, Node):
1434+ src = src.get_name()
1435+
1436+ if isinstance(dst, Node):
1437+ dst = dst.get_name()
1438+
1439+ if self.obj_dict['edges'].has_key( (src, dst) ):
1440+
1441+ if index is not None and index < len(self.obj_dict['edges'][(src, dst)]):
1442+ del self.obj_dict['edges'][(src, dst)][index]
1443+ return True
1444+ else:
1445+ del self.obj_dict['edges'][(src, dst)]
1446+ return True
1447+
1448+ return False
1449+
1450+
1451+ def get_edge(self, src_or_list, dst=None):
1452 """Retrieved an edge from the graph.
1453
1454 Given an edge's source and destination the corresponding
1455- Edge instance will be returned.
1456+ Edge instance(s) will be returned.
1457
1458- If multiple edges exist with that source and destination,
1459+ If one or more edges exist with that source and destination
1460 a list of Edge instances is returned.
1461- If only one edge exists, the instance is returned.
1462- None is returned otherwise.
1463+ An empty list is returned otherwise.
1464 """
1465
1466- edge_points = (src, dst)
1467-
1468+ if isinstance( src_or_list, (list, tuple)) and dst is None:
1469+ edge_points = tuple(src_or_list)
1470+ edge_points_reverse = (edge_points[1], edge_points[0])
1471+ else:
1472+ edge_points = (src_or_list, dst)
1473+ edge_points_reverse = (dst, src_or_list)
1474
1475 match = list()
1476
1477- if self.obj_dict['edges'].has_key( (src, dst) ) or (
1478- self.get_top_graph_type() == 'graph' and self.obj_dict['edges'].has_key( (dst, src) )):
1479+ if self.obj_dict['edges'].has_key( edge_points ) or (
1480+ self.get_top_graph_type() == 'graph' and self.obj_dict['edges'].has_key( edge_points_reverse )):
1481
1482 edges_obj_dict = self.obj_dict['edges'].get(
1483- (src, dst),
1484- self.obj_dict['edges'].get( (dst, src), None ))
1485+ edge_points,
1486+ self.obj_dict['edges'].get( edge_points_reverse, None ))
1487
1488 for edge_obj_dict in edges_obj_dict:
1489 match.append( Edge( edge_points[0], edge_points[1], obj_dict = edge_obj_dict ) )
1490-
1491- if len(match)==1:
1492- return match[0]
1493-
1494+
1495 return match
1496
1497
1498@@ -1281,7 +1481,7 @@
1499
1500 edge_objs = list()
1501
1502- for edge, obj_dict_list in self.obj_dict['edges'].items():
1503+ for edge, obj_dict_list in self.obj_dict['edges'].iteritems():
1504 edge_objs.extend( [ Edge( obj_dict = obj_d ) for obj_d in obj_dict_list ] )
1505
1506 return edge_objs
1507@@ -1296,7 +1496,7 @@
1508 """
1509
1510 if not isinstance(sgraph, Subgraph) and not isinstance(sgraph, Cluster):
1511- raise TypeError('add_subgraph() received a non subgraph class object')
1512+ raise TypeError('add_subgraph() received a non subgraph class object:' + str(sgraph))
1513
1514 if self.obj_dict['subgraphs'].has_key(sgraph.get_name()):
1515
1516@@ -1319,30 +1519,27 @@
1517 Given a subgraph's name the corresponding
1518 Subgraph instance will be returned.
1519
1520- If multiple subgraphs exist with the same name, a list of
1521+ If one or more subgraphs exist with the same name, a list of
1522 Subgraph instances is returned.
1523- If only one Subgraph exists, the instance is returned.
1524- None is returned otherwise.
1525+ An empty list is returned otherwise.
1526 """
1527
1528- match = None
1529-
1530- if self.obj_dict['subgraphs'].has_key( sgraph.get_name() ):
1531-
1532- sgraphs_obj_dict = self.obj_dict['subgraphs'].get( sgraph.get_name() )
1533+ match = list()
1534+
1535+ if self.obj_dict['subgraphs'].has_key( name ):
1536+
1537+ sgraphs_obj_dict = self.obj_dict['subgraphs'].get( name )
1538
1539 for obj_dict_list in sgraphs_obj_dict:
1540- match = [ Subgraph( obj_dict = obj_d ) for obj_d in obj_dict_list ]
1541+ #match.extend( Subgraph( obj_dict = obj_d ) for obj_d in obj_dict_list )
1542+ match.append( Subgraph( obj_dict = obj_dict_list ) )
1543
1544- if len(match)==1:
1545- return match[0]
1546-
1547 return match
1548
1549
1550 def get_subgraphs(self):
1551
1552- return get_subgraph_list()
1553+ return self.get_subgraph_list()
1554
1555
1556 def get_subgraph_list(self):
1557@@ -1354,7 +1551,7 @@
1558
1559 sgraph_objs = list()
1560
1561- for sgraph, obj_dict_list in self.obj_dict['subgraphs'].items():
1562+ for sgraph, obj_dict_list in self.obj_dict['subgraphs'].iteritems():
1563 sgraph_objs.extend( [ Subgraph( obj_dict = obj_d ) for obj_d in obj_dict_list ] )
1564
1565 return sgraph_objs
1566@@ -1365,15 +1562,15 @@
1567
1568 self.obj_dict['parent_graph'] = parent_graph
1569
1570- for obj_list in self.obj_dict['nodes'].values():
1571- for obj in obj_list:
1572- obj['parent_graph'] = parent_graph
1573-
1574- for obj_list in self.obj_dict['edges'].values():
1575- for obj in obj_list:
1576- obj['parent_graph'] = parent_graph
1577-
1578- for obj_list in self.obj_dict['subgraphs'].values():
1579+ for obj_list in self.obj_dict['nodes'].itervalues():
1580+ for obj in obj_list:
1581+ obj['parent_graph'] = parent_graph
1582+
1583+ for obj_list in self.obj_dict['edges'].itervalues():
1584+ for obj in obj_list:
1585+ obj['parent_graph'] = parent_graph
1586+
1587+ for obj_list in self.obj_dict['subgraphs'].itervalues():
1588 for obj in obj_list:
1589 Graph(obj_dict=obj).set_parent_graph(parent_graph)
1590
1591@@ -1393,21 +1590,25 @@
1592 if self==self.get_parent_graph() and self.obj_dict['strict']:
1593
1594 graph.append('strict ')
1595-
1596+
1597 if self.obj_dict['name'] == '':
1598- graph.append( '{\n' )
1599+ if 'show_keyword' in self.obj_dict and self.obj_dict['show_keyword']:
1600+ graph.append( 'subgraph {\n' )
1601+ else:
1602+ graph.append( '{\n' )
1603 else:
1604 graph.append( '%s %s {\n' % (self.obj_dict['type'], self.obj_dict['name']) )
1605
1606
1607- for attr in self.obj_dict['attributes'].keys():
1608+ for attr in self.obj_dict['attributes'].iterkeys():
1609
1610 if self.obj_dict['attributes'].get(attr, None) is not None:
1611
1612- graph.append( '%s=' % attr )
1613 val = self.obj_dict['attributes'].get(attr)
1614-
1615- graph.append( quote_if_necessary(val) )
1616+ if val is not None:
1617+ graph.append( '%s=%s' % (attr, quote_if_necessary(val)) )
1618+ else:
1619+ graph.append( attr )
1620
1621 graph.append( ';\n' )
1622
1623@@ -1415,7 +1616,7 @@
1624 edges_done = set()
1625
1626 edge_obj_dicts = list()
1627- for e in self.obj_dict['edges'].values():
1628+ for e in self.obj_dict['edges'].itervalues():
1629 edge_obj_dicts.extend(e)
1630
1631 if edge_obj_dicts:
1632@@ -1425,11 +1626,11 @@
1633 edge_src_set, edge_dst_set = set(), set()
1634
1635 node_obj_dicts = list()
1636- for e in self.obj_dict['nodes'].values():
1637+ for e in self.obj_dict['nodes'].itervalues():
1638 node_obj_dicts.extend(e)
1639
1640 sgraph_obj_dicts = list()
1641- for sg in self.obj_dict['subgraphs'].values():
1642+ for sg in self.obj_dict['subgraphs'].itervalues():
1643 sgraph_obj_dicts.extend(sg)
1644
1645
1646@@ -1455,7 +1656,7 @@
1647
1648 edge = Edge(obj_dict=obj)
1649
1650- if self.obj_dict.get('simplify', False) and elm in edges_done:
1651+ if self.obj_dict.get('simplify', False) and edge in edges_done:
1652 continue
1653
1654 graph.append( edge.to_string() + '\n' )
1655@@ -1496,8 +1697,10 @@
1656
1657 or using the instance's attributes:
1658
1659- Subgraph.[attribute name], i.e.
1660- subgraph_instance.label, subgraph_instance.fontname
1661+ Subgraph.obj_dict['attributes'][attribute name], i.e.
1662+
1663+ subgraph_instance.obj_dict['attributes']['label']
1664+ subgraph_instance.obj_dict['attributes']['fontname']
1665 """
1666
1667
1668@@ -1541,8 +1744,10 @@
1669
1670 or using the instance's attributes:
1671
1672- Cluster.[attribute name], i.e.
1673- cluster_instance.color, cluster_instance.fontname
1674+ Cluster.obj_dict['attributes'][attribute name], i.e.
1675+
1676+ cluster_instance.obj_dict['attributes']['label']
1677+ cluster_instance.obj_dict['attributes']['fontname']
1678 """
1679
1680
1681@@ -1557,6 +1762,8 @@
1682 self.obj_dict['type'] = 'subgraph'
1683 self.obj_dict['name'] = 'cluster_'+graph_name
1684
1685+ self.create_attribute_methods(CLUSTER_ATTRIBUTES)
1686+
1687
1688
1689
1690@@ -1603,23 +1810,19 @@
1691
1692 f = self.__dict__['write_'+frmt]
1693 f.__doc__ = '''Refer to the docstring accompanying the 'write' method for more information.'''
1694-
1695+
1696+
1697
1698-
1699 def __getstate__(self):
1700-
1701- dict = copy.copy(self.__dict__)
1702- for attr in self.attributes:
1703- del dict['set_'+attr]
1704- del dict['get_'+attr]
1705-
1706- for k in [ x for x in dict.keys() if
1707- x.startswith('write_') or x.startswith('create_') ]:
1708-
1709- del dict[k]
1710-
1711+
1712+ dict = copy.copy(self.obj_dict)
1713+
1714 return dict
1715
1716+ def __setstate__(self, state):
1717+
1718+ self.obj_dict = state
1719+
1720
1721 def set_shape_files(self, file_paths):
1722 """Add the paths of the required image files.
1723@@ -1691,7 +1894,19 @@
1724
1725 dot_fd = file(path, "w+b")
1726 if format == 'raw':
1727- dot_fd.write(self.to_string())
1728+ data = self.to_string()
1729+ if isinstance(data, basestring):
1730+ if not isinstance(data, unicode):
1731+ try:
1732+ data = unicode(data, 'utf-8')
1733+ except:
1734+ pass
1735+
1736+ try:
1737+ data = data.encode('utf-8')
1738+ except:
1739+ pass
1740+ dot_fd.write(data)
1741 else:
1742 dot_fd.write(self.create(prog, format))
1743 dot_fd.close()
1744@@ -1715,11 +1930,22 @@
1745
1746 which are automatically defined for all the supported formats.
1747 [create_ps(), create_gif(), create_dia(), ...]
1748+
1749+ If 'prog' is a list instead of a string the fist item is expected
1750+ to be the program name, followed by any optional command-line
1751+ arguments for it:
1752+
1753+ [ 'twopi', '-Tdot', '-s10' ]
1754 """
1755
1756 if prog is None:
1757 prog = self.prog
1758
1759+ if isinstance(prog, (list, tuple)):
1760+ prog, args = prog[0], prog[1:]
1761+ else:
1762+ args = []
1763+
1764 if self.progs is None:
1765 self.progs = find_graphviz()
1766 if self.progs is None:
1767@@ -1755,9 +1981,11 @@
1768 f = file( os.path.join( tmp_dir, os.path.basename(img) ), 'wb' )
1769 f.write(f_data)
1770 f.close()
1771-
1772+
1773+ cmdline = [self.progs[prog], '-T'+format, tmp_name] + args
1774+
1775 p = subprocess.Popen(
1776- (self.progs[prog], '-T'+format, tmp_name),
1777+ cmdline,
1778 cwd=tmp_dir,
1779 stderr=subprocess.PIPE, stdout=subprocess.PIPE)
1780
1781@@ -1772,8 +2000,7 @@
1782 stdout_output.append(data)
1783 stdout.close()
1784
1785- if stdout_output:
1786- stdout_output = ''.join(stdout_output)
1787+ stdout_output = ''.join(stdout_output)
1788
1789 if not stderr.closed:
1790 stderr_output = list()
1791
1792=== removed file 'setup.cfg'
1793--- setup.cfg 2008-05-26 15:35:04 +0000
1794+++ setup.cfg 1970-01-01 00:00:00 +0000
1795@@ -1,5 +0,0 @@
1796-[egg_info]
1797-tag_build =
1798-tag_date = 0
1799-tag_svn_revision = 0
1800-
1801
1802=== modified file 'setup.py'
1803--- setup.py 2008-05-26 15:35:04 +0000
1804+++ setup.py 2014-02-15 16:49:34 +0000
1805@@ -1,11 +1,15 @@
1806 #!/usr/bin/env python
1807
1808 try:
1809+ from distutils.core import setup
1810+except ImportError, excp:
1811 from setuptools import setup
1812-except ImportError, excp:
1813- from distutils.core import setup
1814
1815 import pydot
1816+import os
1817+
1818+os.environ['COPY_EXTENDED_ATTRIBUTES_DISABLE'] = 'true'
1819+os.environ['COPYFILE_DISABLE'] = 'true'
1820
1821 setup( name = 'pydot',
1822 version = pydot.__version__,
1823@@ -28,4 +32,4 @@
1824 long_description = "\n".join(pydot.__doc__.split('\n')),
1825 py_modules = ['pydot', 'dot_parser'],
1826 install_requires = ['pyparsing', 'setuptools'],
1827- data_files = [('.', ['ChangeLog', 'LICENSE', 'README'])] )
1828+ data_files = [('.', ['LICENSE', 'README'])] )

Subscribers

People subscribed via source and target branches