Skip to content

Commit

Permalink
- modified instrospection in an attempt to resolve issue 55;
Browse files Browse the repository at this point in the history
  • Loading branch information
jaltmayerpizzorno committed Dec 17, 2024
1 parent a509d4f commit b969537
Show file tree
Hide file tree
Showing 2 changed files with 24 additions and 8 deletions.
9 changes: 5 additions & 4 deletions src/slipcover/slipcover.py
Original file line number Diff line number Diff line change
Expand Up @@ -622,12 +622,13 @@ def print_coverage(self, outfile=sys.stdout, *, missing_width=None) -> None:

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

def find_funcs(root):
if is_patchable_function(root):
Expand All @@ -637,7 +638,7 @@ def find_funcs(root):

# Prefer isinstance(x,type) over isclass(x) because many many
# things, such as str(), are classes
elif isinstance(root, type):
elif issubclass(type(root), type):
if root not in visited:
visited.add(root)

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

elif (isinstance(root, classmethod) or isinstance(root, staticmethod)) and \
elif (issubclass(type(root), classmethod) or issubclass(type(root), staticmethod)) and \
is_patchable_function(root.__func__):
if root.__func__ not in visited:
visited.add(root.__func__)
Expand Down
23 changes: 19 additions & 4 deletions tests/test_instrumentation.py
Original file line number Diff line number Diff line change
Expand Up @@ -626,13 +626,14 @@ def foo(n):
assert old_code == foo.__code__, "Code de-instrumented"


def func_names(funcs):
# pytest-asyncio > 0.21.1 adds __pytest_asyncio_... event loop functions
return sorted([f.__name__ for f in funcs if f.__name__ != 'scoped_event_loop'])


def test_find_functions():
import class_test as t

def func_names(funcs):
# pytest-asyncio > 0.21.1 adds __pytest_asyncio_... event loop functions
return sorted([f.__name__ for f in funcs if f.__name__ != 'scoped_event_loop'])

assert ["b", "b_classm", "b_static", "f1", "f2", "f3", "f4", "f5", "f7",
"f_classm", "f_static"] == \
func_names(sc.Slipcover.find_functions(t.__dict__.values(), set()))
Expand All @@ -655,3 +656,17 @@ def func_names(funcs):
visited))


def test_find_functions_class_side_effect():
# Celery overrides __class__ in ways that cause side effects -- see issue 55
class A:
@property
def __class__(self):
raise RuntimeError("We don't want this called")

class B:
foo = A()

def bar(self):
pass

assert ["bar"] == func_names(sc.Slipcover.find_functions(B.__dict__.values(), set()))

0 comments on commit b969537

Please sign in to comment.