Merge lp:~stefanor/objgraph/pure3k into lp:objgraph

Proposed by Marius Gedminas
Status: Merged
Merge reported by: Marius Gedminas
Merged at revision: not available
Proposed branch: lp:~stefanor/objgraph/pure3k
Merge into: lp:objgraph
Diff against target: 441 lines (+135/-58)
8 files modified
Makefile (+10/-4)
generator-sample.txt (+2/-2)
index.txt (+14/-1)
objgraph.py (+64/-36)
references.txt (+2/-2)
setup.py (+18/-5)
tests.py (+24/-7)
uncollectable.txt (+1/-1)
To merge this branch: bzr merge lp:~stefanor/objgraph/pure3k
Reviewer Review Type Date Requested Status
Marius Gedminas Approve
Review via email: mp+43725@code.launchpad.net

Description of the change

Python 3.x compatibility.

I'm creating this merge proposal purely so that I could comment on changes made in this branch.

To post a comment you must log in.
Revision history for this message
Marius Gedminas (mgedmin) wrote :

Overall impression: it's good, except I'd like Python 2.4/5 compatibility back pretty please :)

Revisions 83 and 85 could be merged today.

Is rev. 84 necessary? AFAIU you decided not to use 2to3 after all.

Rev 86: for Python 2.4 compatibility you could stuff next() into the global doctest namespace from
the tests.setUp(test):

    try:
        next
    except NameError:
        # Python 2.4/2.5 compatibility
        test.globs['next'] = lambda it: it.__next__()

so it doesn't clutter the doctest itself.

I'd rephrase rev 87 as

    try:
        basestring
    except NameError:
        # Python 3.x compatibility
        basestring = str

purely because testing for features seems better to me than testing versions, but this is a minor thing.

Rev 89: hm, my gut feeling was that those can be large dicts, and so I avoided creating lists with all the items. But perhaps I'm wrong. How many different types can there be? I should go and check a couple of large applications.

That was for stats.items(); I'm less sanguine about source.items(). I'm especially worried that this will create new temporary references during graph traversal, which we may decide to follow... But I think you'd have noticed something like that.

Rev 90: I'm thinking about also changing

  '%s ...' % (foo.ljust(width), ...)

with

  '%-*s ...' % (width, foo, ...).

And I'm pretty sure

        print(name.ljust(width) + ' ' + count)

is a bug -- count should be an int there. I should add a unit test for that function! A simple smoke test would do, tagged with # doctest: +RANDOM_OUTPUT, like the existing show_growth() test in index.txt.

Rev 91: just how hard would it be to add support for Py2.4/2.5? Is it just next()/__func__/__self__, or do more things break?

Incidentally, I've no conveniently prepackaged 3.2 for running the tests ... all the others are present. I'd like to change that makefile rule to skip 3.2 tests if python3.2 is not in $PATH.

Rev 92: please also add

  Programming Language :: Python :: 2

otherwise it looks like objgraph only supports Python 3. While at it, why not add

  Programming Language :: Python :: 2.4
  Programming Language :: Python :: 2.5
  Programming Language :: Python :: 2.6
  Programming Language :: Python :: 2.7
  Programming Language :: Python :: 3.1

So, to summarize:

  - Revert rev 84
  - Fix bug in show_most_common_types
  - Add Py 2.4/2.5 compat, reinstate their tests in make test-all-pythons
  - Add more trove classifiers

Optionally:

  - add quick smoke test for show_most_common_types
  - rephrase basestring fallback
  - rephrase use of str.ljust()
  - skip python3.2 tests if it's not available
  - investigate possible downsides of using items() instead of iteritems() on py2.x

review: Needs Fixing
Revision history for this message
Marius Gedminas (mgedmin) wrote :

I completely forgot one important thing:

  - tests need to be fixed so they actually pass!

Incidentally, do you know any good image comparison tool, usable from the command line with

  bzr diff --using=diffprogram

? I'm using a shell script wrapper around imagemagick's compare, which is nice, but only works when the images are exactly the same size.

Revision history for this message
Marius Gedminas (mgedmin) wrote :

references.txt needs a fix: instead of using [range(7)] it should use [list(range(7))]. Otherwise Python 3.x uses a single range object instead of multiple int objects, and the test cannot test the output suppression bits.

