Repository: getappmap/appmap-python (language: python)

Issue:
Release of numpy2 causes the Travis build to fail
`numpy` v2 just came out and changed the behavior of `repr(numpy.int64(0))`. Previous, the behavior was the same as `str` (it return `0`), but now it returns `numpy.int64(0)`.


Discussion / hints:

Fix patch:
diff --git a/tox.ini b/tox.ini
index faeb02af..a5ef8a44 100644
--- a/tox.ini
+++ b/tox.ini
@@ -19,7 +19,7 @@ deps=
     poetry
     web: {[web-deps]deps}
     py38: numpy==1.24.4
-    py3{9,10,11,12}: numpy >=1.26
+    py3{9,10,11,12}: numpy >=2
     flask2: Flask >= 2.0, <3.0
     django3: Django >=3.2, <4.0
     sqlalchemy1: sqlalchemy >=1.4.11, <2.0


Test patch:
diff --git a/_appmap/test/data/pytest/expected/pytest.appmap.json b/_appmap/test/data/pytest/expected/pytest-numpy1.appmap.json
similarity index 58%
rename from _appmap/test/data/pytest/expected/pytest.appmap.json
rename to _appmap/test/data/pytest/expected/pytest-numpy1.appmap.json
index 8723789a..cd3ef53c 100644
--- a/_appmap/test/data/pytest/expected/pytest.appmap.json
+++ b/_appmap/test/data/pytest/expected/pytest-numpy1.appmap.json
@@ -8,75 +8,95 @@
       "name": "appmap",
       "url": "https://github.com/applandinc/appmap-python"
     },
-    "source_location": "test_simple.py:5",
-    "name": "hello world",
-    "feature": "Hello world",
     "app": "Simple",
     "recorder": {
       "name": "pytest",
       "type": "tests"
     },
+    "source_location": "test_simple.py:5",
+    "name": "hello world",
+    "feature": "Hello world",
     "test_status": "succeeded"
   },
   "events": [
     {
+      "defined_class": "simple.Simple",
+      "method_id": "hello_world",
+      "path": "simple.py",
+      "lineno": 8,
       "static": false,
       "receiver": {
+        "class": "simple.Simple",
         "kind": "req",
-        "value": "<simple.Simple object at 0xabcdef>",
         "name": "self",
-        "class": "simple.Simple"
+        "value": "<simple.Simple object at 0xabcdef>"
       },
       "parameters": [],
       "id": 1,
       "event": "call",
-      "thread_id": 1,
-      "defined_class": "simple.Simple",
-      "method_id": "hello_world",
-      "path": "simple.py",
-      "lineno": 16
+      "thread_id": 1
     },
     {
+      "defined_class": "simple.Simple",
+      "method_id": "hello",
+      "path": "simple.py",
+      "lineno": 2,
       "static": false,
       "receiver": {
+        "class": "simple.Simple",
         "kind": "req",
-        "value": "<simple.Simple object at 0xabcdef>",
         "name": "self",
-        "class": "simple.Simple"
+        "value": "<simple.Simple object at 0xabcdef>"
       },
       "parameters": [],
       "id": 2,
       "event": "call",
-      "thread_id": 1,
-      "defined_class": "simple.Simple",
-      "method_id": "get_non_json_serializable",
-      "path": "simple.py",
-      "lineno": 13
+      "thread_id": 1
     },
     {
+      "return_value": {
+        "class": "builtins.str",
+        "value": "'Hello'"
+      },
+      "parent_id": 2,
+      "id": 3,
+      "event": "return",
+      "thread_id": 1
+    },
+    {
+      "defined_class": "simple.Simple",
+      "method_id": "world",
+      "path": "simple.py",
+      "lineno": 5,
       "static": false,
       "receiver": {
+        "class": "simple.Simple",
         "kind": "req",
-        "value": "<simple.Simple object at 0xabcdef>",
         "name": "self",
-        "class": "simple.Simple"
+        "value": "<simple.Simple object at 0xabcdef>"
       },
       "parameters": [],
-      "id": 3,
+      "id": 4,
       "event": "call",
-      "thread_id": 1,
-      "defined_class": "simple.Simple",
-      "method_id": "hello",
-      "path": "simple.py",
-      "lineno": 7
+      "thread_id": 1
     },
     {
       "return_value": {
-        "value": "'Hello'",
-        "class": "builtins.str"
+        "class": "builtins.str",
+        "value": "'world!'"
       },
-      "parent_id": 3,
-      "id": 4,
+      "parent_id": 4,
+      "id": 5,
+      "event": "return",
+      "thread_id": 1
+    },
+    {
+      "return_value": {
+        "class": "builtins.str",
+        "value": "'Hello world!'"
+      },
+      "parent_id": 1,
+      "id": 6,
       "event": "return",
       "thread_id": 1
     },
@@ -89,27 +109,52 @@
         "class": "simple.Simple"
       },
       "parameters": [],
