Repository navigation
Description
Summary
The classMap position of a class, and therefore the code object id of its methods,
depends on the order in which functions in its module are first called during a
recording. If a module-level function is called before a method of a class in the same
module, the module is written as a class node and the class is nested inside it. If
the method is called first, the module is a package node. The same method then has a
different id in different recordings (demo/util::Box#get versus demo/util/Box#get),
so tools that match or compare code objects across recordings treat them as two
functions.
Environment
- appmap (Python agent) 3.0.1
- Python 3.10, Ubuntu, pytest
Steps to reproduce
mkdir repro && cd repro
python3 -m venv .venv
.venv/bin/pip install appmap pytest
mkdir -p demo tests
touch demo/__init__.py
cat > demo/util.py <<'PY'
def helper(x):
return x + 1
class Box:
def get(self):
return 1
PY
cat > tests/test_util.py <<'PY'
from demo.util import Box, helper
def test_method_first():
assert helper(Box().get()) == 2
def test_function_first():
helper(1)
assert Box().get() == 1
PY
cat > appmap.yml <<'YML'
name: demo
language: python
appmap_dir: tmp/appmap
packages:
- path: demo
YML
.venv/bin/appmap-python .venv/bin/python -m pytest -q
python3 - <<'PY'
import json, glob
for f in sorted(glob.glob('tmp/appmap/pytest/*.json')):
def walk(n, p):
p = p + [f"{n['type']}:{n['name']}"]
if n['type'] == 'function':
print(' ', ' > '.join(p))
for c in n.get('children', []):
walk(c, p)
print(f.split('/')[-1])
for n in json.load(open(f))['classMap']:
walk(n, [])
PYExpected
Box.get has the same classMap path in both recordings, with util as a package
node in both.
Actual
test_function_first.appmap.json
package:demo > class:util > function:helper
package:demo > class:util > class:Box > function:get
test_method_first.appmap.json
package:demo > package:util > class:Box > function:get
package:demo > package:util > function:helper
The call events are the same in both cases (defined_class demo.util.Box, method
get). Only the classMap differs.
First seen on a real project indexed with appmap index (CLI 3.206.0) and read through
appmap query mcp. There, endaq.device.https_interface.HTTPSCommandInterface
methods appear as endaq/device/https_interface::HTTPSCommandInterface#_writeCommand in
one recording and endaq/device/https_interface/HTTPSCommandInterface#_writeCommand in
another, and a get_call_tree focus on one id does not match the other recording.
Activity