Repository: ramonhagenaars/jsons

Issue:
Add support for pathlib.Path objects
The current `default_object_deserializer` does not support `pathlib.Path` objects. When trying to deserialise the string `data/gqa` to a `pathlib.Path` object, the default `default_object_deserializer` tries to access attributes of the string `data/gqa`, lesding to the following error in a dictionary comprehension:

```
Traceback (most recent call last):
  File "/home/alex/.virtualenvs/python-standard/lib/python3.8/site-packages/jsons/_load_impl.py", line 110, in _do_load
    result = deserializer(json_obj, cls, **kwargs)
  File "/home/alex/.virtualenvs/python-standard/lib/python3.8/site-packages/jsons/deserializers/default_object.py", line 40, in default_object_deserializer
    remaining_attrs = _get_remaining_args(obj, cls, constructor_args,
  File "/home/alex/.virtualenvs/python-standard/lib/python3.8/site-packages/jsons/deserializers/default_object.py", line 174, in _get_remaining_args
    remaining_attrs = {attr_name: obj[attr_name] for attr_name in obj
  File "/home/alex/.virtualenvs/python-standard/lib/python3.8/site-packages/jsons/deserializers/default_object.py", line 174, in <dictcomp>
    remaining_attrs = {attr_name: obj[attr_name] for attr_name in obj
TypeError: string indices must be integers
```

I would like to propose a default serialiser and deserialiser for `pathlib.Path` objects that allows for conversion between `str` and `pathlib.Path`. Whether to support a list of path segments like `["data", "gqa"]` is up for discussion.

Comments:

Patch:
diff --git a/jsons/__init__.py b/jsons/__init__.py
index 188961e..14168e8 100644
--- a/jsons/__init__.py
+++ b/jsons/__init__.py
@@ -91,6 +91,7 @@ from enum import Enum
 from typing import Union, List, Tuple, Iterable
 from uuid import UUID
 from decimal import Decimal