Revision history for this message
Marius Gedminas (mgedmin) wrote :

I'm seeing spurious 2-tuples in some of the graphs generated by 'make images PYTHON=python3.1', e.g. refcounts.png. After adding some instrumentation, I can see that it contains (<code object>, <f_locals-or-f_globals-of-some-code-frame>). It has a refcount of 1 (assuming I can trust the refcount display code in objgraph, which I don't trust) but gc.get_referrers() returns []. Some kind of interpreter internals, held in a C level variable?

I'm not sure what to do about them -- try to suppress or make the tests accept them?

I'd rather display an accurate picture of the situation, so I'm trying to suppress only those objects that are introduced by objgraph itself. So, I think the right thing here is to change the doctest output checker (tests.MyChecker) and have it accept an arbitrary number of nodes somehow... regexp search and replace of "(\d+ nodes)" with "(X nodes)" in both 'want' and 'got'?

Revision history for this message
Marius Gedminas (mgedmin) wrote :

> Incidentally, do you know any good image comparison tool, usable from the
> command line with
>
> bzr diff --using=diffprogram
>
> ? I'm using a shell script wrapper around imagemagick's compare, which is
> nice, but only works when the images are exactly the same size.

And now I'm using this:

  bzr diff --using='imgdiff --lr --viewer eog' *.png

where imgdiff is http://pastie.org/1378134

Revision history for this message
Marius Gedminas (mgedmin) wrote :

> So, I
> think the right thing here is to change the doctest output checker
> (tests.MyChecker) and have it accept an arbitrary number of nodes somehow...
> regexp search and replace of "(\d+ nodes)" with "(X nodes)" in both 'want' and
> 'got'?

This should be done for 'make test', but 'make images' should not ignore node numbers. I want the documentation text to match the images. This is easily achieved by having two different checker classes, one of them used in tests.py, and the other one used in setup.py.

Revision history for this message
Stefano Rivera (stefanor) wrote :

> I'd like Python 2.4/5 compatibility back pretty please :)

Righto, /me digs out an etch VM that has python2.4.

> Is rev. 84 necessary?

No, it isn't.

> tests need to be fixed so they actually pass!

Yeah that's one of the reasons I posted this branch now, so you could have a look and give some initial feedback, before I dived into seeing exactly what was going on in the tests.

> Incidentally, do you know any good image comparison tool, usable from the command line

I'm afraid not, looks like a niche that needs filling :P Oh, and thanks for your script, that'll be handy.

> I'm seeing spurious 2-tuples in some of the graphs

I'll have a look, but I'm doubting I can see much more than you did...

> I want the documentation text to match the images.

Agreed.

Revision history for this message
Stefano Rivera (stefanor) wrote :

> I'd like Python 2.4/5 compatibility back pretty please :)

Restored in r97

> I'd rephrase rev 87 as...

OK, r98

> I avoided creating lists with all the items.

Yes, I can see that being an issue. One of the things I was concerned about. I've added a wrapper in r99. (This is where supporting Python 2 and 3 without 2to3 starts getting a little ugly, but I think it's worth it)

> '%-*s ...' % (width, foo, ...).

Nice, I didn't know about that trick. r100

> is a bug -- count should be an int there. I should add a unit test for that function!

Indeed, it'll catch people working on your code and only testing with unit tests :P Added a test in r102

> Incidentally, I've no conveniently prepackaged 3.2 for running the tests

Available in Debian experimental / Ubuntu natty. I'll go one step further and check for the existence of all pythons before running them. (I have no 2.4 on my machine) r101

> references.txt needs a fix: instead of using [range(7)] it should use [list(range(7))].

r103

Now to get those tests to pass...

lp:~stefanor/objgraph/pure3k updated
93. By Stefano Rivera

Revert r83 (Help 2to3 to do the right thing with functions called filter) no longer necessary

94. By Stefano Rivera

List minor Python versions supported in classifiers

95. By Stefano Rivera

Provide next() compatibility function for tests

96. By Stefano Rivera

Support both im_{func,self} / __{func,self}__

97. By Stefano Rivera

Python 2.4, 2.5 support is restored

98. By Stefano Rivera

