Repository: scoutapp/scout_apm_python
Pull request #634 (2021-03-15 16:41:27)

[pre-commit.ci] pre-commit autoupdate


Patch:
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index acc9ffa..a96dbc7 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -18,11 +18,11 @@ repos:
   - id: black
     language_version: python3
 - repo: https://github.com/pycqa/isort
-  rev: 5.7.0
+  rev: 5.8.0
   hooks:
   - id: isort
-- repo: https://gitlab.com/pycqa/flake8
-  rev: 3.8.4
+- repo: https://github.com/PyCQA/flake8
+  rev: 3.9.0
   hooks:
   - id: flake8
     additional_dependencies:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8d0389d..b2bd468 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,15 @@
 # Changelog
 
+## Pending
+
+### Added
+- Tested with Django 3.2. Only define ``default_app_config`` when using
+  a version of Django earlier than 3.2.
+
+### Fixed
+- Exclude python library paths from backtraces.
+  ([PR #514](https://github.com/scoutapp/scout_apm_python/issues/514))
+
 ## [2.18.0] 2021-02-09
 
 ### Added
diff --git a/setup.py b/setup.py
index 8dba395..fa566cf 100644
--- a/setup.py
+++ b/setup.py
@@ -82,6 +82,7 @@ setup(
         "Framework :: Django :: 2.2",
         "Framework :: Django :: 3.0",
         "Framework :: Django :: 3.1",
+        "Framework :: Django :: 3.2",
         "Framework :: Flask",
         "Framework :: Pyramid",
         "Intended Audience :: Developers",
diff --git a/src/scout_apm/core/backtrace.py b/src/scout_apm/core/backtrace.py
index dee91d2..5551064 100644
--- a/src/scout_apm/core/backtrace.py
+++ b/src/scout_apm/core/backtrace.py
@@ -1,7 +1,9 @@
 # coding=utf-8
 from __future__ import absolute_import, division, print_function, unicode_literals
 
+import itertools
 import sys
+import sysconfig
 import traceback
 
 # Maximum non-Scout frames to target retrieving
@@ -10,23 +12,55 @@ LIMIT = 50
 IGNORED = 1
 
 
+def filter_frames(frames):
+    """Filter the stack trace frames down to non-library code."""
+    paths = sysconfig.get_paths()
+    library_paths = {paths["purelib"], paths["platlib"]}
+    for frame in frames:
+        if not any(frame["file"].startswith(exclusion) for exclusion in library_paths):
+            yield frame
+
+
 if sys.version_info >= (3, 5):
 
+    def frame_walker():
+        """Iterate over each frame of the stack.
+
+        Taken from python3/traceback.ExtractSummary.extract to support
+        iterating over the entire stack, but without creating a large
+        data structure.
+        """
+        for frame, lineno in traceback.walk_stack(sys._getframe().f_back):
+            co = frame.f_code
+            filename = co.co_filename
+            name = co.co_name
+            yield {"file": filename, "line": lineno, "function": name}
+
     def capture():
-        return [
-            {"file": frame.filename, "line": frame.lineno, "function": frame.name}
-            for frame in reversed(
-                traceback.extract_stack(limit=LIMIT + IGNORED)[:-IGNORED]
-            )
-        ]
+        return list(itertools.islice(filter_frames(frame_walker()), LIMIT))
 
 
 else:
 
+    def frame_walker():
+        """Iterate over each frame of the stack.
+
+        Taken from python2.7/traceback.extract_stack to support iterating
+        over the entire stack, but without creating a large data structure.
+        """
+        try:
+            raise ZeroDivisionError
+        except ZeroDivisionError:
+            # Get the current frame
+            f = sys.exc_info()[2].tb_frame.f_back
+
+        while f is not None:
+            lineno = f.f_lineno
+            co = f.f_code
+            filename = co.co_filename
+            name = co.co_name
+            yield {"file": filename, "line": lineno, "function": name}
+            f = f.f_back
+
     def capture():
-        return [
-            {"file": frame[0], "line": frame[1], "function": frame[3]}
-            for frame in reversed(
-                traceback.extract_stack(limit=LIMIT + IGNORED)[:-IGNORED]
-            )
-        ]
+        return list(itertools.islice(filter_frames(frame_walker()), LIMIT))
diff --git a/src/scout_apm/django/__init__.py b/src/scout_apm/django/__init__.py
index 5c78416..88c27ce 100644
--- a/src/scout_apm/django/__init__.py
+++ b/src/scout_apm/django/__init__.py
@@ -1,4 +1,8 @@
 # coding=utf-8
 from __future__ import absolute_import, division, print_function, unicode_literals
 
-default_app_config = "scout_apm.django.apps.ScoutApmDjangoConfig"
+import django
+
+if django.VERSION < (3, 2, 0):
+    # Only define default_app_config when using a version earlier than 3.2
+    default_app_config = "scout_apm.django.apps.ScoutApmDjangoConfig"
diff --git a/tox.ini b/tox.ini
index 97f18d2..7261c91 100644
--- a/tox.ini
+++ b/tox.ini
@@ -8,6 +8,7 @@ envlist =
     {py35,py36,py37,py38,py39}-django22
     {py36,py37,py38,py39}-django30
     {py36,py37,py38,py39}-django31
+    {py36,py37,py38,py39}-django32
 
 [testenv]
 passenv =
@@ -37,6 +38,8 @@ deps =
     django30: djangorestframework
     django31: Django>=3.1,<3.2
     django31: djangorestframework
+    django32: Django>=3.2,<3.3
+    django32: djangorestframework
     dramatiq>=1.0.0 ; python_version >= "3.5"
     elasticsearch
     falcon
@@ -63,7 +66,8 @@ deps =
     requests
     rq
     six
-    sqlalchemy
+    sqlalchemy ; python_version != "3.4"
+    sqlalchemy<1.3.24 ; python_version == "3.4"
     starlette ; python_version >= "3.6"
     webtest
 commands =


Test patch:
diff --git a/tests/unit/core/test_backtrace.py b/tests/unit/core/test_backtrace.py
index 6d3dfd7..c59e5b5 100644
--- a/tests/unit/core/test_backtrace.py
+++ b/tests/unit/core/test_backtrace.py
@@ -1,7 +1,9 @@
 # coding=utf-8
 from __future__ import absolute_import, division, print_function, unicode_literals
 
+import os
 import sys
+import sysconfig
 
 from scout_apm.core import backtrace
 
@@ -37,3 +39,17 @@ def test_capture_limit():
 
     stack = capture_recursive_bottom(backtrace.LIMIT * 2)
     assert len(stack) == backtrace.LIMIT
+
+
+def test_filter_frames():
+    """Verify the frames from the library paths are excluded."""
+    paths = sysconfig.get_paths()
+    library_path = {paths["purelib"], paths["platlib"]}.pop()
+    frames = [
+        {"file": os.path.join(library_path, "test"), "line": 1, "function": "foo"},
+        {"file": "/valid/path", "line": 1, "function": "foo"},
+    ]
+
+    actual = list(backtrace.filter_frames(frames))
+    assert len(actual) == 1
+    assert actual[0]["file"] == "/valid/path"