diff --git a/objgraph.py b/objgraph.py index 1839052..6b4ebf6 100755 --- a/objgraph.py +++ b/objgraph.py @@ -1000,7 +1000,11 @@ def _show_graph(objs, edge_func, swap_source_target, continue if cull_func is not None and cull_func(target): continue - neighbours = edge_func(target) + edges = edge_func(target) + # `neighbours` is `edges` without duplicates, and `counts` is the count of duplicates + counts = collections.Counter(id(v) for v in edges) + neighbours = list({id(v): v for v in edges}.values()) + del edges ignore.add(id(neighbours)) n = 0 skipped = 0 @@ -1016,9 +1020,13 @@ def _show_graph(objs, edge_func, swap_source_target, srcnode, tgtnode = target, source else: srcnode, tgtnode = source, target - elabel = _edge_label(srcnode, tgtnode, shortnames) - f.write(' %s -> %s%s;\n' % (_obj_node_id(srcnode), - _obj_node_id(tgtnode), elabel)) + for elabel, _ in itertools.zip_longest( + _edge_labels(srcnode, tgtnode, shortnames), + range(counts[id(source)]), + fillvalue='', + ): + f.write(' %s -> %s%s;\n' % (_obj_node_id(srcnode), + _obj_node_id(tgtnode), elabel)) if id(source) not in depth: depth[id(source)] = tdepth + 1 queue.append(source) @@ -1208,43 +1216,43 @@ def _gradient(start_color, end_color, depth, max_depth): return h, s, v -def _edge_label(source, target, shortnames=True): +def _edge_labels(source, target, shortnames=True): if (_isinstance(target, dict) and target is getattr(source, '__dict__', None)): - return ' [label="__dict__",weight=10]' + yield ' [label="__dict__",weight=10]' if _isinstance(source, types.FrameType): if target is source.f_locals: - return ' [label="f_locals",weight=10]' + yield ' [label="f_locals",weight=10]' if target is source.f_globals: - return ' [label="f_globals",weight=10]' + yield ' [label="f_globals",weight=10]' if _isinstance(source, types.MethodType): try: if target is source.__self__: - return ' [label="__self__",weight=10]' + yield ' [label="__self__",weight=10]' if target is source.__func__: - return ' [label="__func__",weight=10]' + yield ' [label="__func__",weight=10]' except AttributeError: # pragma: nocover # Python < 2.6 compatibility if target is source.im_self: - return ' [label="im_self",weight=10]' + yield ' [label="im_self",weight=10]' if target is source.im_func: - return ' [label="im_func",weight=10]' + yield ' [label="im_func",weight=10]' if _isinstance(source, types.FunctionType): for k in dir(source): if target is getattr(source, k): - return ' [label="%s",weight=10]' % _quote(k) + yield ' [label="%s",weight=10]' % _quote(k) if _isinstance(source, dict): + tn = _short_typename if shortnames else _long_typename for k, v in iteritems(source): if v is target: if _isinstance(k, basestring) and _is_identifier(k): - return ' [label="%s",weight=2]' % _quote(k) + yield ' [label="%s",weight=2]' % _quote(k) else: - if shortnames: - tn = _short_typename(k) - else: - tn = _long_typename(k) - return ' [label="%s"]' % _quote(tn + "\n" + _safe_repr(k)) - return '' + yield ' [label="%s"]' % _quote(tn(k) + "\n" + _safe_repr(k)) + + +def _edge_label(*args, **kwargs): + return next(_edge_labels(*args, **kwargs), '') _is_identifier = re.compile('[a-zA-Z_][a-zA-Z_0-9]*$').match diff --git a/tests.py b/tests.py index 27a325a..e09e92a 100755 --- a/tests.py +++ b/tests.py @@ -247,6 +247,36 @@ def test_cull_func(self): label_a=label_a, label_b=label_b)) + def test_dict(self): + d = dict.fromkeys("abcdefg") + output = StringIO() + objgraph.show_refs(d, output=output) + self.assertEqual( + output.getvalue(), + textwrap.dedent( + """\ + digraph ObjectGraph {{ + node[shape=box, style=filled, fillcolor=white]; + {d_id}[fontcolor=red]; + {d_id}[label="dict\\n7 items"]; + {d_id}[fillcolor="0,0,1"]; + {d_id} -> {none_id} [label="a",weight=2]; + {d_id} -> {none_id} [label="b",weight=2]; + {d_id} -> {none_id} [label="c",weight=2]; + {d_id} -> {none_id} [label="d",weight=2]; + {d_id} -> {none_id} [label="e",weight=2]; + {d_id} -> {none_id} [label="f",weight=2]; + {d_id} -> {none_id} [label="g",weight=2]; + {none_id}[label="NoneType\\nNone"]; + {none_id}[fillcolor="0,0,0.766667"]; + }} + """ + ).format( + d_id=objgraph._obj_node_id(d), + none_id=objgraph._obj_node_id(None), + ), + ) + @mock.patch('objgraph.IS_INTERACTIVE', True) @mock.patch('objgraph.graphviz', create=True) def test_ipython(self, mock_graphviz):