Repository: Nikoleta-v3/blackbook

Issue:
Support ability to run blackbook over a single file
Currently it seems that there is no way to run `blackbook` over a specific file path. Attempting to format a single notebook at path `path/to/notebooks/Example-Notebook.ipynb` fails to do anything

```
$ pip freeze | grep blackbook
blackbook==0.0.5
$ blackbook path/to/notebooks/Example-Notebook.ipynb 
2019-11-29 14:00:14.403 | INFO     | blackbook.__main__:main:25 - All done! 📖
2019-11-29 14:00:14.403 | INFO     | blackbook.__main__:main:27 - 0 notebooks reformatted. 0 left unchanged.
```

where running

```
$ blackbook path/to/notebooks/
2019-11-29 14:05:26.204 | INFO     | blackbook.__main__:main:25 - All done! 📖
2019-11-29 14:05:26.205 | INFO     | blackbook.__main__:main:27 - 1 notebooks reformatted. 0 left unchanged.
```

It would be nice to add support for targeting a single file, in the same way that Black does.


Discussion:
matthewfeickert: @drvinceknight this is ready for review. Let me know if there are any questions or requested changes.
matthewfeickert: Thanks for the quick review of this PR, @Nikoleta-v3. Can you let me know if you and your group need anything else on this before you merge it in?
Nikoleta-v3: No worries @matthewfeickert. 

We usually aim to have two approvals before merging prs. So currently waiting for someone else from the team to review 👍 
matthewfeickert: > We usually aim to have two approvals before merging prs

Sounds good. We do the same thing for `pyhf`. :)
drvinceknight: Thanks for the contribution @matthewfeickert :+1:

Patch:
diff --git a/README.md b/README.md
index 63d2f49..e0acc79 100644
--- a/README.md
+++ b/README.md
@@ -11,9 +11,16 @@
 ```bash
 $ pip install blackbook
 $ blackbook .
-2019-01-28 17:15:10.857 | INFO     | blackbook.__main__:main:25 - All done! 📖
-2019-01-28 17:15:10.857 | INFO     | blackbook.__main__:main:27 - 1 notebooks
-reformatted. 1 left unchanged.
+2019-01-28 17:15:10.857 | INFO     | blackbook.__main__:main:31 - All done! 📖
+2019-01-28 17:15:10.857 | INFO     | blackbook.__main__:main:33 - 1 notebooks reformatted. 1 left unchanged.
+```
+
+You can also run `blackbook` over a single notebook
+
+```bash
+$ blackbook unformatted.ipynb
+2019-01-28 17:15:12.663 | INFO     | blackbook.__main__:main:31 - All done! 📖
+2019-01-28 17:15:12.663 | INFO     | blackbook.__main__:main:33 - 1 notebooks reformatted. 0 left unchanged.
 ```
 
 ## Why?
diff --git a/src/blackbook/__main__.py b/src/blackbook/__main__.py
index e1542dd..8166e32 100644
--- a/src/blackbook/__main__.py
+++ b/src/blackbook/__main__.py
@@ -10,10 +10,16 @@ import blackbook
 def main(path: pathlib.Path = None) -> None:
     if path is None:  # pragma: no cover
         path = pathlib.Path(sys.argv[1])
+    if not path.exists():
+        raise OSError(f"The path {path} is invalid.")
 
     count = 0
     reformatted_count = 0
-    for notebook_path in blackbook.gen_notebook_files_in_dir(path):
+
+    notebooks = (
+        [path] if path.suffix == ".ipynb" else blackbook.gen_notebook_files_in_dir(path)
+    )
+    for notebook_path in notebooks:
         nb = blackbook.format_notebook_content(notebook_path)
 
         if nb is not None:  # pragma: no cover


Test patch:
diff --git a/tests/test_format_notebook_content.py b/tests/test_format_notebook_content.py
index b16010b..9a70d61 100644
--- a/tests/test_format_notebook_content.py
+++ b/tests/test_format_notebook_content.py
@@ -1,5 +1,6 @@
 import json
 import pathlib
+import shutil
 
 import blackbook
 
@@ -20,6 +21,44 @@ def test_format_notebook_content():
     )
 
 
+def test_format_single_notebook():
+    """
+    Test formatting a single notebook in an directory of unformatted notebooks
+    formats only that notebook
+    """
+    data_path = pathlib.Path(f"{__file__}").parent / "data"
+    source_notebook_path = data_path / "unformatted" / "spaces.ipynb"
+    copy_notebook_path = data_path / "unformatted" / "spaces_copy.ipynb"
+    shutil.copy(str(source_notebook_path), str(copy_notebook_path))
+
+    source_json = json.loads(source_notebook_path.read_text())
+    copy_json = json.loads(copy_notebook_path.read_text())
+
+    assert all(
+        [
+            copy_cell["source"] == source_cell["source"]
+            for copy_cell, source_cell in zip(copy_json["cells"], source_json["cells"])
+        ]
+    )
+
+    # Have to run blackbook main to show spaces_copy.ipynb is unaffected
+    blackbook.__main__.main(source_notebook_path)
+    output_json = json.loads(source_notebook_path.read_text())
+    check_copy_json = json.loads(copy_notebook_path.read_text())
+    # Revert to pre-format state: unformat source notebook and delete copy
+    shutil.copy(str(copy_notebook_path), str(source_notebook_path))
+    pathlib.Path.unlink(copy_notebook_path)
+
+    assert not all(
+        [
+            formatted_cell["source"] == unformatted_cell["source"]
+            for formatted_cell, unformatted_cell in zip(
+                output_json["cells"], check_copy_json["cells"]
+            )
+        ]
+    )
+
+
 def test_format_notebook_content_does_nothing_with_formatted_notebook():
     data_path = pathlib.Path(f"{__file__}").parent / "data"
     formatted_notebook_path = data_path / "formatted" / "spaces.ipynb"
diff --git a/tests/test_main.py b/tests/test_main.py
index 023280a..97e2017 100644
--- a/tests/test_main.py
+++ b/tests/test_main.py
@@ -1,5 +1,7 @@
 import pathlib
 
+import pytest
+
 from blackbook.__main__ import main
 
 
@@ -7,3 +9,9 @@ def test_main_runs_without_failure():
     data_path = pathlib.Path(f"{__file__}").parent / "data" / "formatted"
 
     assert main(data_path) is None
+
+
+def test_invalid_path():
+    with pytest.raises(OSError):
+        data_path = pathlib.Path(f"{__file__}").parent / "invalid"
+        main(data_path)