Style: Use try...except instead of version_info

99. By Stefano Rivera

Use a wrapper function for dict iteritems / items change

100. By Stefano Rivera

Use %-*s instead of ljust()

101. By Stefano Rivera

Test for the presence of all python versions before testing with them

102. By Stefano Rivera

Add test for show_most_common_types()

103. By Stefano Rivera

Wrap range() in list() for py3k

104. By Stefano Rivera

Specify close_fds, to as default changes in Python 3.2, with accompanying warning

105. By Stefano Rivera

Deal with varying node numbers with a doctest option and an additional custom checker

Revision history for this message
Stefano Rivera (stefanor) wrote :

> Now to get those tests to pass...

I'm happy. I've added 3.0 to the list of Pythons to test with, but not to the trove classifiers, as I don't have a 3.0 to test with.

Revision history for this message
Marius Gedminas (mgedmin) wrote :

Excellent!

For the record, all tests pass on Python 2.4 on my machine.

I'm wondering about two little things:

  - perhaps it would be better to use unbound dict methods (dict.items/dict.iteritems) instead of a lambda?

  - for consistency, maybe it would be better to do

      try:
          dict.iteritems
      except AttributeError:
          # Python 3.x
          iteritems = dict.items
      else:
          # Python 2.x
          iteritems = dict.iteritems

    instead of the only remaining sys.version check?

  - perhaps # doctest: +NODES_VARY in almost every example clutter the documentation too much, and it would be simpler/better to just unconditionally turn that bit of normalization on?

Three little things, I'm worrying about three little things.

I'll come in again. :-)

Nevertheless, I'm prepared to merge the branch as is. Tomorrow, since it's 4 AM and I'm prone to mistakes when sleepy.

review: Approve
Revision history for this message
Stefano Rivera (stefanor) wrote :

> perhaps it would be better to use unbound dict methods

Yeah, wasn't thinking about those

> NODES_VARY in almost every example

There were only 4 of them (example images containing stack frames), so I thought it was worth the check, but happy to drop it.

> Tomorrow, since it's 4 AM and I'm prone to mistakes when sleepy.

Good call, I make terrible mistakes when I approve merges late at night

Revision history for this message
Marius Gedminas (mgedmin) wrote :

Of course it had to be 4:30 AM the next day before I found the time to do a merge :)

Revision history for this message
Marius Gedminas (mgedmin) wrote :

And now I can't push to launchpad because I stupidly ran 'bzr upgrade' on my branch, so I have to go hunt the right web page and upgrade launchpad's copy, or slowly do it over 'bzr upgrade lp:objgraph'.

