Repository: python-pillow/Pillow

Issue:
Add support for pathlib.Path objects
Currently, the `open` and `save` functions appear to support strings and raw file descriptors. It would make things easier if `pathlib.Path` objects were supported as well.



Discussion:

Resolving patch:
diff --git a/PIL/Image.py b/PIL/Image.py
--- a/PIL/Image.py
+++ b/PIL/Image.py
@@ -1640,7 +1640,7 @@ def save(self, fp, format=None, **params):
         implement the ``seek``, ``tell``, and ``write``
         methods, and be opened in binary mode.
 
-        :param fp: File name or file object.
+        :param fp: A filename (string), pathlib.Path object or file object.
         :param format: Optional format override.  If omitted, the
            format to use is determined from the filename extension.
            If a file object was used instead of a filename, this
@@ -1653,13 +1653,15 @@ def save(self, fp, format=None, **params):
            may have been created, and may contain partial data.
         """
 
+        filename = ""
         if isPath(fp):
             filename = fp
-        else:
-            if hasattr(fp, "name") and isPath(fp.name):
-                filename = fp.name
-            else:
-                filename = ""
+        elif sys.version_info >= (3, 4):
+            from pathlib import Path
+            if isinstance(fp, Path):
+                filename = str(fp.resolve())
+        elif hasattr(fp, "name") and isPath(fp.name):
+            filename = fp.name
 
         # may mutate self!
         self.load()
@@ -1687,8 +1689,8 @@ def save(self, fp, format=None, **params):
         else:
             save_handler = SAVE[format.upper()]
 
-        if isPath(fp):
-            fp = builtins.open(fp, "wb")
+        if filename:
+            fp = builtins.open(filename, "wb")
             close = 1
         else:
             close = 0
@@ -2265,9 +2267,9 @@ def open(fp, mode="r"):
     :py:meth:`~PIL.Image.Image.load` method).  See
     :py:func:`~PIL.Image.new`.
 
-    :param fp: A filename (string) or a file object.  The file object
-       must implement :py:meth:`~file.read`, :py:meth:`~file.seek`, and
-       :py:meth:`~file.tell` methods, and be opened in binary mode.
+    :param fp: A filename (string), pathlib.Path object or a file object.
+       The file object must implement :py:meth:`~file.read`, :py:meth:`~file.seek`,
+       and :py:meth:`~file.tell` methods, and be opened in binary mode.
     :param mode: The mode.  If given, this argument must be "r".
     :returns: An :py:class:`~PIL.Image.Image` object.
     :exception IOError: If the file cannot be found, or the image cannot be
@@ -2277,11 +2279,15 @@ def open(fp, mode="r"):
     if mode != "r":
         raise ValueError("bad mode %r" % mode)
 
+    filename = ""
     if isPath(fp):
         filename = fp
-        fp = builtins.open(fp, "rb")
-    else:
-        filename = ""
+    elif sys.version_info >= (3, 4):
+        from pathlib import Path
+        if isinstance(fp, Path):
+            filename = str(fp.resolve())
+    if filename:
+        fp = builtins.open(filename, "rb")
 
     try:
         fp.seek(0)


Test patch:
diff --git a/Tests/test_image.py b/Tests/test_image.py
--- a/Tests/test_image.py
+++ b/Tests/test_image.py
@@ -1,6 +1,7 @@
 from helper import unittest, PillowTestCase, hopper
 
 from PIL import Image
+import sys
 
 
 class TestImage(PillowTestCase):
@@ -48,6 +49,14 @@ def test_invalid_image(self):
             im = io.BytesIO(b'')
         self.assertRaises(IOError, lambda: Image.open(im))
 
+    @unittest.skipIf(sys.version_info < (3, 4),
+                     "pathlib only available in Python 3.4 or later")
+    def test_pathlib(self):
+        from pathlib import Path
+        im = Image.open(Path("Tests/images/hopper.jpg"))
+        self.assertEqual(im.mode, "RGB")
+        self.assertEqual(im.size, (128, 128))
+
     def test_internals(self):
 
         im = Image.new("L", (100, 100))