Skip to content

Commit 9e2b378

Browse files
committed
correct labels for dicts
Fixes #49
1 parent 90be2be commit 9e2b378

2 files changed

Lines changed: 82 additions & 26 deletions

File tree

objgraph.py

Lines changed: 47 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,12 @@
7272
# Python 3.x compatibility
7373
iteritems = dict.items
7474

75+
try:
76+
zip_longest = itertools.izip_longest
77+
except AttributeError: # pragma: PY3
78+
# Python 3.x compatibility
79+
zip_longest = itertools.zip_longest
80+
7581
IS_INTERACTIVE = False
7682
try: # pragma: nocover
7783
import graphviz
@@ -1000,7 +1006,10 @@ def _show_graph(objs, edge_func, swap_source_target,
10001006
continue
10011007
if cull_func is not None and cull_func(target):
10021008
continue
1003-
neighbours = edge_func(target)
1009+
edges = edge_func(target)
1010+
counts = collections.Counter(id(v) for v in edges)
1011+
neighbours = list({id(v): v for v in edges}.values())
1012+
del edges
10041013
ignore.add(id(neighbours))
10051014
n = 0
10061015
skipped = 0
@@ -1016,9 +1025,13 @@ def _show_graph(objs, edge_func, swap_source_target,
10161025
srcnode, tgtnode = target, source
10171026
else:
10181027
srcnode, tgtnode = source, target
1019-
elabel = _edge_label(srcnode, tgtnode, shortnames)
1020-
f.write(' %s -> %s%s;\n' % (_obj_node_id(srcnode),
1021-
_obj_node_id(tgtnode), elabel))
1028+
for elabel, _ in zip_longest(
1029+
_edge_labels(srcnode, tgtnode, shortnames),
1030+
range(counts[id(source)]),
1031+
fillvalue='',
1032+
):
1033+
f.write(' %s -> %s%s;\n' % (_obj_node_id(srcnode),
1034+
_obj_node_id(tgtnode), elabel))
10221035
if id(source) not in depth:
10231036
depth[id(source)] = tdepth + 1
10241037
queue.append(source)
@@ -1208,43 +1221,51 @@ def _gradient(start_color, end_color, depth, max_depth):
12081221
return h, s, v
12091222

12101223

1211-
def _edge_label(source, target, shortnames=True):
1224+
def _edge_labels(source, target, shortnames=True):
12121225
if (_isinstance(target, dict)
12131226
and target is getattr(source, '__dict__', None)):
1214-
return ' [label="__dict__",weight=10]'
1227+
return [' [label="__dict__",weight=10]']
12151228
if _isinstance(source, types.FrameType):
12161229
if target is source.f_locals:
1217-
return ' [label="f_locals",weight=10]'
1230+
return [' [label="f_locals",weight=10]']
12181231
if target is source.f_globals:
1219-
return ' [label="f_globals",weight=10]'
1232+
return [' [label="f_globals",weight=10]']
12201233
if _isinstance(source, types.MethodType):
12211234
try:
12221235
if target is source.__self__:
1223-
return ' [label="__self__",weight=10]'
1236+
return [' [label="__self__",weight=10]']
12241237
if target is source.__func__:
1225-
return ' [label="__func__",weight=10]'
1238+
return [' [label="__func__",weight=10]']
12261239
except AttributeError: # pragma: nocover
12271240
# Python < 2.6 compatibility
12281241
if target is source.im_self:
1229-
return ' [label="im_self",weight=10]'
1242+
return [' [label="im_self",weight=10]']
12301243
if target is source.im_func:
1231-
return ' [label="im_func",weight=10]'
1244+
return [' [label="im_func",weight=10]']
12321245
if _isinstance(source, types.FunctionType):
1233-
for k in dir(source):
1234-
if target is getattr(source, k):
1235-
return ' [label="%s",weight=10]' % _quote(k)
1246+
return [
1247+
' [label="%s",weight=10]' % _quote(k)
1248+
for k in dir(source)
1249+
if target is getattr(source, k)
1250+
]
12361251
if _isinstance(source, dict):
1237-
for k, v in iteritems(source):
1238-
if v is target:
1239-
if _isinstance(k, basestring) and _is_identifier(k):
1240-
return ' [label="%s",weight=2]' % _quote(k)
1241-
else:
1242-
if shortnames:
1243-
tn = _short_typename(k)
1244-
else:
1245-
tn = _long_typename(k)
1246-
return ' [label="%s"]' % _quote(tn + "\n" + _safe_repr(k))
1247-
return ''
1252+
tn = _short_typename if shortnames else _long_typename
1253+
return [
1254+
(
1255+
' [label="%s",weight=2]' % _quote(k)
1256+
if _isinstance(k, basestring) and _is_identifier(k)
1257+
else (
1258+
' [label="%s"]' % _quote(tn(k) + "\n" + _safe_repr(k))
1259+
)
1260+
)
1261+
for k, v in iteritems(source)
1262+
if v is target
1263+
]
1264+
return []
1265+
1266+
1267+
def _edge_label(*args, **kwargs):
1268+
return next(iter(_edge_labels(*args, **kwargs)), '')
12481269

12491270

12501271
_is_identifier = re.compile('[a-zA-Z_][a-zA-Z_0-9]*$').match

tests.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
#!/usr/bin/python
2+
import collections
23
import doctest
34
import gc
45
import glob
@@ -247,6 +248,40 @@ def test_cull_func(self):
247248
label_a=label_a,
248249
label_b=label_b))
249250

251+
@skipIf(
252+
sys.version_info < (3, 6),
253+
"Python < 3.6 dicts have random iteration order",
254+
)
255+
def test_dict(self):
256+
d = dict.fromkeys("abcdefg")
257+
output = StringIO()
258+
objgraph.show_refs(d, output=output)
259+
self.assertEqual(
260+
output.getvalue(),
261+
textwrap.dedent(
262+
"""\
263+
digraph ObjectGraph {{
264+
node[shape=box, style=filled, fillcolor=white];
265+
{d_id}[fontcolor=red];
266+
{d_id}[label="dict\\n7 items"];
267+
{d_id}[fillcolor="0,0,1"];
268+
{d_id} -> {none_id} [label="a",weight=2];
269+
{d_id} -> {none_id} [label="b",weight=2];
270+
{d_id} -> {none_id} [label="c",weight=2];
271+
{d_id} -> {none_id} [label="d",weight=2];
272+
{d_id} -> {none_id} [label="e",weight=2];
273+
{d_id} -> {none_id} [label="f",weight=2];
274+
{d_id} -> {none_id} [label="g",weight=2];
275+
{none_id}[label="NoneType\\nNone"];
276+
{none_id}[fillcolor="0,0,0.766667"];
277+
}}
278+
"""
279+
).format(
280+
d_id=objgraph._obj_node_id(d),
281+
none_id=objgraph._obj_node_id(None),
282+
),
283+
)
284+
250285
@mock.patch('objgraph.IS_INTERACTIVE', True)
251286
@mock.patch('objgraph.graphviz', create=True)
252287
def test_ipython(self, mock_graphviz):

0 commit comments

Comments
 (0)