+from pathlib import PurePath
 from jsons._common_impl import NoneType
 from jsons._key_transformers import snakecase, camelcase, pascalcase, lispcase
 from jsons import (
@@ -160,6 +161,7 @@ default_object_serializer = serializers.default_object_serializer
 default_decimal_serializer = serializers.default_decimal_serializer
 default_uuid_serializer = serializers.default_uuid_serializer
 default_union_serializer = serializers.default_union_serializer
+default_path_serializer = serializers.default_path_serializer
 
 default_list_deserializer = deserializers.default_list_deserializer
 default_tuple_deserializer = deserializers.default_tuple_deserializer
@@ -180,6 +182,7 @@ default_iterable_deserializer = deserializers.default_iterable_deserializer
 default_object_deserializer = deserializers.default_object_deserializer
 default_uuid_deserializer = deserializers.default_uuid_deserializer
 default_decimal_deserializer = deserializers.default_decimal_deserializer
+default_path_deserializer = deserializers.default_path_deserializer
 
 # Set the serializers:
 set_serializer(default_tuple_serializer, (tuple, Tuple))
@@ -197,6 +200,7 @@ set_serializer(default_object_serializer, object, False)
 set_serializer(default_uuid_serializer, UUID)
 set_serializer(default_decimal_serializer, Decimal)
 set_serializer(default_union_serializer, Union)
+set_serializer(default_path_serializer, PurePath)
 
 # Set the deserializers:
 set_deserializer(default_list_deserializer, (list, List))
@@ -217,3 +221,4 @@ set_deserializer(default_object_deserializer, object, False)
 set_deserializer(default_uuid_deserializer, UUID)
 set_deserializer(default_complex_deserializer, complex)
 set_deserializer(default_decimal_deserializer, Decimal)
+set_deserializer(default_path_deserializer, PurePath)
diff --git a/jsons/deserializers/__init__.py b/jsons/deserializers/__init__.py
index 557e7a7..e5a78a8 100644
--- a/jsons/deserializers/__init__.py
+++ b/jsons/deserializers/__init__.py
@@ -29,6 +29,7 @@ from jsons.deserializers.default_tuple import (
     default_namedtuple_deserializer
 )
 from jsons.deserializers.default_decimal import default_decimal_deserializer
+from jsons.deserializers.default_path import default_path_deserializer
 
 
 KEY_TRANSFORMER_SNAKECASE = snakecase
diff --git a/jsons/deserializers/default_path.py b/jsons/deserializers/default_path.py
new file mode 100644
index 0000000..76db0df
--- /dev/null
+++ b/jsons/deserializers/default_path.py
@@ -0,0 +1,12 @@
+from pathlib import PurePath
+
+
+def default_path_deserializer(obj: str, cls: type = PurePath, **kwargs) -> PurePath:
+    """
+    Deserialize a string to a `pathlib.PurePath` object. Since ``pathlib``
+    implements ``PurePath``, no filename or existence checks are performed.
+    :param obj: the string to deserialize.
+    :param kwargs: not used.
+    :return: a ``str``.
+    """
+    return cls(obj)
diff --git a/jsons/serializers/__init__.py b/jsons/serializers/__init__.py
index 64433c6..6aa3500 100644
--- a/jsons/serializers/__init__.py
+++ b/jsons/serializers/__init__.py
@@ -20,6 +20,7 @@ from jsons.serializers.default_tuple import default_tuple_serializer
 from jsons.serializers.default_uuid import default_uuid_serializer
 from jsons.serializers.default_decimal import default_decimal_serializer
 from jsons.serializers.default_union import default_union_serializer
+from jsons.serializers.default_path import default_path_serializer
 
 
 KEY_TRANSFORMER_SNAKECASE = snakecase
diff --git a/jsons/serializers/default_path.py b/jsons/serializers/default_path.py
new file mode 100644
index 0000000..205d55d
--- /dev/null
+++ b/jsons/serializers/default_path.py
@@ -0,0 +1,15 @@
+from pathlib import PurePath
+
+
+def default_path_serializer(obj: PurePath, **kwargs) -> str:
+    """
+    Serialize a ``pathlib.PurePath`` object to a ``str``, Posix-style.
+
+    Posix-style strings are used as they can be used to create ``pathlib.Path``
+    objects on both Posix and Windows systems, but Windows-style strings can
+    only be used to create valid ``pathlib.Path`` objects on Windows.
+    :param obj: the path to serialize.
+    :param kwargs: not used.
+    :return: a ``str``.
+    """
+    return obj.as_posix()


Test patch:
diff --git a/tests/test_path.py b/tests/test_path.py
new file mode 100644
index 0000000..204235a
--- /dev/null
+++ b/tests/test_path.py
@@ -0,0 +1,273 @@
+import os.path
+from pathlib import Path, PureWindowsPath, PurePosixPath
+from unittest import TestCase
+
+import jsons
+
+
+class TestPath(TestCase):
+
+    def test_dump_singlepart_relative_path(self):
+        self.assertEqual('abc', jsons.dump(Path('abc')))
+
+    def test_dump_singlepart_pure_windows_path(self):
+        self.assertEqual('abc', jsons.dump(PureWindowsPath('abc')))
+
+    def test_dump_singlepart_pure_posix_path(self):
+        self.assertEqual('abc', jsons.dump(PurePosixPath('abc')))
+
+    def test_dump_multipart_relative_path(self):
+        self.assertEqual(
+            'abc/def/ghi',
+            jsons.dump(Path('abc', 'def', 'ghi'))
+        )
+        self.assertEqual(
+            'abc/def/ghi',
+            jsons.dump(Path('abc/def/ghi'))
+        )
+
+    def test_dump_multipart_pure_windows_path(self):
+        self.assertEqual(
+            'abc/def/ghi',
+            jsons.dump(PureWindowsPath('abc', 'def', 'ghi'))
+        )
+        self.assertEqual(
+            'abc/def/ghi',
+            jsons.dump(PureWindowsPath('abc/def/ghi'))
+        )
+        self.assertEqual(
+            'abc/def/ghi',
+            jsons.dump(PureWindowsPath('abc\\def\\ghi'))
+        )
+
+    def test_dump_multipart_pure_posix_path(self):
+        self.assertEqual(
+            'abc/def/ghi',
+            jsons.dump(PurePosixPath('abc', 'def', 'ghi'))
+        )
+        self.assertEqual(
+            'abc/def/ghi',
+            jsons.dump(PurePosixPath('abc/def/ghi'))
+        )
+        self.assertEqual(
+            'abc\\def\\ghi',
+            jsons.dump(PurePosixPath('abc\\def\\ghi'))
+        )
+
+    def test_dump_multipart_drived_pure_windows_path(self):
+        self.assertEqual(
+            'Z:/abc/def/ghi',
+            jsons.dump(PureWindowsPath('Z:\\', 'abc', 'def', 'ghi'))
+        )
+        self.assertEqual(
+            'Z:/abc/def/ghi',
+            jsons.dump(PureWindowsPath('Z:/abc/def/ghi'))
+        )
+        self.assertEqual(
+            'Z:/abc/def/ghi',
+            jsons.dump(PureWindowsPath('Z:\\abc\\def\\ghi'))
+        )
+
+    def test_dump_multipart_drived_pure_posix_path(self):
+        self.assertEqual(
+            'Z:/abc/def/ghi',
+            jsons.dump(PurePosixPath('Z:', 'abc', 'def', 'ghi'))
+        )
+        self.assertEqual(
+            'Z:/abc/def/ghi',
+            jsons.dump(PurePosixPath('Z:/abc/def/ghi'))
+        )
+        self.assertEqual(
+            'Z:\\abc\\def\\ghi',
+            jsons.dump(PurePosixPath('Z:\\abc\\def\\ghi'))
+        )
+
+    #################
+
+    def test_load_singlepart_relative_path(self):
+        self.assertEqual(
+            Path('abc'),
+            jsons.load('abc', Path)
+        )
+
+    def test_load_singlepart_pure_windows_path(self):
+        self.assertEqual(
+            PureWindowsPath('abc'),
+            jsons.load('abc', PureWindowsPath)
+        )
+
+    def test_load_singlepart_pure_posix_path(self):
+        self.assertEqual(
+            PurePosixPath('abc'),
+            jsons.load('abc', PurePosixPath)
+        )
+
+    def test_load_multipart_relative_path(self):
+        self.assertEqual(
+            Path('abc', 'def', 'ghi'),
+            jsons.load('abc/def/ghi', Path)
+        )
+        self.assertEqual(
+            Path('abc/def/ghi'),
+            jsons.load('abc/def/ghi', Path)
+        )
+
+    def test_load_multipart_pure_windows_path(self):
+        # We should be able to load Posix-style paths on Windows.
+        self.assertEqual(
+            PureWindowsPath('abc', 'def', 'ghi'),
+            jsons.load('abc/def/ghi', PureWindowsPath)
+        )
+        self.assertEqual(
+            PureWindowsPath('abc/def/ghi'),
+            jsons.load('abc/def/ghi', PureWindowsPath)
+        )
+        self.assertEqual(
+            PureWindowsPath('abc\\def\\ghi'),
+            jsons.load('abc/def/ghi', PureWindowsPath)
+        )
+        # We should be able to load Windows-style paths on Windows.
+        self.assertEqual(
+            PureWindowsPath('abc', 'def', 'ghi'),
+            jsons.load('abc\\def\\ghi', PureWindowsPath)
+        )
+        self.assertEqual(
+            PureWindowsPath('abc/def/ghi'),
+            jsons.load('abc\\def\\ghi', PureWindowsPath)
+        )
+        self.assertEqual(
+            PureWindowsPath('abc\\def\\ghi'),
+            jsons.load('abc\\def\\ghi', PureWindowsPath)
+        )
+
+    def test_load_multipart_pure_posix_path(self):
+        # We should be able to load Posix-style paths on Posix systems.
+        self.assertEqual(
+            PurePosixPath('abc', 'def', 'ghi'),
+            jsons.load('abc/def/ghi', PurePosixPath)
+        )
+        self.assertEqual(
+            PurePosixPath('abc/def/ghi'),
+            jsons.load('abc/def/ghi', PurePosixPath)
+        )
+        self.assertNotEqual(
+            PurePosixPath('abc\\def\\ghi'),
+            jsons.load('abc/def/ghi', PurePosixPath)
+        )
+        # Backslashes on Posix systems should be interpreted as escapes.
+        self.assertNotEqual(
+            PurePosixPath('abc', 'def', 'ghi'),
+            jsons.load('abc\\def\\ghi', PurePosixPath)
+        )
+        self.assertNotEqual(
+            PurePosixPath('abc/def/ghi'),
+            jsons.load('abc\\def\\ghi', PurePosixPath)
+        )
+        self.assertEqual(
+            PurePosixPath('abc\\def\\ghi'),
+            jsons.load('abc\\def\\ghi', PurePosixPath)
+        )
+
+    def test_load_multipart_drived_pure_windows_path(self):
+        # We should be able to load Posix-style paths on Windows.
+        self.assertEqual(
+            PureWindowsPath('Z:\\', 'abc', 'def', 'ghi'),
+            jsons.load('Z:/abc/def/ghi', PureWindowsPath)
+        )
+        self.assertEqual(
+            PureWindowsPath('Z:/abc/def/ghi'),
+            jsons.load('Z:/abc/def/ghi', PureWindowsPath)
+        )
+        self.assertEqual(
+            PureWindowsPath('Z:\\abc\\def\\ghi'),
+            jsons.load('Z:/abc/def/ghi', PureWindowsPath)
+        )
+        # We should be able to load Windows-style paths on Windows.
+        self.assertEqual(
+            PureWindowsPath('Z:\\', 'abc', 'def', 'ghi'),
+            jsons.load('Z:\\abc\\def\\ghi', PureWindowsPath)
+        )
+        self.assertEqual(
+            PureWindowsPath('Z:/abc/def/ghi'),
+            jsons.load('Z:\\abc\\def\\ghi', PureWindowsPath)
+        )
+        self.assertEqual(
+            PureWindowsPath('Z:\\abc\\def\\ghi'),
+            jsons.load('Z:\\abc\\def\\ghi', PureWindowsPath)
+        )
+
+    def test_load_multipart_drived_pure_posix_path(self):
+        # We should be able to load Posix-style paths on Windows.
+        self.assertEqual(
+            PurePosixPath('Z:', 'abc', 'def', 'ghi'),
+            jsons.load('Z:/abc/def/ghi', PurePosixPath)
+        )
+        self.assertEqual(
+            PurePosixPath('Z:/abc/def/ghi'),
+            jsons.load('Z:/abc/def/ghi', PurePosixPath)
+        )
+        self.assertNotEqual(
+            PurePosixPath('Z:\\abc\\def\\ghi'),
+            jsons.load('Z:/abc/def/ghi', PurePosixPath)
+        )
+        # Backslashes on Posix systems should be interpreted as escapes.
+        self.assertNotEqual(
+            PurePosixPath('Z:', 'abc', 'def', 'ghi'),
+            jsons.load('Z:\\abc\\def\\ghi', PurePosixPath)
+        )
+        self.assertNotEqual(
+            PurePosixPath('Z:/abc/def/ghi'),
+            jsons.load('Z:\\abc\\def\\ghi', PurePosixPath)
+        )
+        self.assertEqual(
+            PurePosixPath('Z:\\abc\\def\\ghi'),
+            jsons.load('Z:\\abc\\def\\ghi', PurePosixPath)
+        )
+
+    def test_dump_posix_load_windows(self):
+        dump_result = jsons.dump(PurePosixPath('abc', 'def', 'ghi'))
+        self.assertEqual(
+            'abc/def/ghi',
+            dump_result
+        )
+        load_result = jsons.load(dump_result, PureWindowsPath)
+        self.assertEqual(
+            PureWindowsPath('abc', 'def', 'ghi'),
+            load_result
+        )
+
+    def test_dump_windows_load_posix(self):
+        dump_result = jsons.dump(PureWindowsPath('abc', 'def', 'ghi'))
+        self.assertEqual(
+            'abc/def/ghi',
+            dump_result
+        )
+        load_result = jsons.load(dump_result, PurePosixPath)
+        self.assertEqual(
+            PurePosixPath('abc', 'def', 'ghi'),
+            load_result
+        )
+
+    def test_dump_posix_load_posix(self):
+        dump_result = jsons.dump(PurePosixPath('abc', 'def', 'ghi'))
+        self.assertEqual(
+            'abc/def/ghi',
+            dump_result
+        )
+        load_result = jsons.load(dump_result, PurePosixPath)
+        self.assertEqual(
+            PurePosixPath('abc', 'def', 'ghi'),
+            load_result
+        )
+
+    def test_dump_windows_load_windows(self):
+        dump_result = jsons.dump(PureWindowsPath('abc', 'def', 'ghi'))
+        self.assertEqual(
+            'abc/def/ghi',
+            dump_result
+        )
+        load_result = jsons.load(dump_result, PureWindowsPath)
+        self.assertEqual(
+            PureWindowsPath('abc', 'def', 'ghi'),
+            load_result
+        )