Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

correct labels for dicts #50

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 28 additions & 20 deletions objgraph.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
graingert marked this conversation as resolved.
Show resolved Hide resolved
# `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
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So AFAIU neighbours is edges without duplicates, and counts is the count of duplicates. This took me a while to understand, maybe a comment would be helpful?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no need for a comment if we can add more_itertools:

https://more-itertools.readthedocs.io/en/stable/api.html#more_itertools.unique_everseen

neighbours = more_itertools.unique_everseen(edges, key=id)

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd rather not; I like that objgraph is a single file library with no external dependencies I can copy over into any random location on my python path and start using immediately.

ignore.add(id(neighbours))
n = 0
skipped = 0
Expand All @@ -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(
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't work on Python 2.7.

I'd have to stop and think whether I want to drop Python 2.7 compatibility at this point.

_edge_labels(srcnode, tgtnode, shortnames),
range(counts[id(source)]),
fillvalue='',
):
graingert marked this conversation as resolved.
Show resolved Hide resolved
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)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A couple of lines down from here there's a del neighbours, and I think you might want to add del counts in there.

It probably doesn't matter, counts holds only ints, and I doubt anyone expects specific refcounts for those, and also they won't be causing problems with memory usage.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah I figured it wasn't worth it, could wrap this whole thing in a def so everything is automatically deleted

Expand Down Expand Up @@ -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))
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 for adding max-line-length = 80 to setup.cfg's flake8 section to avoid this line too long error.



def _edge_label(*args, **kwargs):
return next(_edge_labels(*args, **kwargs), '')


_is_identifier = re.compile('[a-zA-Z_][a-zA-Z_0-9]*$').match
Expand Down
30 changes: 30 additions & 0 deletions tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down