-      "id": 5,
+      "id": 7,
       "event": "call",
       "thread_id": 1,
       "defined_class": "simple.Simple",
-      "method_id": "world",
+      "method_id": "show_numpy_dict",
       "path": "simple.py",
-      "lineno": 10
+      "lineno": 11
     },
     {
-      "return_value": {
-        "value": "'world!'",
-        "class": "builtins.str"
+      "static": false,
+      "receiver": {
+        "kind": "req",
+        "value": "<simple.Simple object at 0xabcdef>",
+        "name": "self",
+        "class": "simple.Simple"
       },
-      "parent_id": 5,
-      "id": 6,
-      "event": "return",
-      "thread_id": 1
+      "parameters": [
+        {
+          "kind": "req",
+          "value": "{0: 'zero', 1: 'one'}",
+          "name": "d",
+          "class": "builtins.dict",
+          "properties": [
+            {
+              "name": "0",
+              "class": "builtins.str"
+            },
+            {
+              "name": "1",
+              "class": "builtins.str"
+            }
+          ],
+          "size": 2
+        }
+      ],
+      "id": 8,
+      "event": "call",
+      "thread_id": 1,
+      "defined_class": "simple.Simple",
+      "method_id": "get_numpy_dict",
+      "path": "simple.py",
+      "lineno": 18
     },
     {
       "return_value": {
-        "value": "{0: 'Hello', 1: 'world!'}",
+        "value": "{0: 'zero', 1: 'one'}",
         "class": "builtins.dict",
         "properties": [
           {
@@ -123,18 +168,29 @@
         ],
         "size": 2
       },
-      "parent_id": 2,
-      "id": 7,
+      "parent_id": 8,
+      "id": 9,
       "event": "return",
       "thread_id": 1
     },
     {
       "return_value": {
-        "value": "'Hello world!'",
-        "class": "builtins.str"
+        "value": "{0: 'zero', 1: 'one'}",
+        "class": "builtins.dict",
+        "properties": [
+          {
+            "name": "0",
+            "class": "builtins.str"
+          },
+          {
+            "name": "1",
+            "class": "builtins.str"
+          }
+        ],
+        "size": 2
       },
-      "parent_id": 1,
-      "id": 8,
+      "parent_id": 7,
+      "id": 10,
       "event": "return",
       "thread_id": 1
     }
@@ -149,27 +205,33 @@
           "type": "class",
           "children": [
             {
-              "name": "get_non_json_serializable",
+              "name": "get_numpy_dict",
               "type": "function",
-              "location": "simple.py:13",
+              "location": "simple.py:18",
               "static": false
             },
             {
               "name": "hello",
               "type": "function",
-              "location": "simple.py:7",
+              "location": "simple.py:2",
               "static": false
             },
             {
               "name": "hello_world",
               "type": "function",
-              "location": "simple.py:16",
+              "location": "simple.py:8",
+              "static": false
+            },
+            {
+              "name": "show_numpy_dict",
+              "type": "function",
+              "location": "simple.py:11",
               "static": false
             },
             {
               "name": "world",
               "type": "function",
-              "location": "simple.py:10",
+              "location": "simple.py:5",
               "static": false
             }
           ]
diff --git a/_appmap/test/data/pytest/expected/pytest-numpy2.appmap.json b/_appmap/test/data/pytest/expected/pytest-numpy2.appmap.json
new file mode 100644
index 00000000..0f12e30c
--- /dev/null
+++ b/_appmap/test/data/pytest/expected/pytest-numpy2.appmap.json
@@ -0,0 +1,242 @@
+{
+  "version": "1.9",
+  "metadata": {
+    "language": {
+      "name": "python"
+    },
+    "client": {
+      "name": "appmap",
+      "url": "https://github.com/applandinc/appmap-python"
+    },
+    "app": "Simple",
+    "recorder": {
+      "name": "pytest",
+      "type": "tests"
+    },
+    "source_location": "test_simple.py:5",
+    "name": "hello world",
+    "feature": "Hello world",
+    "test_status": "succeeded"
+  },
+  "events": [
+    {
+      "defined_class": "simple.Simple",
+      "method_id": "hello_world",
+      "path": "simple.py",
+      "lineno": 8,
+      "static": false,
+      "receiver": {
+        "class": "simple.Simple",
+        "kind": "req",
+        "name": "self",
+        "value": "<simple.Simple object at 0xabcdef>"
+      },
+      "parameters": [],
+      "id": 1,
+      "event": "call",
+      "thread_id": 1
+    },
+    {
+      "defined_class": "simple.Simple",
+      "method_id": "hello",
+      "path": "simple.py",
+      "lineno": 2,
+      "static": false,
+      "receiver": {
+        "class": "simple.Simple",
+        "kind": "req",
+        "name": "self",
+        "value": "<simple.Simple object at 0xabcdef>"
+      },
+      "parameters": [],
+      "id": 2,
+      "event": "call",
+      "thread_id": 1
+    },
+    {
+      "return_value": {
+        "class": "builtins.str",
+        "value": "'Hello'"
+      },
+      "parent_id": 2,
+      "id": 3,
+      "event": "return",
+      "thread_id": 1
+    },
+    {
+      "defined_class": "simple.Simple",
+      "method_id": "world",
+      "path": "simple.py",
+      "lineno": 5,
+      "static": false,
+      "receiver": {
+        "class": "simple.Simple",
+        "kind": "req",
+        "name": "self",
+        "value": "<simple.Simple object at 0xabcdef>"
+      },
+      "parameters": [],
+      "id": 4,
+      "event": "call",
+      "thread_id": 1
+    },
+    {
+      "return_value": {
+        "class": "builtins.str",
+        "value": "'world!'"
+      },
+      "parent_id": 4,
+      "id": 5,
+      "event": "return",
+      "thread_id": 1
+    },
+    {
+      "return_value": {
+        "class": "builtins.str",
+        "value": "'Hello world!'"
+      },
+      "parent_id": 1,
+      "id": 6,
+      "event": "return",
+      "thread_id": 1
+    },
+    {
+      "static": false,
+      "receiver": {
+        "kind": "req",
+        "value": "<simple.Simple object at 0xabcdef>",
+        "name": "self",
+        "class": "simple.Simple"
+      },
+      "parameters": [],
+      "id": 7,
+      "event": "call",
+      "thread_id": 1,
+      "defined_class": "simple.Simple",
+      "method_id": "show_numpy_dict",
+      "path": "simple.py",
+      "lineno": 11
+    },
+    {
+      "static": false,
+      "receiver": {
+        "kind": "req",
+        "value": "<simple.Simple object at 0xabcdef>",
+        "name": "self",
+        "class": "simple.Simple"
+      },
+      "parameters": [
+        {
+          "kind": "req",
+          "value": "{np.int64(0): 'zero', np.int64(1): 'one'}",
+          "name": "d",
+          "class": "builtins.dict",
+          "properties": [
+            {
+              "name": "0",
+              "class": "builtins.str"
+            },
+            {
+              "name": "1",
+              "class": "builtins.str"
+            }
+          ],
+          "size": 2
+        }
+      ],
+      "id": 8,
+      "event": "call",
+      "thread_id": 1,
+      "defined_class": "simple.Simple",
+      "method_id": "get_numpy_dict",
+      "path": "simple.py",
+      "lineno": 18
+    },
+    {
+      "return_value": {
+        "value": "{np.int64(0): 'zero', np.int64(1): 'one'}",
+        "class": "builtins.dict",
+        "properties": [
+          {
+            "name": "0",
+            "class": "builtins.str"
+          },
+          {
+            "name": "1",
+            "class": "builtins.str"
+          }
+        ],
+        "size": 2
+      },
+      "parent_id": 8,
+      "id": 9,
+      "event": "return",
+      "thread_id": 1
+    },
+    {
+      "return_value": {
+        "value": "{np.int64(0): 'zero', np.int64(1): 'one'}",
+        "class": "builtins.dict",
+        "properties": [
+          {
+            "name": "0",
+            "class": "builtins.str"
+          },
+          {
+            "name": "1",
+            "class": "builtins.str"
+          }
+        ],
+        "size": 2
+      },
+      "parent_id": 7,
+      "id": 10,
+      "event": "return",
+      "thread_id": 1
+    }
+  ],
+  "classMap": [
+    {
+      "name": "simple",
+      "type": "package",
+      "children": [
+        {
+          "name": "Simple",
+          "type": "class",
+          "children": [
+            {
+              "name": "get_numpy_dict",
+              "type": "function",
+              "location": "simple.py:18",
+              "static": false
+            },
+            {
+              "name": "hello",
+              "type": "function",
+              "location": "simple.py:2",
+              "static": false
+            },
+            {
+              "name": "hello_world",
+              "type": "function",
+              "location": "simple.py:8",
+              "static": false
+            },
+            {
+              "name": "show_numpy_dict",
+              "type": "function",
+              "location": "simple.py:11",
+              "static": false
+            },
+            {
+              "name": "world",
+              "type": "function",
+              "location": "simple.py:5",
+              "static": false
+            }
+          ]
+        }
+      ]
+    }
+  ]
+}
\ No newline at end of file
diff --git a/_appmap/test/data/pytest/expected/status_errored.metadata.json b/_appmap/test/data/pytest/expected/status_errored.metadata.json
index 45b3bed1..1c9d0f21 100644
--- a/_appmap/test/data/pytest/expected/status_errored.metadata.json
+++ b/_appmap/test/data/pytest/expected/status_errored.metadata.json
@@ -2,7 +2,7 @@
   "test_status": "failed",
   "test_failure": {
     "message": "RuntimeError: test error",
-    "location": "test_simple.py:28"
+    "location": "test_simple.py:30"
   },
   "exception": {
     "class": "RuntimeError",
diff --git a/_appmap/test/data/pytest/expected/status_failed.metadata.json b/_appmap/test/data/pytest/expected/status_failed.metadata.json
index cc971c33..cca17c0d 100644
--- a/_appmap/test/data/pytest/expected/status_failed.metadata.json
+++ b/_appmap/test/data/pytest/expected/status_failed.metadata.json
@@ -2,7 +2,7 @@
   "test_status": "failed",
   "test_failure": {
     "message": "AssertionError: assert False",
-    "location": "test_simple.py:14"
+    "location": "test_simple.py:16"
   },
   "exception": {
     "class": "AssertionError",
diff --git a/_appmap/test/data/pytest/expected/status_xfailed.metadata.json b/_appmap/test/data/pytest/expected/status_xfailed.metadata.json
index 992d824d..56494885 100644
--- a/_appmap/test/data/pytest/expected/status_xfailed.metadata.json
+++ b/_appmap/test/data/pytest/expected/status_xfailed.metadata.json
@@ -2,7 +2,7 @@
   "test_status": "failed",
   "test_failure": {
     "message": "AssertionError: assert False",
-    "location": "test_simple.py:19"
+    "location": "test_simple.py:21"
   },
   "exception": {
     "class": "AssertionError",
diff --git a/_appmap/test/data/pytest/simple.py b/_appmap/test/data/pytest/simple.py
index af0338eb..455c80a5 100644
--- a/_appmap/test/data/pytest/simple.py
+++ b/_appmap/test/data/pytest/simple.py
@@ -1,8 +1,3 @@
-import numpy
-
-zero = numpy.int64(0)
-one = numpy.int64(1)
-
 class Simple:
     def hello(self):
         return "Hello"
@@ -10,9 +5,15 @@ def hello(self):
     def world(self):
         return "world!"
 
-    def get_non_json_serializable(self):
-        return { zero: self.hello(), one: self.world() }
-
     def hello_world(self):
-        result = self.get_non_json_serializable()
-        return "%s %s" % (result[zero], result[one])
+        return "%s %s" % (self.hello(), self.world())
+
+    def show_numpy_dict(self):
+        from numpy import int64
+
+        d = self.get_numpy_dict({int64(0): "zero", int64(1): "one"})
+        print(d)
+        return d
+
+    def get_numpy_dict(self, d):
+        return d
\ No newline at end of file
diff --git a/_appmap/test/data/pytest/test_simple.py b/_appmap/test/data/pytest/test_simple.py
index c75c3593..05afaf4b 100644
--- a/_appmap/test/data/pytest/test_simple.py
+++ b/_appmap/test/data/pytest/test_simple.py
@@ -4,10 +4,12 @@
 
 
 def test_hello_world():
-    import simple
+    from simple import Simple
 
     os.chdir("/tmp")
-    assert simple.Simple().hello_world() == "Hello world!"
+    assert Simple().hello_world() == "Hello world!"
+
+    assert len(Simple().show_numpy_dict()) > 0
 
 
 def test_status_failed():
diff --git a/_appmap/test/test_test_frameworks.py b/_appmap/test/test_test_frameworks.py
index e5093585..815e380b 100644
--- a/_appmap/test/test_test_frameworks.py
+++ b/_appmap/test/test_test_frameworks.py
@@ -7,10 +7,11 @@
 import sys
 import types
 from abc import ABC, abstractmethod
+from importlib.metadata import version as md_version
 from pathlib import Path
 
 import pytest
-
+from packaging import version
 from _appmap import recording
 
 from ..test.helpers import DictIncluding
@@ -95,13 +96,14 @@ def setup_class(cls):
         cls._test_type = "pytest"
 
     def run_tests(self, testdir):
-        result = testdir.runpytest("-vv")
+        result = testdir.runpytest("-svv")
         result.assert_outcomes(passed=4, failed=2, xpassed=1, xfailed=1)
 
     def test_enabled(self, testdir):
         self.run_tests(testdir)
         assert len(list(testdir.output().iterdir())) == 6
-        verify_expected_appmap(testdir)
+        numpy_version = version.parse(md_version("numpy"))
+        verify_expected_appmap(testdir, f"-numpy{numpy_version.major}")
         verify_expected_metadata(testdir)
 
 
@@ -190,15 +192,18 @@ def output_dir():
     return pytester
 
 
-def verify_expected_appmap(testdir):
+def verify_expected_appmap(testdir, suffix=""):
     appmap_json = list(testdir.output().glob("*test_hello_world.appmap.json"))
     assert len(appmap_json) == 1  # sanity check
     generated_appmap = normalize_appmap(appmap_json[0].read_text())
 
-    appmap_json = testdir.expected / (f"{testdir.test_type}.appmap.json")
+    appmap_json = testdir.expected / (f"{testdir.test_type}{suffix}.appmap.json")
     expected_appmap = json.loads(appmap_json.read_text())
 
-    assert generated_appmap == expected_appmap, f"expected appmap file {appmap_json}"
+    assert generated_appmap == expected_appmap, (
+        f"expected appmap file {appmap_json}\n"
+        + f"generated appmap: {json.dumps(generated_appmap, indent=2)}"
+    )
 
 
 def verify_expected_metadata(testdir):
@@ -212,4 +217,6 @@ def verify_expected_metadata(testdir):
         name = pattern.search(file.name).group(1)
         metadata = json.loads(file.read_text())["metadata"]
         expected = testdir.expected / f"{name}.metadata.json"
-        assert metadata == DictIncluding(json.loads(expected.read_text()))
+        assert metadata == DictIncluding(
+            json.loads(expected.read_text())
+        ), f"expected appmap: {file}"