Repository: scikit-image/scikit-image

Issue:
bug: imsave cannot detect right extension of pathlib 
imsave cannot detect right extension of pathlib 

```python
from pathlib import Path
from  skimage import io

filename = Path("out.jpg")
io.imsave(filename, im) # this saves im as a png file though the extension is jpg

io.imsave("out.jpg", im) #this is working correctly
io.imsave(str(filename), im) #this is working correctly
```


Discussion:
Wow, pathlib strikes again! CC @hmaarrfk 

Thanks for the report @qinst64, we'll look into it.
what version of python are you using?
@hmaarrfk `python 3.6` and `skimage 0.14.0` from pip
@qinst64  tip: make your code example fully self contained. `im` was not defined.

```python
from pathlib import Path
from  skimage import io
from skimage import img_as_ubyte
import numpy as np

# pip install imageio
# io.use_plugin('imageio')  # If you uncomment this line, your code should be OK

im = img_as_ubyte(np.random.random((10, 10)))
io.imsave(Path("pathlib_should_be_jpg.jpg"), im) # this saves im as a png file though the extension is jpg. The header of the binary file specifies PNG

io.imsave("str_should_be_jpg.jpg", im) #this is working correctly
io.imsave(str(Path("pathlib_to_str_should_be_jpg.jpg")), im) #this is working correctly
```
I might even edit your original post to include all the information you missed:

~~Funny enough, on my machine it works ok.~~ So strange. You are right. Seems to be a bug in PIL. Try uncommenting the line about specifying the plugin. In 0.14.1 we will change to using `imageio` as default and this shouldn't be an issue anymore. You should file a bugreport with [Pillow](https://github.com/python-pillow/Pillow), but in your example, call the PIL functions directly.

Honestly, pathlib has been hit and miss. Many `pythonic` libraries seem to do OK, but many libraries that interact with `c` do not behave so well. You are doing well testing your code as meticulously as you are. This kind of stuff is important.



If this solves your problem, I would suggest you change the title for a request for more documentation about the peculiarities of using PIL.

I think the correct solution is to have PIL support pathlib upstream, but we would need to tell users about which version of PIL they should have installed.
@hmaarrfk  Thank you. `io.use_plugin('imageio')` does work, it seems that my `io.save` uses `pil` as the default plugin  
However, I did not find any problem while using PIL
``` python
from PIL import Image
from pathlib import Path
im = Image.open("test.jpg")
im.save(Path("out.jpg")) # this is working correctly
``` 
Then I dig into the source code `imsave` inside `skimage/io/_plugins/pil_plugin.py`
around line 204
```python
def imsave(fname, arr, format_str=None, **kwargs):
    #...
    # default to PNG if file-like object
    if not isinstance(fname, str) and format_str is None:
        format_str = "PNG"
    # Check for png in filename
    if (isinstance(fname, str)
            and fname.lower().endswith(".png")):
        format_str = "PNG"
    
    arr = np.asanyarray(arr)
    #...
```
I guess some type assert can be added to distinguish `Path`. i.e. `isinstance(fname, Path)`
Dam, good catch!!!!
I'm not really a fan of having any logic in our plugin wrappers. 

I'm not sure this is something I am interested in working on. I am pretty against having any kind of "convenience functions" in skimage. For me, the whole `io` module is "convenience" (which was probably important in earlier versions).
There is always an edge case that we can't catch, and I think it would have been better for you to throw an error message instead of "doing magic".

I'm not sure what the best way to fix this is. I would say we should convert the `str` to a `pathlib` object but that had problems with older versions of some library that we have as a "minimum version". Maybe it was even numpy. There is also a desire to keep 0.14 as an LTS release to support python 2.7, and therefore a "solution" would have to consider compatibility with 2.7. Do we want to support something like [pathlib2](https://pypi.org/project/pathlib2/)? Anyway, this is a whole can of worms.

My tip for you: use PIL or imageio directly. (imageio likely uses PIL anyway).

My suggestion for scikit-image: get rid of `io`.

If you are interested in submitting a PR, please include a reference the logic used in `imageio` to detect the filetype. I would be interested in comparing your solution to theirs.

