Skip to content

classMap nesting, and so code object ids, depend on call order within a module #388

Open

Description

@devin-ai-integration

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, [])
PY

Expected

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions