Repository: statsmodels/statsmodels

Issue:
Handle pathlib paths with `get_file_obj`
#### Is your feature request related to a problem? Please describe
I'm trying to read a statsmodels.tsa.statespace.sarimax.SARIMAXResults pickle file I've saved. I'd like to pass a pathlib.Path object as I find it easier to handle (I work with different machines, Mac and Windows).

#### Describe the solution you'd like
The function `open` already supports pathlib objects. Why not using it?

You could replace `statsmodels.iolib.openfile.get_file_obj` with:

```python
if _is_string_like(fname):
    return _open(fname, mode, encoding)
try:  # try opening it with 'open'
    return open(fname, mode, encoding)
except TypeError:  # else, maybe return context manager (please check if it's always TypeError)
    try:
        # Make sure the object has the write methods
        if 'r' in mode:
            fname.read
        if 'w' in mode or 'a' in mode:
            fname.write
    except AttributeError:
        raise ValueError('fname must be a string or a file-like object')
    return EmptyContextManager(fname)
```

This is just a suggestion though :)


Discussion:
A PR would be welcome for this enhancement.

On Tue, Apr 21, 2020, 21:09 Giulio Beseghi <notifications@github.com> wrote:

> Is your feature request related to a problem? Please describe
>
> I'm trying to read a statsmodels.tsa.statespace.sarimax.SARIMAXResults
> pickle file I've saved. I'd like to pass a pathlib.Path object as I find it
> easier to handle (I work with different machines, Mac and Windows).
> Describe the solution you'd like
>
> The function open already supports pathlib objects. Why not using it?
>
> You could replace statsmodels.iolib.openfile.get_file_obj with:
>
> if _is_string_like(fname):
>     return _open(fname, mode, encoding)try:  # try opening it with 'open'
>     return open(fname, mode, encoding)except TypeError:  # else, maybe return context manager (please check if it's always TypeError)
>     try:
>         # Make sure the object has the write methods
>         if 'r' in mode:
>             fname.read
>         if 'w' in mode or 'a' in mode:
>             fname.write
>     except AttributeError:
>         raise ValueError('fname must be a string or a file-like object')
>     return EmptyContextManager(fname)
>
> This is just a suggestion though :)
>
> —
> You are receiving this because you are subscribed to this thread.
> Reply to this email directly, view it on GitHub
> <https://github.com/statsmodels/statsmodels/issues/6652>, or unsubscribe
> <https://github.com/notifications/unsubscribe-auth/ABKTSRPYR7MPDUCTIUYXJYDRNX4P7ANCNFSM4MNSR7KQ>
> .
>

I've never done it, but I can try. Is there a guide about it?

Resolving patch:
diff --git a/statsmodels/iolib/openfile.py b/statsmodels/iolib/openfile.py
--- a/statsmodels/iolib/openfile.py
+++ b/statsmodels/iolib/openfile.py
@@ -34,13 +34,14 @@ def _open(fname, mode, encoding):
 
 def get_file_obj(fname, mode='r', encoding=None):
     """
-    Light wrapper to handle strings and let files (anything else) pass through.
+    Light wrapper to handle strings, path objects and let files (anything else)
+    pass through.
 
     It also handle '.gz' files.
 
     Parameters
     ----------
-    fname : str or file-like object
+    fname : str, path object or file-like object
         File to open / forward
     mode : str
         Argument passed to the 'open' or 'gzip.open' function
@@ -56,11 +57,14 @@ def get_file_obj(fname, mode='r', encoding=None):
     if _is_string_like(fname):
         return _open(fname, mode, encoding)
     try:
-        # Make sure the object has the write methods
-        if 'r' in mode:
-            fname.read
-        if 'w' in mode or 'a' in mode:
-            fname.write
-    except AttributeError:
-        raise ValueError('fname must be a string or a file-like object')
-    return EmptyContextManager(fname)
+        return open(fname, mode, encoding=encoding)  # handle pathlib-like objs
+    except TypeError:
+        try:
+            # Make sure the object has the write methods
+            if "r" in mode:
+                fname.read
+            if "w" in mode or "a" in mode:
+                fname.write
+        except AttributeError:
+            raise ValueError("fname must be a string or a file-like object")
+        return EmptyContextManager(fname)


Test patch:
diff --git a/statsmodels/iolib/tests/test_pickle.py b/statsmodels/iolib/tests/test_pickle.py
--- a/statsmodels/iolib/tests/test_pickle.py
+++ b/statsmodels/iolib/tests/test_pickle.py
@@ -1,4 +1,5 @@
 from io import BytesIO
+import pathlib
 import tempfile
 
 from numpy.testing import assert_equal
@@ -8,16 +9,27 @@
 
 
 def test_pickle():
-    tmpdir = tempfile.mkdtemp(prefix='pickle')
+    tmpdir = tempfile.mkdtemp(prefix="pickle")
     a = lrange(10)
-    save_pickle(a, tmpdir+'/res.pkl')
-    b = load_pickle(tmpdir+'/res.pkl')
+
+    # test with str
+    path_str = tmpdir + "/res.pkl"
+    save_pickle(a, path_str)
+    b = load_pickle(path_str)
     assert_equal(a, b)
 
+    # test with pathlib
+    path_pathlib = pathlib.Path(tmpdir) / "res2.pkl"
+    save_pickle(a, path_pathlib)
+    c = load_pickle(path_pathlib)
+    assert_equal(a, c)
+
     # cleanup, tested on Windows
     try:
         import os
-        os.remove(tmpdir+'/res.pkl')
+
+        os.remove(path_str)
+        os.remove(path_pathlib)
         os.rmdir(tmpdir)
     except (OSError, IOError):
         pass
@@ -27,6 +39,6 @@ def test_pickle():
     fh = BytesIO()
     save_pickle(a, fh)
     fh.seek(0, 0)
-    c = load_pickle(fh)
+    d = load_pickle(fh)
     fh.close()
-    assert_equal(a, c)
+    assert_equal(a, d)