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:
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

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.

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)

pip install -e .
parse_log_pytest
requirements.txt
pytest pytest-cov isort
3.7
requirements.txt
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
black==23.3.0
-e git+https://github.com/Nikoleta-v3/blackbook.git@31468676b827657269a5730769111a759bb018c6#egg=blackbook
certifi @ file:///croot/certifi_1671487769961/work/certifi
click==8.1.8
coverage==7.2.7
exceptiongroup==1.2.2
importlib-metadata==6.7.0
iniconfig==2.0.0
isort==5.11.5
loguru==0.7.3
mypy-extensions==1.0.0
packaging==24.0
pathspec==0.11.2
platformdirs==4.0.0
pluggy==1.2.0
pytest==7.4.4
pytest-cov==4.1.0
tomli==2.0.1
typed-ast==1.5.5
typing_extensions==4.7.1
zipp==3.15.0

name: blackbook
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - certifi=2022.12.7=py37h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=1.1.1w=h7f8727e_0
  - pip=22.3.1=py37h06a4308_0
  - python=3.7.16=h7a1cb2a_0
  - readline=8.2=h5eee18b_0
  - setuptools=65.6.3=py37h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - wheel=0.38.4=py37h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - black==23.3.0
      - click==8.1.8
      - coverage==7.2.7
      - exceptiongroup==1.2.2
      - importlib-metadata==6.7.0
      - iniconfig==2.0.0
      - isort==5.11.5
      - loguru==0.7.3
      - mypy-extensions==1.0.0
      - packaging==24.0
      - pathspec==0.11.2
      - platformdirs==4.0.0
      - pluggy==1.2.0
      - pytest==7.4.4
      - pytest-cov==4.1.0
      - tomli==2.0.1
      - typed-ast==1.5.5
      - typing-extensions==4.7.1
      - zipp==3.15.0
prefix: /opt/conda/envs/blackbook

tests/test_format_notebook_content.py::test_format_single_notebook tests/test_main.py::test_invalid_path
tests/test_format_notebook_content.py::test_format_notebook_content tests/test_format_notebook_content.py::test_format_notebook_content_does_nothing_with_formatted_notebook tests/test_main.py::test_main_runs_without_failure
src/blackbook/__main__.py README.md
src/blackbook/__main__.py README.md