For reference, here is the problematic code.
https://github.com/scikit-image/scikit-image/blob/68cb241fc364a25f1af73cdf3829fd30914503ba/skimage/io/_plugins/pil_plugin.py#L239

And there are likely other lines of in io that use `str`. We should probably make a regex search for something like `isinstance(.*, str)`
Won't respond to everything @hmaarrfk wrote other than to broadly agree.

Regarding backporting a fix, I might put this in the "too hard, upgrade to Py3" basket. The "right but quick" fix is to add a check for `isinstance(fname, Path)` in the same clause. A hacky option for PY2/3 compatibility in the backport would be to say `if sys.version_info[0] == 2: Path = str` and then the check becomes redundant but works. 
@jni @hmaarrfk Forgive me if I'm being naive, but is it possible to convert `fname` to `str` explicitly in the beginning? Since, we specify that the type of `fname` should be `str`:

https://github.com/scikit-image/scikit-image/blob/a5818a60caeee1c5073434b2ed45da42ab73869f/skimage/io/_io.py#L107-L114
Converting to a string for python 3 would be the wrong way to go. Paths are more locations, that can be described by strings, but it is useful to treat them as abstract objects. 

For python 2, maybe
Isn't #5546 fixing this issue?

Resolving patch:
diff --git a/skimage/io/_io.py b/skimage/io/_io.py
--- a/skimage/io/_io.py
+++ b/skimage/io/_io.py
@@ -128,6 +128,8 @@ def imsave(fname, arr, plugin=None, check_contrast=True, **plugin_args):
     and largest file size (default 75).  This is only available when using
     the PIL and imageio plugins.
     """
+    if isinstance(fname, pathlib.Path):
+        fname = str(fname.resolve())
     if plugin is None and hasattr(fname, 'lower'):
         if fname.lower().endswith(('.tiff', '.tif')):
             plugin = 'tifffile'


Test patch:
diff --git a/skimage/io/tests/test_tifffile.py b/skimage/io/tests/test_tifffile.py
--- a/skimage/io/tests/test_tifffile.py
+++ b/skimage/io/tests/test_tifffile.py
@@ -1,11 +1,13 @@
-import itertools
+import pathlib
 from tempfile import NamedTemporaryFile
-from skimage.io import imread, imsave, use_plugin, reset_plugins
+
 import numpy as np
+import pytest
+from numpy.testing import assert_array_almost_equal, assert_array_equal
+
+from skimage._shared.testing import fetch
+from skimage.io import imread, imsave, reset_plugins, use_plugin
 
-from skimage._shared.testing import (assert_array_equal,
-                                     assert_array_almost_equal,
-                                     parametrize, fetch)
 
 def setup():
     use_plugin('tifffile')
@@ -51,10 +53,12 @@ def test_imread_handle():
 
 
 class TestSave:
-    def roundtrip(self, dtype, x):
+    def roundtrip(self, dtype, x, use_pathlib=False):
         f = NamedTemporaryFile(suffix='.tif')
         fname = f.name
         f.close()
+        if use_pathlib:
+            fname = pathlib.Path(fname)
         imsave(fname, x, check_contrast=False)
         y = imread(fname)
         assert_array_equal(x, y)
@@ -62,12 +66,14 @@ def roundtrip(self, dtype, x):
     shapes = ((10, 10), (10, 10, 3), (10, 10, 4))
     dtypes = (np.uint8, np.uint16, np.float32, np.int16, np.float64)
 
-    @parametrize("shape, dtype", itertools.product(shapes, dtypes))
-    def test_imsave_roundtrip(self, shape, dtype):
+    @pytest.mark.parametrize("shape", shapes)
+    @pytest.mark.parametrize("dtype", dtypes)
+    @pytest.mark.parametrize("use_pathlib", [False, True])
+    def test_imsave_roundtrip(self, shape, dtype, use_pathlib):
         x = np.random.rand(*shape)
 
         if not np.issubdtype(dtype, np.floating):
             x = (x * np.iinfo(dtype).max).astype(dtype)
         else:
             x = x.astype(dtype)
-        self.roundtrip(dtype, x)
+        self.roundtrip(dtype, x, use_pathlib)