Skip to content

Commit b969537

Browse files
- modified instrospection in an attempt to resolve issue 55;
1 parent a509d4f commit b969537

File tree

2 files changed

+24
-8
lines changed

2 files changed

+24
-8
lines changed

src/slipcover/slipcover.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -622,12 +622,13 @@ def print_coverage(self, outfile=sys.stdout, *, missing_width=None) -> None:
622622

623623
@staticmethod
624624
def find_functions(items, visited : set):
625-
import inspect
625+
# Don't use isinstance() or inspect.isfunction, as isinstance as may call __class__,
626+
# which may have side effects (e.g., using Celery https://github.com/celery/celery).
626627
def is_patchable_function(func):
627628
# PyPy has no "builtin functions" like CPython. instead, it uses
628629
# regular functions, with a special type of code object.
629630
# the second condition is always True on CPython
630-
return inspect.isfunction(func) and type(func.__code__) is types.CodeType
631+
return issubclass(type(func), types.FunctionType) and type(func.__code__) is types.CodeType
631632

632633
def find_funcs(root):
633634
if is_patchable_function(root):
@@ -637,7 +638,7 @@ def find_funcs(root):
637638

638639
# Prefer isinstance(x,type) over isclass(x) because many many
639640
# things, such as str(), are classes
640-
elif isinstance(root, type):
641+
elif issubclass(type(root), type):
641642
if root not in visited:
642643
visited.add(root)
643644

@@ -653,7 +654,7 @@ def find_funcs(root):
653654
yield from find_funcs(base.__dict__[obj_key])
654655
break
655656

656-
elif (isinstance(root, classmethod) or isinstance(root, staticmethod)) and \
657+
elif (issubclass(type(root), classmethod) or issubclass(type(root), staticmethod)) and \
657658
is_patchable_function(root.__func__):
658659
if root.__func__ not in visited:
659660
visited.add(root.__func__)

tests/test_instrumentation.py

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -626,13 +626,14 @@ def foo(n):
626626
assert old_code == foo.__code__, "Code de-instrumented"
627627

628628

629+
def func_names(funcs):
630+
# pytest-asyncio > 0.21.1 adds __pytest_asyncio_... event loop functions
631+
return sorted([f.__name__ for f in funcs if f.__name__ != 'scoped_event_loop'])
632+
633+
629634
def test_find_functions():
630635
import class_test as t
631636

632-
def func_names(funcs):
633-
# pytest-asyncio > 0.21.1 adds __pytest_asyncio_... event loop functions
634-
return sorted([f.__name__ for f in funcs if f.__name__ != 'scoped_event_loop'])
635-
636637
assert ["b", "b_classm", "b_static", "f1", "f2", "f3", "f4", "f5", "f7",
637638
"f_classm", "f_static"] == \
638639
func_names(sc.Slipcover.find_functions(t.__dict__.values(), set()))
@@ -655,3 +656,17 @@ def func_names(funcs):
655656
visited))
656657

657658

659+
def test_find_functions_class_side_effect():
660+
# Celery overrides __class__ in ways that cause side effects -- see issue 55
661+
class A:
662+
@property
663+
def __class__(self):
664+
raise RuntimeError("We don't want this called")
665+
666+
class B:
667+
foo = A()
668+
669+
def bar(self):
670+
pass
671+
672+
assert ["bar"] == func_names(sc.Slipcover.find_functions(B.__dict__.values(), set()))

0 commit comments

Comments
 (0)