Repository: jupyter/nbformat

Issue:
Please support pathlib.Path objects
Could you please add support for `pathlib.Path` objects in `nbformat.read` and `nbformat.write`?

See [pathlib](https://docs.python.org/3/library/pathlib.html) and [PEP 519](https://www.python.org/dev/peps/pep-0519/).

```
>>> import nbformat
>>> from pathlib import Path
>>> path = Path("tests/test2.ipynb")
>>> nb = nbformat.read(Path("tests/test2.ipynb"), as_version=4)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/Users/deil/anaconda3/lib/python3.7/site-packages/nbformat/__init__.py", line 141, in read
    return reads(fp.read(), as_version, **kwargs)
AttributeError: 'PosixPath' object has no attribute 'read'
```

Looking at the implementation, it's clear why `pathlib.Path` isn't supported:

https://github.com/jupyter/nbformat/blob/b6b5a18e5a40d37f1cc0f71f65108288bdec9bb7/nbformat/__init__.py#L114-L141

But what is a good pattern to support `pathlib.Path` in addition?
Just an extra `if isinstance(fp, pathlib.Path)`, or is there a better way?

Note that the docstring still contains a caveat concerning Python 2, and `py3compat` is used. Probably that could be removed now.

I wanted to send a PR, but it wasn't clear to me how to write it. If you comment or point out an example how you do this in other places in the Jupyter codebase, I can take a shot at it.

Discussion:

Patch:
diff --git a/nbformat/__init__.py b/nbformat/__init__.py
index 7552e30..bb9c0fc 100644
--- a/nbformat/__init__.py
+++ b/nbformat/__init__.py
@@ -133,11 +133,12 @@ def read(fp, as_version, **kwargs):
     nb : NotebookNode
         The notebook that was read.
     """
-    if isinstance(fp, (str, bytes)):
-        with io.open(fp, encoding='utf-8') as f:
-            return read(f, as_version, **kwargs)
 
-    return reads(fp.read(), as_version, **kwargs)
+    try:
+        return reads(fp.read(), as_version, **kwargs)
+    except AttributeError:
+        with io.open(fp, encoding='utf-8') as f:
+            return reads(f.read(), as_version, **kwargs)
 
 
 def write(nb, fp, version=NO_CONVERT, **kwargs):
@@ -158,13 +159,16 @@ def write(nb, fp, version=NO_CONVERT, **kwargs):
         If unspecified, or specified as nbformat.NO_CONVERT,
         the notebook's own version will be used and no conversion performed.
     """
-    if isinstance(fp, (str, bytes)):
-        with io.open(fp, 'w', encoding='utf-8') as f:
-            return write(nb, f, version=version, **kwargs)
-
     s = writes(nb, version, **kwargs)
     if isinstance(s, bytes):
         s = s.decode('utf8')
-    fp.write(s)
-    if not s.endswith(u'\n'):
-        fp.write(u'\n')
+
+    try:
+        fp.write(s)
+        if not s.endswith(u'\n'):
+            fp.write(u'\n')
+    except AttributeError:
+        with io.open(fp, 'w', encoding='utf-8') as f:
+            f.write(s)
+            if not s.endswith(u'\n'):
+                f.write(u'\n')


Test patch:
diff --git a/nbformat/tests/test_api.py b/nbformat/tests/test_api.py
index f93f827..f991e2f 100644
--- a/nbformat/tests/test_api.py
+++ b/nbformat/tests/test_api.py
@@ -5,6 +5,9 @@
 
 import json
 import os
+import pathlib
+import sys
+import unittest
 
 from .base import TestsBase
 
@@ -47,3 +50,17 @@ class TestAPI(TestsBase):
             dest = os.path.join(td, 'echidna.ipynb')
             write(nb, dest)
             assert os.path.isfile(dest)
+
+    @unittest.skipIf(
+        sys.version_info < (3, 6, 0),
+        "python versions 3.5 and lower don't support opening pathlib.Path objects"
+    )
+    def test_read_write_pathlib_object(self):
+        """read() and write() take path-like objects such as pathlib objects"""
+        path = pathlib.Path(self._get_files_path()) / u'test4.ipynb'
+        nb = read(path, as_version=4)
+
+        with TemporaryDirectory() as td:
+            dest = pathlib.Path(td) / 'echidna.ipynb'
+            write(nb, dest)
+            assert os.path.isfile(dest)