prompt: We are currently solving the following issue within our repository. Here is the issue text:
  --- BEGIN ISSUE ---
  capture_backtrace raises AttributeError on PEP-420 namespace packages
  The new `capture_backtrace` function in `scout_apm.core.backtrace` raises an AttributeError when the stack includes a [PEP-420] namespace package.
  
  This is caused by the [`module_filepath` function](https://github.com/scoutapp/scout_apm_python/blob/v2.21.0/src/scout_apm/core/backtrace.py#L26-L33), specifically line 32:
  
  ```python
      module_dir = sys.modules[root_module].__file__.rsplit(os.sep, 2)[0]
  ```
  
  If `sys.modules[root_module]` is a [PEP-420] namespace package, this will raise
  ```
  AttributeError: 'NoneType' object has no attribute 'rsplit'
  ```
  
  ### Steps to reproduce
  
  Create a namespace package, with some modules inside, e.g.:
  ```
  namespace/
      foo/
          __init__.py
      bar/
         __init__.py
  ```
  
  Then on an interactive Python shell:
  
  ```
  >>> from scout_apm.core.backtrace import module_filepath
  >>> from namespace import foo
  >>> module_filepath("namespace.foo", "namespace")
  Traceback (most recent call last):
    File "<stdin>", line 1, in <module>
    File "/home/jack/venvs/tmp-a17ac7185189989/lib/python3.8/site-packages/scout_apm/core/backtrace.py", line 32, in module_filepath
      module_dir = sys.modules[root_module].__file__.rsplit(os.sep, 2)[0]
  AttributeError: 'NoneType' object has no attribute 'rsplit'
  ```
  
  ### Details
  
  - Tested with version 2.21.0
  - Current workaround is to pin version to 2.20.0
  
  [PEP-420]: https://www.python.org/dev/peps/pep-0420/
  
  --- END ISSUE ---
  
  Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
  --- BEGIN FILES ---
  Path: `src/scout_apm/core/backtrace.py`
  Content:
  ```
  1 # coding=utf-8
  2 from __future__ import absolute_import, division, print_function, unicode_literals
  3 
  4 import itertools
  5 import os
  6 import sys
  7 import sysconfig
  8 import traceback
  9 import warnings
  10 
  11 # Maximum non-Scout frames to target retrieving
  12 LIMIT = 50
  13 # How many upper frames from inside Scout to ignore
  14 IGNORED = 1
  15 
  16 
  17 def filter_frames(frames):
  18     """Filter the stack trace frames down to non-library code."""
  19     paths = sysconfig.get_paths()
  20     library_paths = {paths["purelib"], paths["platlib"]}
  21     for frame in frames:
  22         if not any(frame["file"].startswith(exclusion) for exclusion in library_paths):
  23             yield frame
  24 
  25 
  26 def module_filepath(module, filepath):
  27     """Get the filepath relative to the base module."""
  28     root_module = module.split(".", 1)[0]
  29     if root_module == module:
  30         return os.path.basename(filepath)
  31 
  32     module_dir = sys.modules[root_module].__file__.rsplit(os.sep, 2)[0]
  33     return filepath.split(module_dir, 1)[-1].lstrip(os.sep)
  34 
  35 
  36 def filepath(frame):
  37     """Get the filepath for frame."""
  38     module = frame.f_globals.get("__name__", None)
  39     filepath = frame.f_code.co_filename
  40 
  41     if filepath.endswith(".pyc"):
  42         filepath = filepath[:-1]
  43 
  44     if not module:
  45         return filepath
  46     return module_filepath(module, filepath)
  47 
  48 
  49 if sys.version_info >= (3, 5):
  50 
  51     def stacktrace_walker(tb):
  52         """Iterate over each frame of the stack downards for exceptions."""
  53         for frame, lineno in traceback.walk_tb(tb):
  54             name = frame.f_code.co_name
  55             yield {"file": filepath(frame), "line": lineno, "function": name}
  56 
  57     def backtrace_walker():
  58         """Iterate over each frame of the stack upwards.
  59 
  60         Taken from python3/traceback.ExtractSummary.extract to support
  61         iterating over the entire stack, but without creating a large
  62         data structure.
  63         """
  64         start_frame = sys._getframe().f_back
  65         for frame, lineno in traceback.walk_stack(start_frame):
  66             name = frame.f_code.co_name
  67             yield {"file": filepath(frame), "line": lineno, "function": name}
  68 
  69 
  70 else:
  71 
  72     def stacktrace_walker(tb):
  73         """Iterate over each frame of the stack downards for exceptions."""
  74         while tb is not None:
  75             lineno = tb.tb_lineno
  76             name = tb.tb_frame.f_code.co_name
  77             yield {
  78                 "file": filepath(tb.tb_frame),
  79                 "line": lineno,
  80                 "function": name,
  81             }
  82             tb = tb.tb_next
  83 
  84     def backtrace_walker():
  85         """Iterate over each frame of the stack upwards.
  86 
  87         Taken from python2.7/traceback.extract_stack to support iterating
  88         over the entire stack, but without creating a large data structure.
  89         """
  90         try:
  91             raise ZeroDivisionError
  92         except ZeroDivisionError:
  93             # Get the current frame
  94             frame = sys.exc_info()[2].tb_frame.f_back
  95 
  96         while frame is not None:
  97             lineno = frame.f_lineno
  98             name = frame.f_code.co_name
  99             yield {"file": filepath(frame), "line": lineno, "function": name}
  100             frame = frame.f_back
  101 
  102 
  103 def capture_backtrace():
  104     walker = filter_frames(backtrace_walker())
  105     return list(itertools.islice(walker, LIMIT))
  106 
  107 
  108 def capture_stacktrace(tb):
  109     walker = stacktrace_walker(tb)
  110     return list(reversed(list(itertools.islice(walker, LIMIT))))
  111 
  112 
  113 def capture():
  114     warnings.warn(
  115         "capture is deprecated, instead use capture_backtrace instead.",
  116         DeprecationWarning,
  117         2,
  118     )
  119     return capture_backtrace()
  120
  ```
  --- END FILES ---
  
  Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.
  
  Here is an example:
  
  ```diff
  diff --git a/examples/server_async.py b/examples/server_async.py
  --- a/examples/server_async.py
  +++ b/examples/server_async.py
  @@ -313,4 +313,4 @@
   
   
   if __name__ == "__main__":
  -    asyncio.run(run_async_server("."), debug=True)
  +    asyncio.run(run_async_server(), debug=True)
  diff --git a/examples/server_sync.py b/examples/server_sync.py
  --- a/examples/server_sync.py
  +++ b/examples/server_sync.py
  @@ -313,5 +313,5 @@
   
   
   if __name__ == "__main__":
  -    server = run_sync_server(".")
  +    server = run_sync_server()
       server.shutdown()
  
  ```
  
golden_diff: diff --git a/src/scout_apm/core/backtrace.py b/src/scout_apm/core/backtrace.py
  --- a/src/scout_apm/core/backtrace.py
  +++ b/src/scout_apm/core/backtrace.py
  @@ -7,6 +7,9 @@
   import sysconfig
   import traceback
   import warnings
  +from logging import getLogger
  +
  +logger = getLogger(__name__)
   
   # Maximum non-Scout frames to target retrieving
   LIMIT = 50
  @@ -25,11 +28,25 @@
   
   def module_filepath(module, filepath):
       """Get the filepath relative to the base module."""
  -    root_module = module.split(".", 1)[0]
  -    if root_module == module:
  +    root_module_name = module.split(".", 1)[0]
  +    if root_module_name == module:
           return os.path.basename(filepath)
   
  -    module_dir = sys.modules[root_module].__file__.rsplit(os.sep, 2)[0]
  +    root_module = sys.modules[root_module_name]
  +    if root_module.__file__:
  +        module_dir = root_module.__file__.rsplit(os.sep, 2)[0]
  +    elif root_module.__path__:
  +        # Default to using the first path specified for the module.
  +        module_dir = root_module.__path__[0].rsplit(os.sep, 1)[0]
  +        if len(root_module.__path__) > 1:
  +            logger.debug(
  +                "{} has {} paths. Use the first and ignore the rest.".format(
  +                    root_module, len(root_module.__path__)
  +                )
  +            )
  +    else:
  +        # If the file path don't exist, then return the full path.
  +        return filepath
       return filepath.split(module_dir, 1)[-1].lstrip(os.sep)
  
verification_info: {"golden_diff": "diff --git a/src/scout_apm/core/backtrace.py b/src/scout_apm/core/backtrace.py\n--- a/src/scout_apm/core/backtrace.py\n+++ b/src/scout_apm/core/backtrace.py\n@@ -7,6 +7,9 @@\n import sysconfig\n import traceback\n import warnings\n+from logging import getLogger\n+\n+logger = getLogger(__name__)\n \n # Maximum non-Scout frames to target retrieving\n LIMIT = 50\n@@ -25,11 +28,25 @@\n \n def module_filepath(module, filepath):\n     \"\"\"Get the filepath relative to the base module.\"\"\"\n-    root_module = module.split(\".\", 1)[0]\n-    if root_module == module:\n+    root_module_name = module.split(\".\", 1)[0]\n+    if root_module_name == module:\n         return os.path.basename(filepath)\n \n-    module_dir = sys.modules[root_module].__file__.rsplit(os.sep, 2)[0]\n+    root_module = sys.modules[root_module_name]\n+    if root_module.__file__:\n+        module_dir = root_module.__file__.rsplit(os.sep, 2)[0]\n+    elif root_module.__path__:\n+        # Default to using the first path specified for the module.\n+        module_dir = root_module.__path__[0].rsplit(os.sep, 1)[0]\n+        if len(root_module.__path__) > 1:\n+            logger.debug(\n+                \"{} has {} paths. Use the first and ignore the rest.\".format(\n+                    root_module, len(root_module.__path__)\n+                )\n+            )\n+    else:\n+        # If the file path don't exist, then return the full path.\n+        return filepath\n     return filepath.split(module_dir, 1)[-1].lstrip(os.sep)\n", "issue": "capture_backtrace raises AttributeError on PEP-420 namespace packages\nThe new `capture_backtrace` function in `scout_apm.core.backtrace` raises an AttributeError when the stack includes a [PEP-420] namespace package.\r\n\r\nThis is caused by the [`module_filepath` function](https://github.com/scoutapp/scout_apm_python/blob/v2.21.0/src/scout_apm/core/backtrace.py#L26-L33), specifically line 32:\r\n\r\n```python\r\n    module_dir = sys.modules[root_module].__file__.rsplit(os.sep, 2)[0]\r\n```\r\n\r\nIf `sys.modules[root_module]` is a [PEP-420] namespace package, this will raise\r\n```\r\nAttributeError: 'NoneType' object has no attribute 'rsplit'\r\n```\r\n\r\n### Steps to reproduce\r\n\r\nCreate a namespace package, with some modules inside, e.g.:\r\n```\r\nnamespace/\r\n    foo/\r\n        __init__.py\r\n    bar/\r\n       __init__.py\r\n```\r\n\r\nThen on an interactive Python shell:\r\n\r\n```\r\n>>> from scout_apm.core.backtrace import module_filepath\r\n>>> from namespace import foo\r\n>>> module_filepath(\"namespace.foo\", \"namespace\")\r\nTraceback (most recent call last):\r\n  File \"<stdin>\", line 1, in <module>\r\n  File \"/home/jack/venvs/tmp-a17ac7185189989/lib/python3.8/site-packages/scout_apm/core/backtrace.py\", line 32, in module_filepath\r\n    module_dir = sys.modules[root_module].__file__.rsplit(os.sep, 2)[0]\r\nAttributeError: 'NoneType' object has no attribute 'rsplit'\r\n```\r\n\r\n### Details\r\n\r\n- Tested with version 2.21.0\r\n- Current workaround is to pin version to 2.20.0\r\n\r\n[PEP-420]: https://www.python.org/dev/peps/pep-0420/\n", "before_files": [{"content": "# coding=utf-8\nfrom __future__ import absolute_import, division, print_function, unicode_literals\n\nimport itertools\nimport os\nimport sys\nimport sysconfig\nimport traceback\nimport warnings\n\n# Maximum non-Scout frames to target retrieving\nLIMIT = 50\n# How many upper frames from inside Scout to ignore\nIGNORED = 1\n\n\ndef filter_frames(frames):\n    \"\"\"Filter the stack trace frames down to non-library code.\"\"\"\n    paths = sysconfig.get_paths()\n    library_paths = {paths[\"purelib\"], paths[\"platlib\"]}\n    for frame in frames:\n        if not any(frame[\"file\"].startswith(exclusion) for exclusion in library_paths):\n            yield frame\n\n\ndef module_filepath(module, filepath):\n    \"\"\"Get the filepath relative to the base module.\"\"\"\n    root_module = module.split(\".\", 1)[0]\n    if root_module == module:\n        return os.path.basename(filepath)\n\n    module_dir = sys.modules[root_module].__file__.rsplit(os.sep, 2)[0]\n    return filepath.split(module_dir, 1)[-1].lstrip(os.sep)\n\n\ndef filepath(frame):\n    \"\"\"Get the filepath for frame.\"\"\"\n    module = frame.f_globals.get(\"__name__\", None)\n    filepath = frame.f_code.co_filename\n\n    if filepath.endswith(\".pyc\"):\n        filepath = filepath[:-1]\n\n    if not module:\n        return filepath\n    return module_filepath(module, filepath)\n\n\nif sys.version_info >= (3, 5):\n\n    def stacktrace_walker(tb):\n        \"\"\"Iterate over each frame of the stack downards for exceptions.\"\"\"\n        for frame, lineno in traceback.walk_tb(tb):\n            name = frame.f_code.co_name\n            yield {\"file\": filepath(frame), \"line\": lineno, \"function\": name}\n\n    def backtrace_walker():\n        \"\"\"Iterate over each frame of the stack upwards.\n\n        Taken from python3/traceback.ExtractSummary.extract to support\n        iterating over the entire stack, but without creating a large\n        data structure.\n        \"\"\"\n        start_frame = sys._getframe().f_back\n        for frame, lineno in traceback.walk_stack(start_frame):\n            name = frame.f_code.co_name\n            yield {\"file\": filepath(frame), \"line\": lineno, \"function\": name}\n\n\nelse:\n\n    def stacktrace_walker(tb):\n        \"\"\"Iterate over each frame of the stack downards for exceptions.\"\"\"\n        while tb is not None:\n            lineno = tb.tb_lineno\n            name = tb.tb_frame.f_code.co_name\n            yield {\n                \"file\": filepath(tb.tb_frame),\n                \"line\": lineno,\n                \"function\": name,\n            }\n            tb = tb.tb_next\n\n    def backtrace_walker():\n        \"\"\"Iterate over each frame of the stack upwards.\n\n        Taken from python2.7/traceback.extract_stack to support iterating\n        over the entire stack, but without creating a large data structure.\n        \"\"\"\n        try:\n            raise ZeroDivisionError\n        except ZeroDivisionError:\n            # Get the current frame\n            frame = sys.exc_info()[2].tb_frame.f_back\n\n        while frame is not None:\n            lineno = frame.f_lineno\n            name = frame.f_code.co_name\n            yield {\"file\": filepath(frame), \"line\": lineno, \"function\": name}\n            frame = frame.f_back\n\n\ndef capture_backtrace():\n    walker = filter_frames(backtrace_walker())\n    return list(itertools.islice(walker, LIMIT))\n\n\ndef capture_stacktrace(tb):\n    walker = stacktrace_walker(tb)\n    return list(reversed(list(itertools.islice(walker, LIMIT))))\n\n\ndef capture():\n    warnings.warn(\n        \"capture is deprecated, instead use capture_backtrace instead.\",\n        DeprecationWarning,\n        2,\n    )\n    return capture_backtrace()\n", "path": "src/scout_apm/core/backtrace.py"}], "after_files": [{"content": "# coding=utf-8\nfrom __future__ import absolute_import, division, print_function, unicode_literals\n\nimport itertools\nimport os\nimport sys\nimport sysconfig\nimport traceback\nimport warnings\nfrom logging import getLogger\n\nlogger = getLogger(__name__)\n\n# Maximum non-Scout frames to target retrieving\nLIMIT = 50\n# How many upper frames from inside Scout to ignore\nIGNORED = 1\n\n\ndef filter_frames(frames):\n    \"\"\"Filter the stack trace frames down to non-library code.\"\"\"\n    paths = sysconfig.get_paths()\n    library_paths = {paths[\"purelib\"], paths[\"platlib\"]}\n    for frame in frames:\n        if not any(frame[\"file\"].startswith(exclusion) for exclusion in library_paths):\n            yield frame\n\n\ndef module_filepath(module, filepath):\n    \"\"\"Get the filepath relative to the base module.\"\"\"\n    root_module_name = module.split(\".\", 1)[0]\n    if root_module_name == module:\n        return os.path.basename(filepath)\n\n    root_module = sys.modules[root_module_name]\n    if root_module.__file__:\n        module_dir = root_module.__file__.rsplit(os.sep, 2)[0]\n    elif root_module.__path__:\n        # Default to using the first path specified for the module.\n        module_dir = root_module.__path__[0].rsplit(os.sep, 1)[0]\n        if len(root_module.__path__) > 1:\n            logger.debug(\n                \"{} has {} paths. Use the first and ignore the rest.\".format(\n                    root_module, len(root_module.__path__)\n                )\n            )\n    else:\n        # If the file path don't exist, then return the full path.\n        return filepath\n    return filepath.split(module_dir, 1)[-1].lstrip(os.sep)\n\n\ndef filepath(frame):\n    \"\"\"Get the filepath for frame.\"\"\"\n    module = frame.f_globals.get(\"__name__\", None)\n    filepath = frame.f_code.co_filename\n\n    if filepath.endswith(\".pyc\"):\n        filepath = filepath[:-1]\n\n    if not module:\n        return filepath\n    return module_filepath(module, filepath)\n\n\nif sys.version_info >= (3, 5):\n\n    def stacktrace_walker(tb):\n        \"\"\"Iterate over each frame of the stack downards for exceptions.\"\"\"\n        for frame, lineno in traceback.walk_tb(tb):\n            name = frame.f_code.co_name\n            yield {\"file\": filepath(frame), \"line\": lineno, \"function\": name}\n\n    def backtrace_walker():\n        \"\"\"Iterate over each frame of the stack upwards.\n\n        Taken from python3/traceback.ExtractSummary.extract to support\n        iterating over the entire stack, but without creating a large\n        data structure.\n        \"\"\"\n        start_frame = sys._getframe().f_back\n        for frame, lineno in traceback.walk_stack(start_frame):\n            name = frame.f_code.co_name\n            yield {\"file\": filepath(frame), \"line\": lineno, \"function\": name}\n\n\nelse:\n\n    def stacktrace_walker(tb):\n        \"\"\"Iterate over each frame of the stack downards for exceptions.\"\"\"\n        while tb is not None:\n            lineno = tb.tb_lineno\n            name = tb.tb_frame.f_code.co_name\n            yield {\n                \"file\": filepath(tb.tb_frame),\n                \"line\": lineno,\n                \"function\": name,\n            }\n            tb = tb.tb_next\n\n    def backtrace_walker():\n        \"\"\"Iterate over each frame of the stack upwards.\n\n        Taken from python2.7/traceback.extract_stack to support iterating\n        over the entire stack, but without creating a large data structure.\n        \"\"\"\n        try:\n            raise ZeroDivisionError\n        except ZeroDivisionError:\n            # Get the current frame\n            frame = sys.exc_info()[2].tb_frame.f_back\n\n        while frame is not None:\n            lineno = frame.f_lineno\n            name = frame.f_code.co_name\n            yield {\"file\": filepath(frame), \"line\": lineno, \"function\": name}\n            frame = frame.f_back\n\n\ndef capture_backtrace():\n    walker = filter_frames(backtrace_walker())\n    return list(itertools.islice(walker, LIMIT))\n\n\ndef capture_stacktrace(tb):\n    walker = stacktrace_walker(tb)\n    return list(reversed(list(itertools.islice(walker, LIMIT))))\n\n\ndef capture():\n    warnings.warn(\n        \"capture is deprecated, instead use capture_backtrace instead.\",\n        DeprecationWarning,\n        2,\n    )\n    return capture_backtrace()\n", "path": "src/scout_apm/core/backtrace.py"}]}