Repository: rollbar/pyrollbar

Issue:
Getting 'PosixPath' object has no attribute 'lower' with django
I configured integration with django following the docs, however when an exception is raise somewhere in the app I get in addition

```
django_1  | During handling of the above exception, another exception occurred:
django_1  | 
django_1  | Traceback (most recent call last):
django_1  |   File "/usr/local/lib/python3.8/site-packages/rollbar/__init__.py", line 944, in _add_locals_data
django_1  |     if arginfo.locals and _check_add_locals(cur_frame, frame_num, num_frames):
django_1  |   File "/usr/local/lib/python3.8/site-packages/rollbar/__init__.py", line 1047, in _check_add_locals
django_1  |     ('root' in SETTINGS and (frame.get('filename') or '').lower().startswith((SETTINGS['root'] or '').lower()))))
django_1  | AttributeError: 'PosixPath' object has no attribute 'lower'
```

Is there a way to get rid of that?

Discussion:

Patch:
diff --git a/rollbar/__init__.py b/rollbar/__init__.py
index 42b8d5e..71b3a82 100644
--- a/rollbar/__init__.py
+++ b/rollbar/__init__.py
@@ -1118,8 +1118,14 @@ def _check_add_locals(frame, frame_num, total_frames):
     """
     # Include the last frames locals
     # Include any frame locals that came from a file in the project's root
+    root = SETTINGS.get('root')
+    if root:
+        # coerce to string, in case root is a Path object
+        root = str(root)
+    else:
+        root = ''
     return any(((frame_num == total_frames - 1),
-                ('root' in SETTINGS and (frame.get('filename') or '').lower().startswith((SETTINGS['root'] or '').lower()))))
+                ('root' in SETTINGS and (frame.get('filename') or '').lower().startswith(root.lower()))))
 
 
 def _get_actual_request(request):
diff --git a/rollbar/lib/transform.py b/rollbar/lib/transform.py
index 2ba7a03..49391ec 100644
--- a/rollbar/lib/transform.py
+++ b/rollbar/lib/transform.py
@@ -32,5 +32,8 @@ class Transform(object):
     def transform_boolean(self, o, key=None):
         return self.default(o, key=key)
 
+    def transform_path(self, o, key=None):
+        return self.default(str(o), key=key)
+
     def transform_custom(self, o, key=None):
         return self.default(o, key=key)
diff --git a/rollbar/lib/transforms/__init__.py b/rollbar/lib/transforms/__init__.py
index 3f1b880..6b4c59c 100644
--- a/rollbar/lib/transforms/__init__.py
+++ b/rollbar/lib/transforms/__init__.py
@@ -79,6 +79,7 @@ def _transform(obj, transform, key=None):
         "list_handler": lambda o, key=None: do_transform("list", o, key=key),
         "set_handler": lambda o, key=None: do_transform("set", o, key=key),
         "mapping_handler": lambda o, key=None: do_transform("dict", o, key=key),
+        "path_handler": lambda o, key=None: do_transform("path", o, key=key),
         "circular_reference_handler": lambda o, key=None, ref_key=None: do_transform(
             "circular_reference", o, key=key, ref_key=ref_key
         ),
diff --git a/rollbar/lib/traverse.py b/rollbar/lib/traverse.py
index 6a613da..de00d5b 100644
--- a/rollbar/lib/traverse.py
+++ b/rollbar/lib/traverse.py
@@ -1,5 +1,5 @@
 import logging
-
+from pathlib import Path
 
 from rollbar.lib import binary_type, string_types, circular_reference_label
 
@@ -16,6 +16,7 @@ from rollbar.lib.type_info import (
     LIST,
     SET,
     STRING,
+    PATH,
 )
 
 
@@ -49,6 +50,9 @@ def _noop_set(a, **_):
 def _noop_mapping(a, **_):
     return dict(a)
 
+def _noop_path(a, **_):
+    return Path(a)
+
 
 _default_handlers = {
     CIRCULAR: _noop_circular,
@@ -58,6 +62,7 @@ _default_handlers = {
     NAMEDTUPLE: _noop_namedtuple,
     LIST: _noop_list,
     SET: _noop_set,
+    PATH: _noop_path,
     MAPPING: _noop_mapping,
 }
 
@@ -71,6 +76,7 @@ def traverse(
     list_handler=_default_handlers[LIST],
     set_handler=_default_handlers[SET],
     mapping_handler=_default_handlers[MAPPING],
+    path_handler=_default_handlers[PATH],
     default_handler=_default_handlers[DEFAULT],
     circular_reference_handler=_default_handlers[CIRCULAR],
     allowed_circular_reference_types=None,
@@ -97,6 +103,7 @@ def traverse(
         "list_handler": list_handler,
         "set_handler": set_handler,
         "mapping_handler": mapping_handler,
+        "path_handler": path_handler,
         "default_handler": default_handler,
         "circular_reference_handler": circular_reference_handler,
         "allowed_circular_reference_types": allowed_circular_reference_types,
@@ -137,6 +144,8 @@ def traverse(
                 {k: traverse(v, key=key + (k,), **kw) for k, v in obj.items()},
                 key=key,
             )
+        elif obj_type is PATH:
+            return path_handler(obj, key=key)
         elif obj_type is DEFAULT:
             for handler_type, handler in custom_handlers.items():
                 if isinstance(obj, handler_type):
diff --git a/rollbar/lib/type_info.py b/rollbar/lib/type_info.py
index 7cf0af5..9d1bd23 100644
--- a/rollbar/lib/type_info.py
+++ b/rollbar/lib/type_info.py
@@ -2,6 +2,7 @@ from rollbar.lib import binary_type, string_types
 
 
 from collections.abc import Mapping, Sequence, Set
+from pathlib import Path
 
 
 CIRCULAR = -1
@@ -12,6 +13,7 @@ NAMEDTUPLE = 3
 LIST = 4
 SET = 5
 STRING = 6
+PATH = 7
 
 
 def get_type(obj):
@@ -33,6 +35,9 @@ def get_type(obj):
     if isinstance(obj, Sequence):
         return LIST
 
+    if isinstance(obj, Path):
+        return PATH
+
     return DEFAULT
 
 
@@ -45,5 +50,6 @@ __all__ = [
     "LIST",
     "SET",
     "STRING",
+    "PATH",
     "get_type",
 ]


Test patch:
diff --git a/rollbar/test/test_rollbar.py b/rollbar/test/test_rollbar.py
index 3107e9e..ba44885 100644
--- a/rollbar/test/test_rollbar.py
+++ b/rollbar/test/test_rollbar.py
@@ -12,6 +12,8 @@ try:
 except ImportError:
     from io import StringIO
 
+from pathlib import Path
+
 from unittest import mock
 
 import unittest
@@ -1866,6 +1868,23 @@ class RollbarTest(BaseTest):
         request = Request(scope)
         user_ip = rollbar._starlette_extract_user_ip(request)
         self.assertEqual(user_ip, ip_forwarded_for.decode())
+    
+    @mock.patch('rollbar.send_payload')
+    def test_root_path(self, send_payload):
+        prev_root = rollbar.SETTINGS['root']
+        rollbar.SETTINGS['root'] = Path("/tmp")
+        try:
+            called_with('original value')
+        except:
+            rollbar.report_exc_info()
+        finally:
+            rollbar.SETTINGS['root'] = prev_root
+
+        self.assertEqual(send_payload.called, True)
+
+        payload = send_payload.call_args[0][0]
+        self.assertEqual(payload['data']['server']['root'], "/tmp")
+
 
 ### Helpers