I think I'll try the slow way. At least it's simple.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'Makefile'
2--- Makefile 2010-12-08 22:06:31 +0000
3+++ Makefile 2010-12-16 13:53:08 +0000
4@@ -32,10 +32,16 @@
5
6 .PHONY: test-all-pythons
7 test-all-pythons:
8- make test PYTHON=python2.4
9- make test PYTHON=python2.5
10- make test PYTHON=python2.6
11- make test PYTHON=python2.7
12+ set -e; \
13+ for ver in 2.4 2.5 2.6 2.7 3.0 3.1 3.2; do \
14+ if which python$$ver > /dev/null; then \
15+ $(MAKE) test PYTHON=python$$ver; \
16+ else \
17+ echo "=================================="; \
18+ echo "Skipping python$$ver, not available."; \
19+ echo "=================================="; \
20+ fi; \
21+ done
22
23 .PHONY: preview-pypi-description
24 preview-pypi-description:
25
26=== modified file 'generator-sample.txt'
27--- generator-sample.txt 2010-12-05 15:46:33 +0000
28+++ generator-sample.txt 2010-12-16 13:53:08 +0000
29@@ -17,7 +17,7 @@
30 and we make it active
31
32 >>> it = count_to_three()
33- >>> it.next()
34+ >>> next(it)
35 1
36
37 Now we can see that our Canary object is alive in memory
38@@ -30,7 +30,7 @@
39
40 >>> objgraph.show_backrefs(objgraph.by_type('Canary'),
41 ... max_depth=7,
42- ... filename='canary.png')
43+ ... filename='canary.png') # doctest: +NODES_VARY
44 Graph written to ....dot (15 nodes)
45 Image generated as canary.png
46
47
48=== modified file 'index.txt'
49--- index.txt 2010-12-08 22:53:30 +0000
50+++ index.txt 2010-12-16 13:53:08 +0000
51@@ -28,7 +28,7 @@
52
53 Now try
54
55- >>> objgraph.show_backrefs([x], filename='sample-backref-graph.png')
56+ >>> objgraph.show_backrefs([x], filename='sample-backref-graph.png') # doctest: +NODES_VARY
57 Graph written to ....dot (8 nodes)
58 Image generated as sample-backref-graph.png
59
60@@ -39,6 +39,19 @@
61 :scale: 50%
62
63
64+Memory overview
65+---------------
66+
67+To get a quick overview of the objects in memory
68+
69+ >>> objgraph.show_most_common_types() # doctest: +RANDOM_OUTPUT
70+ tuple 5351
71+ function 1369
72+ wrapper_descriptor 967
73+ dict 786
74+ ...
75+
76+
77 Memory leak example
78 -------------------
79
80
81=== modified file 'objgraph.py'
82--- objgraph.py 2010-12-08 23:11:28 +0000
83+++ objgraph.py 2010-12-16 13:53:08 +0000
84@@ -52,6 +52,19 @@
85 import itertools
86
87
88+try:
89+ basestring
90+except NameError:
91+ # Python 3.x compatibility
92+ basestring = str
93+
94+# Dictionary iteration behavior change in Python 3:
95+if sys.version_info < (3,):
96+ iteritems = lambda x: x.iteritems()
97+else:
98+ iteritems = lambda x: x.items()
99+
100+
101 def count(typename):
102 """Count objects tracked by the garbage collector with a given class name.
103
104@@ -132,7 +145,7 @@
105 stats = most_common_types(limit)
106 width = max(len(name) for name, count in stats)
107 for name, count in stats:
108- print name.ljust(width), count
109+ print('%-*s %i' % (width, name, count))
110
111
112 def show_growth(limit=10, peak_stats={}):
113@@ -160,7 +173,7 @@
114 gc.collect()
115 stats = typestats()
116 deltas = {}
117- for name, count in stats.iteritems():
118+ for name, count in iteritems(stats):
119 old_count = peak_stats.get(name, 0)
120 if count > old_count:
121 deltas[name] = count - old_count
122@@ -172,7 +185,7 @@
123 if deltas:
124 width = max(len(name) for name, count in deltas)
125 for name, delta in deltas:
126- print name.ljust(width), "%9d %+9d" % (stats[name], delta)
127+ print('%-*s%9d %+9d' % (width, name, stats[name], delta))
128
129
130 def by_type(typename):
131@@ -399,13 +412,13 @@
132 if not isinstance(objs, (list, tuple)):
133 objs = [objs]
134 if filename and filename.endswith('.dot'):
135- f = file(filename, 'w')
136+ f = open(filename, 'w')
137 dot_filename = filename
138 else:
139 fd, dot_filename = tempfile.mkstemp('.dot', text=True)
140 f = os.fdopen(fd, "w")
141- print >> f, 'digraph ObjectGraph {'
142- print >> f, ' node[shape=box, style=filled, fillcolor=white];'
143+ f.write('digraph ObjectGraph {\n'
144+ ' node[shape=box, style=filled, fillcolor=white];\n')
145 queue = []
146 depth = {}
147 ignore = set(extra_ignore)
148@@ -417,7 +430,7 @@
149 ignore.add(id(sys._getframe())) # this function
150 ignore.add(id(sys._getframe(1))) # show_refs/show_backrefs, most likely
151 for obj in objs:
152- print >> f, ' %s[fontcolor=red];' % (obj_node_id(obj))
153+ f.write(' %s[fontcolor=red];\n' % (obj_node_id(obj)))
154 depth[id(obj)] = 0
155 queue.append(obj)
156 del obj
157@@ -427,7 +440,7 @@
158 nodes += 1
159 target = queue.pop(0)
160 tdepth = depth[id(target)]
161- print >> f, ' %s[label="%s"];' % (obj_node_id(target), obj_label(target, extra_info, refcounts))
162+ f.write(' %s[label="%s"];\n' % (obj_node_id(target), obj_label(target, extra_info, refcounts)))
163 h, s, v = gradient((0, 0, 1), (0, 0, .3), tdepth, max_depth)
164 if inspect.ismodule(target):
165 h = .3
166@@ -436,12 +449,12 @@
167 h = .6
168 s = .6
169 v = 0.5 + v * 0.5
170- print >> f, ' %s[fillcolor="%g,%g,%g"];' % (obj_node_id(target), h, s, v)
171+ f.write(' %s[fillcolor="%g,%g,%g"];\n' % (obj_node_id(target), h, s, v))
172 if v < 0.5:
173- print >> f, ' %s[fontcolor=white];' % (obj_node_id(target))
174+ f.write(' %s[fontcolor=white];\n' % (obj_node_id(target)))
175 if hasattr(getattr(target, '__class__', None), '__del__'):
176- print >> f, " %s->%s_has_a_del[color=red,style=dotted,len=0.25,weight=10];" % (obj_node_id(target), obj_node_id(target))
177- print >> f, ' %s_has_a_del[label="__del__",shape=doublecircle,height=0.25,color=red,fillcolor="0,.5,1",fontsize=6];' % (obj_node_id(target))
178+ f.write(" %s->%s_has_a_del[color=red,style=dotted,len=0.25,weight=10];\n" % (obj_node_id(target), obj_node_id(target)))
179+ f.write(' %s_has_a_del[label="__del__",shape=doublecircle,height=0.25,color=red,fillcolor="0,.5,1",fontsize=6];\n' % (obj_node_id(target)))
180 if tdepth >= max_depth:
181 continue
182 if inspect.ismodule(target) and not swap_source_target:
183@@ -467,7 +480,7 @@
184 else:
185 srcnode, tgtnode = source, target
186 elabel = edge_label(srcnode, tgtnode)
187- print >> f, ' %s -> %s%s;' % (obj_node_id(srcnode), obj_node_id(tgtnode), elabel)
188+ f.write(' %s -> %s%s;\n' % (obj_node_id(srcnode), obj_node_id(tgtnode), elabel))
189 if id(source) not in depth:
190 depth[id(source)] = tdepth + 1
191 queue.append(source)
192@@ -482,36 +495,36 @@
193 else:
194 label = "%d more backreferences" % skipped
195 edge = "too_many_%s->%s" % (obj_node_id(target), obj_node_id(target))
196- print >> f, ' %s[color=red,style=dotted,len=0.25,weight=10];' % edge
197- print >> f, ' too_many_%s[label="%s",shape=box,height=0.25,color=red,fillcolor="%g,%g,%g",fontsize=6];' % (obj_node_id(target), label, h, s, v)
198- print >> f, ' too_many_%s[fontcolor=white];' % (obj_node_id(target))
199- print >> f, "}"
200+ f.write(' %s[color=red,style=dotted,len=0.25,weight=10];\n' % edge)
201+ f.write(' too_many_%s[label="%s",shape=box,height=0.25,color=red,fillcolor="%g,%g,%g",fontsize=6];\n' % (obj_node_id(target), label, h, s, v))
202+ f.write(' too_many_%s[fontcolor=white];\n' % (obj_node_id(target)))
203+ f.write("}\n")
204 f.close()
205- print "Graph written to %s (%d nodes)" % (dot_filename, nodes)
206+ print("Graph written to %s (%d nodes)" % (dot_filename, nodes))
207 if not filename and program_in_path('xdot'):
208- print "Spawning graph viewer (xdot)"
209- subprocess.Popen(['xdot', dot_filename])
210+ print("Spawning graph viewer (xdot)")
211+ subprocess.Popen(['xdot', dot_filename], close_fds=True)
212 elif program_in_path('dot'):
213 if not filename:
214- print "Graph viewer (xdot) not found, generating a png instead"
215+ print("Graph viewer (xdot) not found, generating a png instead")
216 if filename and filename.endswith('.png'):
217- f = file(filename, 'wb')
218+ f = open(filename, 'wb')
219 png_filename = filename
220 else:
221 if filename:
222- print "Unrecognized file type (%s)" % filename
223+ print("Unrecognized file type (%s)" % filename)
224 fd, png_filename = tempfile.mkstemp('.png', text=False)
225 f = os.fdopen(fd, "wb")
226 dot = subprocess.Popen(['dot', '-Tpng', dot_filename],
227- stdout=f)
228+ stdout=f, close_fds=False)
229 dot.wait()
230 f.close()
231- print "Image generated as %s" % png_filename
232+ print("Image generated as %s" % png_filename)
233 else:
234 if filename:
235- print "Graph viewer (xdot) and image renderer (dot) not found, not doing anything else"
236+ print("Graph viewer (xdot) and image renderer (dot) not found, not doing anything else")
237 else:
238- print "Unrecognized file type (%s), not doing anything else" % filename
239+ print("Unrecognized file type (%s), not doing anything else" % filename)
240
241
242 def obj_node_id(obj):
243@@ -551,10 +564,18 @@
244 types.BuiltinFunctionType)):
245 return obj.__name__
246 if isinstance(obj, types.MethodType):
247- if obj.im_self is not None:
248- return obj.im_func.__name__ + ' (bound)'
249- else:
250- return obj.im_func.__name__
251+ try:
252+ if obj.__self__ is not None:
253+ return obj.__func__.__name__ + ' (bound)'
254+ else:
255+ return obj.__func__.__name__
256+ except AttributeError:
257+ # Python < 2.6 compatibility
258+ if obj.im_self is not None:
259+ return obj.im_func.__name__ + ' (bound)'
260+ else:
261+ return obj.im_func.__name__
262+
263 if isinstance(obj, types.FrameType):
264 return '%s:%s' % (obj.f_code.co_filename, obj.f_lineno)
265 if isinstance(obj, (tuple, list, dict, set)):
266@@ -586,12 +607,19 @@
267 if target is source.f_globals:
268 return ' [label="f_globals",weight=10]'
269 if isinstance(source, types.MethodType):
270- if target is source.im_self:
271- return ' [label="im_self",weight=10]'
272- if target is source.im_func:
273- return ' [label="im_func",weight=10]'
274+ try:
275+ if target is source.__self__:
276+ return ' [label="__self__",weight=10]'
277+ if target is source.__func__:
278+ return ' [label="__func__",weight=10]'
279+ except AttributeError:
280+ # Python < 2.6 compatibility
281+ if target is source.im_self:
282+ return ' [label="im_self",weight=10]'
283+ if target is source.im_func:
284+ return ' [label="im_func",weight=10]'
285 if isinstance(source, dict):
286- for k, v in source.iteritems():
287+ for k, v in iteritems(source):
288 if v is target:
289 if isinstance(k, basestring) and k:
290 return ' [label="%s",weight=2]' % quote(k)
291
292=== modified file 'references.txt'
293--- references.txt 2010-12-05 14:53:52 +0000
294+++ references.txt 2010-12-16 13:53:08 +0000
295@@ -4,7 +4,7 @@
296 Objects that have too many references are truncated
297
298 >>> import objgraph
299- >>> objgraph.show_refs([range(7)], too_many=5, filename='too-many.png')
300+ >>> objgraph.show_refs([list(range(7))], too_many=5, filename='too-many.png')
301 Graph written to ....dot (6 nodes)
302 Image generated as too-many.png
303
304@@ -32,7 +32,7 @@
305 >>> import sys
306 >>> one_reference = object()
307 >>> objgraph.show_backrefs([one_reference], refcounts=True,
308- ... filename='refcounts.png')
309+ ... filename='refcounts.png') # doctest: +NODES_VARY
310 Graph written to ....dot (5 nodes)
311 Image generated as refcounts.png
312
313
314=== modified file 'setup.py'
315--- setup.py 2010-12-08 22:53:30 +0000
316+++ setup.py 2010-12-16 13:53:08 +0000
317@@ -1,5 +1,5 @@
318 #!/usr/bin/python
319-import os, sys, unittest, doctest
320+import os, re, sys, unittest, doctest
321
322 try:
323 from setuptools import setup
324@@ -27,9 +27,11 @@
325
326
327 def get_version():
328- d = {}
329- exec read('objgraph.py') in d
330- return d['__version__']
331+ r = re.compile('^__version__ = "(.+)"$')
332+ for line in read('objgraph.py').splitlines():
333+ m = r.match(line)
334+ if m:
335+ return m.group(1)
336
337
338 def get_description():
339@@ -43,7 +45,7 @@
340 if not doctests:
341 doctests = tests.find_doctests()
342 suite = doctest.DocFileSuite(optionflags=doctest.ELLIPSIS,
343- checker=tests.MyChecker(),
344+ checker=tests.RandomOutputChecker(),
345 *doctests)
346 result = unittest.TextTestRunner().run(suite)
347 if not result.wasSuccessful():
348@@ -63,4 +65,15 @@
349 license='MIT',
350 description='Draws Python object reference graphs with graphviz',
351 long_description=get_description(),
352+ classifiers=[
353+ 'Programming Language :: Python',
354+ 'Programming Language :: Python :: 2',
355+ 'Programming Language :: Python :: 2.4',
356+ 'Programming Language :: Python :: 2.5',
357+ 'Programming Language :: Python :: 2.6',
358+ 'Programming Language :: Python :: 2.7',
359+ 'Programming Language :: Python :: 3',
360+ 'Programming Language :: Python :: 3.1',
361+ 'Programming Language :: Python :: 3.2',
362+ ],
363 py_modules=['objgraph'])
364
365=== modified file 'tests.py'
366--- tests.py 2010-12-08 22:53:30 +0000
367+++ tests.py 2010-12-16 13:53:08 +0000
368@@ -1,16 +1,18 @@
369 #!/usr/bin/python
370-import unittest
371 import doctest
372-import tempfile
373+import glob
374 import os
375+import re
376 import shutil
377-import glob
378-
379-
380+import tempfile
381+import unittest
382+
383+
384+NODES_VARY = doctest.register_optionflag('NODES_VARY')
385 RANDOM_OUTPUT = doctest.register_optionflag('RANDOM_OUTPUT')
386
387
388-class MyChecker(doctest.OutputChecker):
389+class RandomOutputChecker(doctest.OutputChecker):
390
391 def check_output(self, want, got, optionflags):
392 if optionflags & RANDOM_OUTPUT:
393@@ -18,12 +20,27 @@
394 return doctest.OutputChecker.check_output(self, want, got, optionflags)
395
396
397+class IgnoreNodeCountChecker(RandomOutputChecker):
398+ _r = re.compile('\(\d+ nodes\)$', re.MULTILINE)
399+
400+ def check_output(self, want, got, optionflags):
401+ if optionflags & NODES_VARY:
402+ want = self._r.sub('(X nodes)', want)
403+ got = self._r.sub('(X nodes)', got)
404+ return RandomOutputChecker.check_output(self, want, got, optionflags)
405+
406+
407 def setUp(test):
408 test.tmpdir = tempfile.mkdtemp(prefix='test-objgraph-')
409 test.prevdir = os.getcwd()
410 test.prevtempdir = tempfile.tempdir
411 tempfile.tempdir = test.tmpdir
412 os.chdir(test.tmpdir)
413+ try:
414+ next
415+ except NameError:
416+ # Python < 2.6 compatibility
417+ test.globs['next'] = lambda it: it.next()
418
419
420 def tearDown(test):
421@@ -41,7 +58,7 @@
422 doctests = find_doctests()
423 return doctest.DocFileSuite(setUp=setUp, tearDown=tearDown,
424 optionflags=doctest.ELLIPSIS,
425- checker=MyChecker(),
426+ checker=IgnoreNodeCountChecker(),
427 *doctests)
428
429 if __name__ == '__main__':
430
431=== modified file 'uncollectable.txt'
432--- uncollectable.txt 2010-12-08 21:41:46 +0000
433+++ uncollectable.txt 2010-12-16 13:53:08 +0000
434@@ -27,7 +27,7 @@
435 We highlight these objects by showing the existence of a ``__del__``.
436
437 >>> objgraph.show_backrefs(objgraph.by_type('Nondestructible'),
438- ... filename='finalizers.png')
439+ ... filename='finalizers.png') # doctest: +NODES_VARY
440 Graph written to ....dot (8 nodes)
441 Image generated as finalizers.png
442

Subscribers

People subscribed via source and target branches