Repository: psd-tools/psd-tools (python)

Issue:
Path object handling regression in 1.10.3
Prior to commit 94df69aecde44096dea1ed94334309c7039cc0a6 [diff](https://github.com/psd-tools/psd-tools/commit/94df69aecde44096dea1ed94334309c7039cc0a6#diff-77f111aeb647f9a82dc1507b77c2beaf25705688e1a864bf95497ad88a37cf74), using a pathlib Path object in PSDImage.open and save functions worked just fine.
In that recent commit, the following logic was changed in src/psd_tools/api/psd_image.py:

```py
    def open(cls, fp, **kwargs):
        ...
        if hasattr(fp, "read"):
            self = cls(PSD.read(fp, **kwargs))
        else:
            with open(fp, "rb") as f:
                self = cls(PSD.read(f, **kwargs))
        return self

    def save(self, fp, mode="wb", **kwargs):
        ...

        if hasattr(fp, "write"):
            self._record.write(fp, **kwargs)
        else:
            with open(fp, mode) as f:
                self._record.write(f, **kwargs)
```
to
```py
    def open(cls, fp: BinaryIO | str, **kwargs: Any) -> Self:
        ...
        if isinstance(fp, str):
            with open(fp, "rb") as f:
                self = cls(PSD.read(f, **kwargs))
        else:
            self = cls(PSD.read(fp, **kwargs))
        return self

    def save(self, fp: BinaryIO | str, mode: str = "wb", **kwargs: Any) -> None:
        ...

        if isinstance(fp, str):
            with open(fp, mode) as f:
                self._record.write(f, **kwargs)
        else:
            self._record.write(fp, **kwargs)
```
This new logic fails when passed a pathlib.Path object, part of the standard library, when it worked before.
The fix is trivial:

You can use the same type annotation as [PIL does](https://github.com/python-pillow/Pillow/blob/main/src/PIL/_typing.py#L50):
```py
StrOrBytesPath = Union[str, bytes, os.PathLike[str], os.PathLike[bytes]]
```
and instead of `isinstance(fp, str)` you could use `isinstance(fp, (bytes, str, os.PathLike))` the way [PIL does](https://github.com/python-pillow/Pillow/blob/main/src/PIL/_util.py#L9)

**To Reproduce**

```python
from pathlib import Path

psd = PSDImage.open(Path('filename.psd'))
```

**Expected behavior**
As in prior versions, it should save/open the image.


**Environment**
 - Version [v1.10.3]

**Additional context**
I'd be interested in knowing what prompted you to change the code in the first place, since I don't see an issue with the
old version. What bug did you encounter or problem do I not know about?

Thank you for your time.


Pull request description:
Allow more types for file name
Changes:
- Fix the error for the `Path` object (Fix #451)

Patch:
diff --git a/src/psd_tools/api/psd_image.py b/src/psd_tools/api/psd_image.py
index e3d8b40..d29684d 100644
--- a/src/psd_tools/api/psd_image.py
+++ b/src/psd_tools/api/psd_image.py
@@ -5,6 +5,7 @@ PSD Image module.
 from __future__ import annotations
 
 import logging
+import os
 from typing import Any, BinaryIO, Callable, Literal
 
 try:
@@ -131,7 +132,7 @@ class PSDImage(GroupMixin):
         )
 
     @classmethod
-    def open(cls, fp: BinaryIO | str, **kwargs: Any) -> Self:
+    def open(cls, fp: BinaryIO | str | bytes | os.PathLike, **kwargs: Any) -> Self:
         """
         Open a PSD document.
 
@@ -140,14 +141,14 @@ class PSDImage(GroupMixin):
             default 'macroman'. Some psd files need explicit encoding option.
         :return: A :py:class:`~psd_tools.api.psd_image.PSDImage` object.
         """
-        if isinstance(fp, str):
+        if isinstance(fp, (str, bytes, os.PathLike)):
             with open(fp, "rb") as f:
                 self = cls(PSD.read(f, **kwargs))
         else:
             self = cls(PSD.read(fp, **kwargs))
         return self
 
-    def save(self, fp: BinaryIO | str, mode: str = "wb", **kwargs: Any) -> None:
+    def save(self, fp: BinaryIO | str | bytes | os.PathLike, mode: str = "wb", **kwargs: Any) -> None:
         """
         Save the PSD file.
 
@@ -159,7 +160,7 @@ class PSDImage(GroupMixin):
 
         self._update_record()
 
-        if isinstance(fp, str):
+        if isinstance(fp, (str, bytes, os.PathLike)):
             with open(fp, mode) as f:
                 self._record.write(f, **kwargs)
         else:


Test patch:
diff --git a/tests/psd_tools/api/test_psd_image.py b/tests/psd_tools/api/test_psd_image.py
index 22061f7..9863c17 100644
--- a/tests/psd_tools/api/test_psd_image.py
+++ b/tests/psd_tools/api/test_psd_image.py
@@ -2,12 +2,12 @@ from __future__ import absolute_import, unicode_literals
 
 import logging
 import os
+from pathlib import Path
 
 import pytest
 from IPython.lib.pretty import pprint
 
 from psd_tools.api.psd_image import PSDImage
-from psd_tools.api.layers import Group
 from psd_tools.constants import ColorMode, Compression
 
 from ..utils import full_name
@@ -46,9 +46,10 @@ def test_frompil_psb():
     "filename",
     [
         "colormodes/4x4_8bit_rgb.psd",
+        Path("colormodes/4x4_8bit_rgb.psd"),
     ],
 )
-def test_open_save(filename, tmpdir):
+def test_open(filename):
     input_path = full_name(filename)
     PSDImage.open(input_path)
     with open(input_path, "rb") as f:
@@ -58,6 +59,7 @@ def test_open_save(filename, tmpdir):
 def test_save(fixture, tmpdir):
     output_path = os.path.join(str(tmpdir), "output.psd")
     fixture.save(output_path)
+    fixture.save(Path(output_path))
     with open(output_path, "wb") as f:
         fixture.save(f)
 
@@ -123,7 +125,7 @@ def test_repr_pretty(fixture):
         os.path.join("third-party-psds", "cactus_top.psd"),
     ],
 )
-def test_open(filename):
+def test_open2(filename):
     assert isinstance(PSDImage.open(full_name(filename)), PSDImage)