All patches and comments are welcome. Please squash your changes to logical
commits before using git-format-patch and git-send-email to
patches@git.madduck.net.
If you'd read over the Git project's submission guidelines and adhered to them,
I'd be especially grateful.
1 from dataclasses import replace
4 from contextlib import ExitStack as does_not_raise
5 from typing import ContextManager
7 from click.testing import CliRunner
8 from black.handle_ipynb_magics import jupyter_dependencies_are_installed
17 from black import Mode
18 from _pytest.monkeypatch import MonkeyPatch
19 from tests.util import DATA_DIR
21 pytestmark = pytest.mark.jupyter
22 pytest.importorskip("IPython", reason="IPython is an optional dependency")
23 pytest.importorskip("tokenize_rt", reason="tokenize-rt is an optional dependency")
25 JUPYTER_MODE = Mode(is_ipynb=True)
27 EMPTY_CONFIG = DATA_DIR / "empty_pyproject.toml"
32 def test_noop() -> None:
34 with pytest.raises(NothingChanged):
35 format_cell(src, fast=True, mode=JUPYTER_MODE)
38 @pytest.mark.parametrize("fast", [True, False])
39 def test_trailing_semicolon(fast: bool) -> None:
41 result = format_cell(src, fast=fast, mode=JUPYTER_MODE)
42 expected = 'foo = "a";'
43 assert result == expected
46 def test_trailing_semicolon_with_comment() -> None:
47 src = 'foo = "a" ; # bar'
48 result = format_cell(src, fast=True, mode=JUPYTER_MODE)
49 expected = 'foo = "a"; # bar'
50 assert result == expected
53 def test_trailing_semicolon_with_comment_on_next_line() -> None:
54 src = "import black;\n\n# this is a comment"
55 with pytest.raises(NothingChanged):
56 format_cell(src, fast=True, mode=JUPYTER_MODE)
59 def test_trailing_semicolon_indented() -> None:
60 src = "with foo:\n plot_bar();"
61 with pytest.raises(NothingChanged):
62 format_cell(src, fast=True, mode=JUPYTER_MODE)
65 def test_trailing_semicolon_noop() -> None:
67 with pytest.raises(NothingChanged):
68 format_cell(src, fast=True, mode=JUPYTER_MODE)
71 @pytest.mark.parametrize(
74 pytest.param(JUPYTER_MODE, id="default mode"),
76 replace(JUPYTER_MODE, python_cell_magics={"cust1", "cust1"}),
77 id="custom cell magics mode",
81 def test_cell_magic(mode: Mode) -> None:
82 src = "%%time\nfoo =bar"
83 result = format_cell(src, fast=True, mode=mode)
84 expected = "%%time\nfoo = bar"
85 assert result == expected
88 def test_cell_magic_noop() -> None:
90 with pytest.raises(NothingChanged):
91 format_cell(src, fast=True, mode=JUPYTER_MODE)
94 @pytest.mark.parametrize(
97 pytest.param(JUPYTER_MODE, id="default mode"),
99 replace(JUPYTER_MODE, python_cell_magics={"cust1", "cust1"}),
100 id="custom cell magics mode",
104 @pytest.mark.parametrize(
107 pytest.param("ls =!ls", "ls = !ls", id="System assignment"),
108 pytest.param("!ls\n'foo'", '!ls\n"foo"', id="System call"),
109 pytest.param("!!ls\n'foo'", '!!ls\n"foo"', id="Other system call"),
110 pytest.param("?str\n'foo'", '?str\n"foo"', id="Help"),
111 pytest.param("??str\n'foo'", '??str\n"foo"', id="Other help"),
113 "%matplotlib inline\n'foo'",
114 '%matplotlib inline\n"foo"',
115 id="Line magic with argument",
117 pytest.param("%time\n'foo'", '%time\n"foo"', id="Line magic without argument"),
119 "env = %env var", "env = %env var", id="Assignment to environment variable"
121 pytest.param("env = %env", "env = %env", id="Assignment to magic"),
124 def test_magic(src: str, expected: str, mode: Mode) -> None:
125 result = format_cell(src, fast=True, mode=mode)
126 assert result == expected
129 @pytest.mark.parametrize(
133 "%%html --isolated\n2+2",
134 "%%writefile e.txt\n meh\n meh",
137 def test_non_python_magics(src: str) -> None:
138 with pytest.raises(NothingChanged):
139 format_cell(src, fast=True, mode=JUPYTER_MODE)
142 def test_set_input() -> None:
144 with pytest.raises(NothingChanged):
145 format_cell(src, fast=True, mode=JUPYTER_MODE)
148 def test_input_already_contains_transformed_magic() -> None:
149 src = '%time foo()\nget_ipython().run_cell_magic("time", "", "foo()\\n")'
150 with pytest.raises(NothingChanged):
151 format_cell(src, fast=True, mode=JUPYTER_MODE)
154 def test_magic_noop() -> None:
156 with pytest.raises(NothingChanged):
157 format_cell(src, fast=True, mode=JUPYTER_MODE)
160 def test_cell_magic_with_magic() -> None:
161 src = "%%timeit -n1\nls =!ls"
162 result = format_cell(src, fast=True, mode=JUPYTER_MODE)
163 expected = "%%timeit -n1\nls = !ls"
164 assert result == expected
167 @pytest.mark.parametrize(
168 "mode, expected_output, expectation",
172 "%%custom_python_magic -n1 -n2\nx=2",
173 pytest.raises(NothingChanged),
174 id="No change when cell magic not registered",
177 replace(JUPYTER_MODE, python_cell_magics={"cust1", "cust1"}),
178 "%%custom_python_magic -n1 -n2\nx=2",
179 pytest.raises(NothingChanged),
180 id="No change when other cell magics registered",
183 replace(JUPYTER_MODE, python_cell_magics={"custom_python_magic", "cust1"}),
184 "%%custom_python_magic -n1 -n2\nx = 2",
186 id="Correctly change when cell magic registered",
190 def test_cell_magic_with_custom_python_magic(
191 mode: Mode, expected_output: str, expectation: ContextManager[object]
194 result = format_cell(
195 "%%custom_python_magic -n1 -n2\nx=2",
199 assert result == expected_output
202 def test_cell_magic_nested() -> None:
203 src = "%%time\n%%time\n2+2"
204 result = format_cell(src, fast=True, mode=JUPYTER_MODE)
205 expected = "%%time\n%%time\n2 + 2"
206 assert result == expected
209 def test_cell_magic_with_magic_noop() -> None:
210 src = "%%t -n1\nls = !ls"
211 with pytest.raises(NothingChanged):
212 format_cell(src, fast=True, mode=JUPYTER_MODE)
215 def test_automagic() -> None:
216 src = "pip install black"
217 with pytest.raises(NothingChanged):
218 format_cell(src, fast=True, mode=JUPYTER_MODE)
221 def test_multiline_magic() -> None:
222 src = "%time 1 + \\\n2"
223 with pytest.raises(NothingChanged):
224 format_cell(src, fast=True, mode=JUPYTER_MODE)
227 def test_multiline_no_magic() -> None:
229 result = format_cell(src, fast=True, mode=JUPYTER_MODE)
231 assert result == expected
234 def test_cell_magic_with_invalid_body() -> None:
235 src = "%%time\nif True"
236 with pytest.raises(NothingChanged):
237 format_cell(src, fast=True, mode=JUPYTER_MODE)
240 def test_empty_cell() -> None:
242 with pytest.raises(NothingChanged):
243 format_cell(src, fast=True, mode=JUPYTER_MODE)
246 def test_entire_notebook_empty_metadata() -> None:
247 with open(DATA_DIR / "notebook_empty_metadata.ipynb", "rb") as fd:
248 content_bytes = fd.read()
249 content = content_bytes.decode()
250 result = format_file_contents(content, fast=True, mode=JUPYTER_MODE)
255 ' "cell_type": "code",\n'
256 ' "execution_count": null,\n'
264 ' "print(\\"foo\\")"\n'
268 ' "cell_type": "code",\n'
269 ' "execution_count": null,\n'
277 ' "nbformat_minor": 4\n'
280 assert result == expected
283 def test_entire_notebook_trailing_newline() -> None:
284 with open(DATA_DIR / "notebook_trailing_newline.ipynb", "rb") as fd:
285 content_bytes = fd.read()
286 content = content_bytes.decode()
287 result = format_file_contents(content, fast=True, mode=JUPYTER_MODE)
292 ' "cell_type": "code",\n'
293 ' "execution_count": null,\n'
301 ' "print(\\"foo\\")"\n'
305 ' "cell_type": "code",\n'
306 ' "execution_count": null,\n'
313 ' "interpreter": {\n'
314 ' "hash": "e758f3098b5b55f4d87fe30bbdc1367f20f246b483f96267ee70e6c40cb185d8"\n' # noqa:B950
317 ' "display_name": "Python 3.8.10 64-bit (\'black\': venv)",\n'
318 ' "name": "python3"\n'
320 ' "language_info": {\n'
321 ' "name": "python",\n'
326 ' "nbformat_minor": 4\n'
329 assert result == expected
332 def test_entire_notebook_no_trailing_newline() -> None:
333 with open(DATA_DIR / "notebook_no_trailing_newline.ipynb", "rb") as fd:
334 content_bytes = fd.read()
335 content = content_bytes.decode()
336 result = format_file_contents(content, fast=True, mode=JUPYTER_MODE)
341 ' "cell_type": "code",\n'
342 ' "execution_count": null,\n'
350 ' "print(\\"foo\\")"\n'
354 ' "cell_type": "code",\n'
355 ' "execution_count": null,\n'
362 ' "interpreter": {\n'
363 ' "hash": "e758f3098b5b55f4d87fe30bbdc1367f20f246b483f96267ee70e6c40cb185d8"\n' # noqa: B950
366 ' "display_name": "Python 3.8.10 64-bit (\'black\': venv)",\n'
367 ' "name": "python3"\n'
369 ' "language_info": {\n'
370 ' "name": "python",\n'
375 ' "nbformat_minor": 4\n'
378 assert result == expected
381 def test_entire_notebook_without_changes() -> None:
382 with open(DATA_DIR / "notebook_without_changes.ipynb", "rb") as fd:
383 content_bytes = fd.read()
384 content = content_bytes.decode()
385 with pytest.raises(NothingChanged):
386 format_file_contents(content, fast=True, mode=JUPYTER_MODE)
389 def test_non_python_notebook() -> None:
390 with open(DATA_DIR / "non_python_notebook.ipynb", "rb") as fd:
391 content_bytes = fd.read()
392 content = content_bytes.decode()
393 with pytest.raises(NothingChanged):
394 format_file_contents(content, fast=True, mode=JUPYTER_MODE)
397 def test_empty_string() -> None:
398 with pytest.raises(NothingChanged):
399 format_file_contents("", fast=True, mode=JUPYTER_MODE)
402 def test_unparseable_notebook() -> None:
403 path = DATA_DIR / "notebook_which_cant_be_parsed.ipynb"
404 msg = rf"File '{re.escape(str(path))}' cannot be parsed as valid Jupyter notebook\."
405 with pytest.raises(ValueError, match=msg):
406 format_file_in_place(path, fast=True, mode=JUPYTER_MODE)
409 def test_ipynb_diff_with_change() -> None:
410 result = runner.invoke(
413 str(DATA_DIR / "notebook_trailing_newline.ipynb"),
415 f"--config={EMPTY_CONFIG}",
418 expected = "@@ -1,3 +1,3 @@\n %%time\n \n-print('foo')\n+print(\"foo\")\n"
419 assert expected in result.output
422 def test_ipynb_diff_with_no_change() -> None:
423 result = runner.invoke(
426 str(DATA_DIR / "notebook_without_changes.ipynb"),
428 f"--config={EMPTY_CONFIG}",
431 expected = "1 file would be left unchanged."
432 assert expected in result.output
435 def test_cache_isnt_written_if_no_jupyter_deps_single(
436 monkeypatch: MonkeyPatch, tmp_path: pathlib.Path
438 # Check that the cache isn't written to if Jupyter dependencies aren't installed.
439 jupyter_dependencies_are_installed.cache_clear()
440 nb = DATA_DIR / "notebook_trailing_newline.ipynb"
441 tmp_nb = tmp_path / "notebook.ipynb"
442 with open(nb) as src, open(tmp_nb, "w") as dst:
443 dst.write(src.read())
445 "black.jupyter_dependencies_are_installed", lambda verbose, quiet: False
447 result = runner.invoke(
448 main, [str(tmp_path / "notebook.ipynb"), f"--config={EMPTY_CONFIG}"]
450 assert "No Python files are present to be formatted. Nothing to do" in result.output
451 jupyter_dependencies_are_installed.cache_clear()
453 "black.jupyter_dependencies_are_installed", lambda verbose, quiet: True
455 result = runner.invoke(
456 main, [str(tmp_path / "notebook.ipynb"), f"--config={EMPTY_CONFIG}"]
458 assert "reformatted" in result.output
461 def test_cache_isnt_written_if_no_jupyter_deps_dir(
462 monkeypatch: MonkeyPatch, tmp_path: pathlib.Path
464 # Check that the cache isn't written to if Jupyter dependencies aren't installed.
465 jupyter_dependencies_are_installed.cache_clear()
466 nb = DATA_DIR / "notebook_trailing_newline.ipynb"
467 tmp_nb = tmp_path / "notebook.ipynb"
468 with open(nb) as src, open(tmp_nb, "w") as dst:
469 dst.write(src.read())
471 "black.files.jupyter_dependencies_are_installed", lambda verbose, quiet: False
473 result = runner.invoke(main, [str(tmp_path), f"--config={EMPTY_CONFIG}"])
474 assert "No Python files are present to be formatted. Nothing to do" in result.output
475 jupyter_dependencies_are_installed.cache_clear()
477 "black.files.jupyter_dependencies_are_installed", lambda verbose, quiet: True
479 result = runner.invoke(main, [str(tmp_path), f"--config={EMPTY_CONFIG}"])
480 assert "reformatted" in result.output
483 def test_ipynb_flag(tmp_path: pathlib.Path) -> None:
484 nb = DATA_DIR / "notebook_trailing_newline.ipynb"
485 tmp_nb = tmp_path / "notebook.a_file_extension_which_is_definitely_not_ipynb"
486 with open(nb) as src, open(tmp_nb, "w") as dst:
487 dst.write(src.read())
488 result = runner.invoke(
494 f"--config={EMPTY_CONFIG}",
497 expected = "@@ -1,3 +1,3 @@\n %%time\n \n-print('foo')\n+print(\"foo\")\n"
498 assert expected in result.output
501 def test_ipynb_and_pyi_flags() -> None:
502 nb = DATA_DIR / "notebook_trailing_newline.ipynb"
503 result = runner.invoke(
510 f"--config={EMPTY_CONFIG}",
513 assert isinstance(result.exception, SystemExit)
514 expected = "Cannot pass both `pyi` and `ipynb` flags!\n"
515 assert result.output == expected
518 def test_unable_to_replace_magics(monkeypatch: MonkeyPatch) -> None:
519 src = "%%time\na = 'foo'"
520 monkeypatch.setattr("black.handle_ipynb_magics.TOKEN_HEX", lambda _: "foo")
522 AssertionError, match="Black was not able to replace IPython magic"
524 format_cell(src, fast=True, mode=JUPYTER_MODE)