Repository: scoutapp/scout_apm_python (python)

Issue:
Error with the scout add-on in Heroku
With version 2.14.1 of this package, I encounter the following error:
`  File "/code/nluproxy/__init__.py", line 3, in <module>

    from .celery import app as celery_app

  File "/code/nluproxy/celery.py", line 12, in <module>

    scout_apm.celery.install(app)

  File "/usr/local/lib/python3.7/site-packages/scout_apm/celery.py", line 58, in install

    installed = scout_apm.core.install()

  File "/usr/local/lib/python3.7/site-packages/scout_apm/compat.py", line 132, in wrapper

    return func(*args, **kwargs)

  File "/usr/local/lib/python3.7/site-packages/scout_apm/core/__init__.py", line 46, in install

    report_app_metadata()

  File "/usr/local/lib/python3.7/site-packages/scout_apm/core/metadata.py", line 17, in report_app_metadata

    event_value=get_metadata(),

  File "/usr/local/lib/python3.7/site-packages/scout_apm/core/metadata.py", line 37, in get_metadata

    "libraries": get_python_packages_versions(),

  File "/usr/local/lib/python3.7/site-packages/scout_apm/core/metadata.py", line 60, in get_python_packages_versions

    for distribution in distributions()

TypeError: '<' not supported between instances of 'NoneType' and 'str'

`

Pull request description:
Fix crash when scanning packages with incomplete metadata
Fixes #522.

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b283850..3995556 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,16 +1,23 @@
 # Changelog
 
+## Pending
+
+### Fixed
+
+- Fix crash when scanning packages that have a Name or Version that is
+  `None`.
+  ([Issue #522](https://github.com/scoutapp/scout_apm_python/issues/522))
+
 ## [2.14.1] 2020-04-28
 
-## Fixed
+### Fixed
 
 - Update Core Agent to version 1.2.9, fixing several bugs.
   ([Issue #516](https://github.com/scoutapp/scout_apm_python/issues/516))
 
-
 ## [2.14.0] 2020-04-08
 
-## Fixed
+### Fixed
 
 - Support Django's `BASE_DIR` setting being a `pathlib.Path`, as the default
   template in Django 3.1 will set.
diff --git a/src/scout_apm/core/metadata.py b/src/scout_apm/core/metadata.py
index 7b135fb..d8eead4 100644
--- a/src/scout_apm/core/metadata.py
+++ b/src/scout_apm/core/metadata.py
@@ -56,6 +56,15 @@ def get_python_packages_versions():
         return []
 
     return sorted(
-        (distribution.metadata["Name"], distribution.metadata["Version"])
+        (
+            distribution.metadata["Name"],
+            (distribution.metadata["Version"] or "Unknown"),
+        )
         for distribution in distributions()
+        # Filter out distributions wtih None for name or value. This can be the
+        # case for packages without a METADATA or PKG-INFO file in their relevant
+        # distribution directory. According to comments in importlib.metadata
+        # internals this is possible for certain old packages, but I could only
+        # recreate it by deliberately deleting said files.
+        if distribution.metadata["Name"]
     )


Test patch:
diff --git a/tests/unit/core/test_metadata.py b/tests/unit/core/test_metadata.py
index e7d5243..8cbe03b 100644
--- a/tests/unit/core/test_metadata.py
+++ b/tests/unit/core/test_metadata.py
@@ -5,8 +5,8 @@ import sys
 
 import pytest
 
-from scout_apm.core.metadata import report_app_metadata
-from tests.compat import mock
+from scout_apm.core.metadata import get_python_packages_versions, report_app_metadata
+from tests.compat import SimpleNamespace, mock
 from tests.tools import pretend_package_unavailable
 
 
@@ -42,3 +42,20 @@ def test_report_app_metadata_no_importlib_metadata(send):
     message = command.message()
     assert message["ApplicationEvent"]["event_type"] == "scout.metadata"
     assert message["ApplicationEvent"]["event_value"]["libraries"] == []
+
+
+def test_get_python_packages_versions_None_package():
+    if sys.version_info >= (3, 8):
+        target = "importlib.metadata.distributions"
+    else:
+        target = "importlib_metadata.distributions"
+
+    def yielding_nones():
+        yield SimpleNamespace(metadata={"Name": "first-package", "Version": "1.0.0"})
+        yield SimpleNamespace(metadata={"Name": "second-package", "Version": None})
+        yield SimpleNamespace(metadata={"Name": None, "Version": "3.0.0"})
+
+    with mock.patch(target, new=yielding_nones):
+        packages = get_python_packages_versions()
+
+    assert packages == [("first-package", "1.0.0"), ("second-package", "Unknown")]