Skip to content

Commit 79f96c8

Browse files
committed
Fixed recursion bug
line_profiler/line_profiler.py::LineProfiler.add_class(), .add_module() Added object-identity tracking to avoid possible recursion if the namespaces profiled reference themselves or one another tests/test_explicit_profile.py test_profiler_add_class_recursion_guard() New test for adding self-/mutually-referential classes to be profiled
1 parent adec060 commit 79f96c8

2 files changed

Lines changed: 61 additions & 2 deletions

File tree

line_profiler/line_profiler.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -149,12 +149,20 @@ def _add_namespace(self, namespace, *, wrap=False):
149149
n (int):
150150
Number of members added to the profiler.
151151
"""
152+
return self._add_namespace_inner(set(), namespace, wrap=wrap)
153+
154+
def _add_namespace_inner(self, duplicate_tracker, namespace, *,
155+
wrap=False):
152156
count = 0
153-
add_cls = self.add_class
157+
add_cls = self._add_namespace_inner
154158
add_func = self.add_callable
155159
for attr, value in vars(namespace).items():
160+
if id(value) in duplicate_tracker:
161+
continue
162+
duplicate_tracker.add(id(value))
156163
if isinstance(value, type):
157-
count += 1 if add_cls(value, wrap=wrap) else 0
164+
if add_cls(duplicate_tracker, value, wrap=wrap):
165+
count += 1
158166
continue
159167
try:
160168
func_needs_adding = add_func(value)

tests/test_explicit_profile.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -498,6 +498,57 @@ def func3():
498498
assert line.split()[1] == ('1' if has_timing else 'pass')
499499

500500

501+
def test_profiler_add_class_recursion_guard():
502+
"""
503+
Test that if we were to add a pair of classes which each of them
504+
has a reference to the other in its namespace, we don't end up in
505+
infinite recursion.
506+
"""
507+
with contextlib.ExitStack() as stack:
508+
enter = stack.enter_context
509+
enter(ub.ChDir(enter(tempfile.TemporaryDirectory())))
510+
curdir = ub.Path.cwd()
511+
(curdir / 'script.py').write_text(ub.codeblock("""
512+
from line_profiler import LineProfiler
513+
514+
515+
class Class1:
516+
def method1(self):
517+
pass
518+
519+
class ChildClass1:
520+
def child_method_1(self):
521+
pass
522+
523+
524+
class Class2:
525+
def method2(self):
526+
pass
527+
528+
class ChildClass2:
529+
def child_method_2(self):
530+
pass
531+
532+
OtherClass = Class1
533+
# A duplicate reference shouldn't affect profiling either
534+
YetAnotherClass = Class1
535+
536+
537+
# Add self/mutual references
538+
Class1.ThisClass = Class1
539+
Class1.OtherClass = Class2
540+
541+
profile = LineProfiler()
542+
profile.add_class(Class1)
543+
assert len(profile.functions) == 4
544+
assert Class1.method1 in profile.functions
545+
assert Class2.method2 in profile.functions
546+
assert Class1.ChildClass1.child_method_1 in profile.functions
547+
assert Class2.ChildClass2.child_method_2 in profile.functions
548+
"""))
549+
ub.cmd([sys.executable, 'script.py'], verbose=2).check_returncode()
550+
551+
501552
if __name__ == '__main__':
502553
...
503554
test_simple_explicit_nonglobal_usage()

0 commit comments

Comments
 (0)