Repository: scoutapp/scout_apm_python
Pull request #410 (2019-11-29 13:39:29)

Rewrite backtrace and tests
Make it faster by using a split implementation based on the Python version, and a list comprehension

Combine the tests since they really all tested one output.

Avoid capturing the frame for `capture()` itself, as per comment in tests - it's not very useful information to the user.


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index fea4582..6113089 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -22,6 +22,8 @@
 - Always capture SQL tracebacks for `executemany` queries going over the
   built-in count threshold
   ([PR #409](https://github.com/scoutapp/scout_apm_python/pull/409)).
+- Don't capture internal traceback function's frame in tracebacks
+  ([PR #410](https://github.com/scoutapp/scout_apm_python/pull/410)).
 
 ## [2.8.1] 2019-11-19
 
diff --git a/src/scout_apm/core/backtrace.py b/src/scout_apm/core/backtrace.py
index 5e85d04..4293850 100644
--- a/src/scout_apm/core/backtrace.py
+++ b/src/scout_apm/core/backtrace.py
@@ -1,31 +1,22 @@
 # coding=utf-8
-"""
-Module for helper functions to capture tracebacks
-"""
 from __future__ import absolute_import, division, print_function, unicode_literals
 
+import sys
 import traceback
 
+if sys.version_info >= (3, 5):
 
-def capture():
-    stack = traceback.extract_stack()
-    formatted_stack = []
-    for frame in stack:
-        # Python 2.7 and 3.4 returned tuples
-        if type(frame) is tuple:
-            filename = frame[0]
-            line = frame[1]
-            function = frame[3]
-        # 3.5+ returned objects
-        else:
-            filename = frame.filename
-            line = frame.lineno
-            function = frame.name
+    def capture():
+        return [
+            {"file": frame.filename, "line": frame.lineno, "function": frame.name}
+            for frame in reversed(traceback.extract_stack()[:-1])
+        ]
 
-        formatted_stack.append({"file": filename, "line": line, "function": function})
 
-    # Python puts the closest stack frames at the end of the traceback. But we
-    # want them up front
-    formatted_stack.reverse()
+else:
 
-    return formatted_stack
+    def capture():
+        return [
+            {"file": frame[0], "line": frame[1], "function": frame[3]}
+            for frame in reversed(traceback.extract_stack()[:-1])
+        ]


Test patch:
diff --git a/tests/tools.py b/tests/tools.py
index b2cca88..b14f723 100644
--- a/tests/tools.py
+++ b/tests/tools.py
@@ -14,6 +14,9 @@ from tests.compat import mock, nullcontext
 skip_if_python_2 = pytest.mark.skipif(
     sys.version_info[0] == 2, reason="Requires Python 3"
 )
+skip_if_python_3 = pytest.mark.skipif(
+    sys.version_info[0] == 3, reason="Requires Python 2"
+)
 
 skip_if_objtrace_not_extension = pytest.mark.skipif(
     not objtrace.is_extension, reason="Requires objtrace C extension"
diff --git a/tests/unit/core/test_backtrace.py b/tests/unit/core/test_backtrace.py
index 80dafe1..2c97066 100644
--- a/tests/unit/core/test_backtrace.py
+++ b/tests/unit/core/test_backtrace.py
@@ -1,26 +1,28 @@
 # coding=utf-8
 from __future__ import absolute_import, division, print_function, unicode_literals
 
-from scout_apm.core import backtrace
+import sys
 
+from scout_apm.core import backtrace
 
-def test_traceback_contains_file_line_function():
-    traceback = backtrace.capture()
-    for frame in traceback:
-        assert set(frame) == {"file", "line", "function"}
 
+def test_capture():
+    stack = backtrace.capture()
 
-def test_traceback_returns_correct_types():
-    traceback = backtrace.capture()
-    for frame in traceback:
+    assert isinstance(stack, list)
+    assert len(stack) >= 1
+    for frame in stack:
+        assert isinstance(frame, dict)
+        assert set(frame.keys()) == {"file", "line", "function"}
         assert isinstance(frame["file"], str)
         assert isinstance(frame["line"], int)
         assert isinstance(frame["function"], str)
 
+    assert stack[0]["file"] == format_py_filename(__file__)
+
 
-def test_traceback_contains_inner_frame_first():
-    traceback = backtrace.capture()
-    # Consider removing the frame corresponding to the capture() call?
-    # On Python 2, __file__ points to the compiled bytecode, not the source.
-    assert traceback[0]["file"] == backtrace.__file__.replace(".pyc", ".py")
-    assert traceback[1]["file"] == __file__.replace(".pyc", ".py")
+def format_py_filename(filename):
+    if sys.version_info[0] == 2 and filename.endswith(".pyc"):
+        # Python 2 will include .pyc filename if it's used, so strip that
+        return filename[:-1]
+    return filename