Skip to content

Commit

Permalink
fixup! fix: reenable instrumentation of properties
Browse files Browse the repository at this point in the history
  • Loading branch information
apotterri committed Jul 31, 2024
1 parent 7b3119a commit 84c74b9
Show file tree
Hide file tree
Showing 3 changed files with 79 additions and 16 deletions.
42 changes: 27 additions & 15 deletions _appmap/event.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from functools import lru_cache, partial
from inspect import Parameter, Signature
from itertools import chain
import types

from .env import Env
from .recorder import Recorder
Expand Down Expand Up @@ -175,6 +176,31 @@ def to_dict(self, value):
ret.update(describe_value(self.name, value))
return ret

def _get_name_parts(filterable):
"""
Return the module name and qualname for filterable.obj.
If filterable.obj is an operator.attrgetter that we've determined is associated with a property,
compute the names from the fqname of the filterable. If it's anything else, try to get its
__module__ and __qualname__, falling back to default values if they're not available.
"""
fn = filterable.obj
assert callable(fn), f"{filterable} doesn't have a callable obj"

if (
type(fn) is operator.attrgetter
and (parts := filterable.fqname.split("."))
and len(parts) > 2
):
# filterable.fqname was set when we identified this filterable as a property
modname = ".".join(parts[:-2])
qualname = ".".join(parts[-2:])
else:
modname = getattr(fn, "__module__", "unknown")
qualname = getattr(fn, "__qualname__", None)
if qualname is None:
qualname = getattr(fn.__class__, "__name__", "unknown")
return modname, qualname

class CallEvent(Event):
# pylint: disable=method-cache-max-size-none
Expand Down Expand Up @@ -318,21 +344,7 @@ def __init__(self, filterable, parameters, labels):
super().__init__("call")
fn = filterable.obj
self._fn = fn
if type(fn) is not operator.attrgetter:
# fn is a regular function
modname = fn.__module__
qualname = fn.__qualname__
elif (parts := filterable.fqname.split(".")) and len(parts) > 2:
# fn is an attrgetter, which will is being used as the getter for a property. If
# filterable.fqname has enough components to be the fully-qualified name of a class
# member, set the module name and qualname based on those components.
modname = ".".join(parts[:-2])
qualname = ".".join(parts[-2:])
else:
# The two previous cases should handle all known possibilities, but don't crash if
# somethign else sneaks in.
modname = "unknown"
qualname = "unknown"
modname, qualname = _get_name_parts(filterable)
self._fqfn = FqFnName(modname, qualname)

fntype = filterable.fntype
Expand Down
17 changes: 16 additions & 1 deletion _appmap/test/data/properties_class.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
from functools import cached_property
from functools import cached_property, partial
import operator
from typing import NoReturn

def free_read_only(self):
return self._read_only

def free_func():
return "hello world"
class PropertiesClass:
def __init__(self):
self._read_only = "read only"
Expand Down Expand Up @@ -54,3 +58,14 @@ def cached_read_only(self):
return self._read_only

operator_read_only = property(operator.attrgetter("cached_read_only"))

tastes = {"bacon": "yum"}

def __getitem__(self, key):
return self.tastes[key]

taste = property(operator.itemgetter("bacon"))

free_read_only_prop = property(free_read_only)

static_partial_method = staticmethod(partial(free_func))
36 changes: 36 additions & 0 deletions _appmap/test/test_properties.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,3 +109,39 @@ def test_operator_attrgetter(events):
"defined_class": "properties_class.PropertiesClass",
"method_id": "operator_read_only (get)",
})

def test_operator_itemgetter(events):
from properties_class import PropertiesClass

ec = PropertiesClass()
assert ec.taste == "yum"
assert len(events) == 2
assert events[0].to_dict() == DictIncluding({
"event": "call",
"defined_class": "operator",
"method_id": "itemgetter (get)",
})


def test_free_function(events):
from properties_class import PropertiesClass

ec = PropertiesClass()
assert ec.free_read_only_prop == "read only"
assert len(events) == 2
assert events[0].to_dict() == DictIncluding({
"event": "call",
"defined_class": "properties_class",
"method_id": "free_read_only (get)",
})


@pytest.mark.xfail(
raises=AssertionError,
reason="needs fix for https://github.com/getappmap/appmap-python/issues/365",
)
def test_functools_partial(events):
from properties_class import PropertiesClass

PropertiesClass.static_partial_method()
assert len(events) > 0

0 comments on commit 84c74b9

Please sign in to comment.