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)
30 def test_noop() -> None:
32 with pytest.raises(NothingChanged):
33 format_cell(src, fast=True, mode=JUPYTER_MODE)
36 @pytest.mark.parametrize("fast", [True, False])
37 def test_trailing_semicolon(fast: bool) -> None:
39 result = format_cell(src, fast=fast, mode=JUPYTER_MODE)
40 expected = 'foo = "a";'
41 assert result == expected
44 def test_trailing_semicolon_with_comment() -> None:
45 src = 'foo = "a" ; # bar'
46 result = format_cell(src, fast=True, mode=JUPYTER_MODE)
47 expected = 'foo = "a"; # bar'
48 assert result == expected
51 def test_trailing_semicolon_with_comment_on_next_line() -> None:
52 src = "import black;\n\n# this is a comment"
53 with pytest.raises(NothingChanged):
54 format_cell(src, fast=True, mode=JUPYTER_MODE)
57 def test_trailing_semicolon_indented() -> None:
58 src = "with foo:\n plot_bar();"
59 with pytest.raises(NothingChanged):
60 format_cell(src, fast=True, mode=JUPYTER_MODE)
63 def test_trailing_semicolon_noop() -> None:
65 with pytest.raises(NothingChanged):
66 format_cell(src, fast=True, mode=JUPYTER_MODE)
69 @pytest.mark.parametrize(
72 pytest.param(JUPYTER_MODE, id="default mode"),
74 replace(JUPYTER_MODE, python_cell_magics={"cust1", "cust1"}),
75 id="custom cell magics mode",
79 def test_cell_magic(mode: Mode) -> None:
80 src = "%%time\nfoo =bar"
81 result = format_cell(src, fast=True, mode=mode)
82 expected = "%%time\nfoo = bar"
83 assert result == expected
86 def test_cell_magic_noop() -> None:
88 with pytest.raises(NothingChanged):
89 format_cell(src, fast=True, mode=JUPYTER_MODE)
92 @pytest.mark.parametrize(
95 pytest.param(JUPYTER_MODE, id="default mode"),
97 replace(JUPYTER_MODE, python_cell_magics={"cust1", "cust1"}),
98 id="custom cell magics mode",
102 @pytest.mark.parametrize(
105 pytest.param("ls =!ls", "ls = !ls", id="System assignment"),
106 pytest.param("!ls\n'foo'", '!ls\n"foo"', id="System call"),
107 pytest.param("!!ls\n'foo'", '!!ls\n"foo"', id="Other system call"),
108 pytest.param("?str\n'foo'", '?str\n"foo"', id="Help"),
109 pytest.param("??str\n'foo'", '??str\n"foo"', id="Other help"),
111 "%matplotlib inline\n'foo'",
112 '%matplotlib inline\n"foo"',
113 id="Line magic with argument",
115 pytest.param("%time\n'foo'", '%time\n"foo"', id="Line magic without argument"),
117 "env = %env var", "env = %env var", id="Assignment to environment variable"
119 pytest.param("env = %env", "env = %env", id="Assignment to magic"),
122 def test_magic(src: str, expected: str, mode: Mode) -> None:
123 result = format_cell(src, fast=True, mode=mode)
124 assert result == expected
127 @pytest.mark.parametrize(
131 "%%html --isolated\n2+2",
132 "%%writefile e.txt\n meh\n meh",
135 def test_non_python_magics(src: str) -> None:
136 with pytest.raises(NothingChanged):
137 format_cell(src, fast=True, mode=JUPYTER_MODE)
140 def test_set_input() -> None:
142 with pytest.raises(NothingChanged):
143 format_cell(src, fast=True, mode=JUPYTER_MODE)
146 def test_input_already_contains_transformed_magic() -> None:
147 src = '%time foo()\nget_ipython().run_cell_magic("time", "", "foo()\\n")'
148 with pytest.raises(NothingChanged):
149 format_cell(src, fast=True, mode=JUPYTER_MODE)
152 def test_magic_noop() -> None:
154 with pytest.raises(NothingChanged):
155 format_cell(src, fast=True, mode=JUPYTER_MODE)
158 def test_cell_magic_with_magic() -> None:
159 src = "%%timeit -n1\nls =!ls"
160 result = format_cell(src, fast=True, mode=JUPYTER_MODE)
161 expected = "%%timeit -n1\nls = !ls"
162 assert result == expected
165 @pytest.mark.parametrize(
166 "mode, expected_output, expectation",
170 "%%custom_python_magic -n1 -n2\nx=2",
171 pytest.raises(NothingChanged),
172 id="No change when cell magic not registered",
175 replace(JUPYTER_MODE, python_cell_magics={"cust1", "cust1"}),
176 "%%custom_python_magic -n1 -n2\nx=2",
177 pytest.raises(NothingChanged),
178 id="No change when other cell magics registered",
181 replace(JUPYTER_MODE, python_cell_magics={"custom_python_magic", "cust1"}),
182 "%%custom_python_magic -n1 -n2\nx = 2",
184 id="Correctly change when cell magic registered",
188 def test_cell_magic_with_custom_python_magic(
189 mode: Mode, expected_output: str, expectation: ContextManager[object]
192 result = format_cell(
193 "%%custom_python_magic -n1 -n2\nx=2",
197 assert result == expected_output
200 def test_cell_magic_nested() -> None:
201 src = "%%time\n%%time\n2+2"
202 result = format_cell(src, fast=True, mode=JUPYTER_MODE)
203 expected = "%%time\n%%time\n2 + 2"
204 assert result == expected
207 def test_cell_magic_with_magic_noop() -> None:
208 src = "%%t -n1\nls = !ls"
209 with pytest.raises(NothingChanged):
210 format_cell(src, fast=True, mode=JUPYTER_MODE)
213 def test_automagic() -> None:
214 src = "pip install black"
215 with pytest.raises(NothingChanged):
216 format_cell(src, fast=True, mode=JUPYTER_MODE)
219 def test_multiline_magic() -> None:
220 src = "%time 1 + \\\n2"
221 with pytest.raises(NothingChanged):
222 format_cell(src, fast=True, mode=JUPYTER_MODE)
225 def test_multiline_no_magic() -> None:
227 result = format_cell(src, fast=True, mode=JUPYTER_MODE)
229 assert result == expected
232 def test_cell_magic_with_invalid_body() -> None:
233 src = "%%time\nif True"
234 with pytest.raises(NothingChanged):
235 format_cell(src, fast=True, mode=JUPYTER_MODE)
238 def test_empty_cell() -> None:
240 with pytest.raises(NothingChanged):
241 format_cell(src, fast=True, mode=JUPYTER_MODE)
244 def test_entire_notebook_empty_metadata() -> None:
245 with open(DATA_DIR / "notebook_empty_metadata.ipynb", "rb") as fd:
246 content_bytes = fd.read()
247 content = content_bytes.decode()
248 result = format_file_contents(content, fast=True, mode=JUPYTER_MODE)
253 ' "cell_type": "code",\n'
254 ' "execution_count": null,\n'
262 ' "print(\\"foo\\")"\n'
266 ' "cell_type": "code",\n'
267 ' "execution_count": null,\n'
275 ' "nbformat_minor": 4\n'
278 assert result == expected
281 def test_entire_notebook_trailing_newline() -> None:
282 with open(DATA_DIR / "notebook_trailing_newline.ipynb", "rb") as fd:
283 content_bytes = fd.read()
284 content = content_bytes.decode()
285 result = format_file_contents(content, fast=True, mode=JUPYTER_MODE)
290 ' "cell_type": "code",\n'
291 ' "execution_count": null,\n'
299 ' "print(\\"foo\\")"\n'
303 ' "cell_type": "code",\n'
304 ' "execution_count": null,\n'
311 ' "interpreter": {\n'
312 ' "hash": "e758f3098b5b55f4d87fe30bbdc1367f20f246b483f96267ee70e6c40cb185d8"\n' # noqa:B950
315 ' "display_name": "Python 3.8.10 64-bit (\'black\': venv)",\n'
316 ' "name": "python3"\n'
318 ' "language_info": {\n'
319 ' "name": "python",\n'
324 ' "nbformat_minor": 4\n'
327 assert result == expected
330 def test_entire_notebook_no_trailing_newline() -> None:
331 with open(DATA_DIR / "notebook_no_trailing_newline.ipynb", "rb") as fd:
332 content_bytes = fd.read()
333 content = content_bytes.decode()
334 result = format_file_contents(content, fast=True, mode=JUPYTER_MODE)
339 ' "cell_type": "code",\n'
340 ' "execution_count": null,\n'
348 ' "print(\\"foo\\")"\n'
352 ' "cell_type": "code",\n'
353 ' "execution_count": null,\n'
360 ' "interpreter": {\n'
361 ' "hash": "e758f3098b5b55f4d87fe30bbdc1367f20f246b483f96267ee70e6c40cb185d8"\n' # noqa: B950
364 ' "display_name": "Python 3.8.10 64-bit (\'black\': venv)",\n'
365 ' "name": "python3"\n'
367 ' "language_info": {\n'
368 ' "name": "python",\n'
373 ' "nbformat_minor": 4\n'
376 assert result == expected
379 def test_entire_notebook_without_changes() -> None:
380 with open(DATA_DIR / "notebook_without_changes.ipynb", "rb") as fd:
381 content_bytes = fd.read()
382 content = content_bytes.decode()
383 with pytest.raises(NothingChanged):
384 format_file_contents(content, fast=True, mode=JUPYTER_MODE)
387 def test_non_python_notebook() -> None:
388 with open(DATA_DIR / "non_python_notebook.ipynb", "rb") as fd:
389 content_bytes = fd.read()
390 content = content_bytes.decode()
391 with pytest.raises(NothingChanged):
392 format_file_contents(content, fast=True, mode=JUPYTER_MODE)
395 def test_empty_string() -> None:
396 with pytest.raises(NothingChanged):
397 format_file_contents("", fast=True, mode=JUPYTER_MODE)
400 def test_unparseable_notebook() -> None:
401 path = DATA_DIR / "notebook_which_cant_be_parsed.ipynb"
402 msg = rf"File '{re.escape(str(path))}' cannot be parsed as valid Jupyter notebook\."
403 with pytest.raises(ValueError, match=msg):
404 format_file_in_place(path, fast=True, mode=JUPYTER_MODE)
407 def test_ipynb_diff_with_change() -> None:
408 result = runner.invoke(
411 str(DATA_DIR / "notebook_trailing_newline.ipynb"),
415 expected = "@@ -1,3 +1,3 @@\n %%time\n \n-print('foo')\n" '+print("foo")\n'
416 assert expected in result.output
419 def test_ipynb_diff_with_no_change() -> None:
420 result = runner.invoke(
423 str(DATA_DIR / "notebook_without_changes.ipynb"),
427 expected = "1 file would be left unchanged."
428 assert expected in result.output
431 def test_cache_isnt_written_if_no_jupyter_deps_single(
432 monkeypatch: MonkeyPatch, tmp_path: pathlib.Path
434 # Check that the cache isn't written to if Jupyter dependencies aren't installed.
435 jupyter_dependencies_are_installed.cache_clear()
436 nb = DATA_DIR / "notebook_trailing_newline.ipynb"
437 tmp_nb = tmp_path / "notebook.ipynb"
438 with open(nb) as src, open(tmp_nb, "w") as dst:
439 dst.write(src.read())
441 "black.jupyter_dependencies_are_installed", lambda verbose, quiet: False
443 result = runner.invoke(main, [str(tmp_path / "notebook.ipynb")])
444 assert "No Python files are present to be formatted. Nothing to do" in result.output
445 jupyter_dependencies_are_installed.cache_clear()
447 "black.jupyter_dependencies_are_installed", lambda verbose, quiet: True
449 result = runner.invoke(main, [str(tmp_path / "notebook.ipynb")])
450 assert "reformatted" in result.output
453 def test_cache_isnt_written_if_no_jupyter_deps_dir(
454 monkeypatch: MonkeyPatch, tmp_path: pathlib.Path
456 # Check that the cache isn't written to if Jupyter dependencies aren't installed.
457 jupyter_dependencies_are_installed.cache_clear()
458 nb = DATA_DIR / "notebook_trailing_newline.ipynb"
459 tmp_nb = tmp_path / "notebook.ipynb"
460 with open(nb) as src, open(tmp_nb, "w") as dst:
461 dst.write(src.read())
463 "black.files.jupyter_dependencies_are_installed", lambda verbose, quiet: False
465 result = runner.invoke(main, [str(tmp_path)])
466 assert "No Python files are present to be formatted. Nothing to do" in result.output
467 jupyter_dependencies_are_installed.cache_clear()
469 "black.files.jupyter_dependencies_are_installed", lambda verbose, quiet: True
471 result = runner.invoke(main, [str(tmp_path)])
472 assert "reformatted" in result.output
475 def test_ipynb_flag(tmp_path: pathlib.Path) -> None:
476 nb = DATA_DIR / "notebook_trailing_newline.ipynb"
477 tmp_nb = tmp_path / "notebook.a_file_extension_which_is_definitely_not_ipynb"
478 with open(nb) as src, open(tmp_nb, "w") as dst:
479 dst.write(src.read())
480 result = runner.invoke(
488 expected = "@@ -1,3 +1,3 @@\n %%time\n \n-print('foo')\n" '+print("foo")\n'
489 assert expected in result.output
492 def test_ipynb_and_pyi_flags() -> None:
493 nb = DATA_DIR / "notebook_trailing_newline.ipynb"
494 result = runner.invoke(
503 assert isinstance(result.exception, SystemExit)
504 expected = "Cannot pass both `pyi` and `ipynb` flags!\n"
505 assert result.output == expected
508 def test_unable_to_replace_magics(monkeypatch: MonkeyPatch) -> None:
509 src = "%%time\na = 'foo'"
510 monkeypatch.setattr("black.handle_ipynb_magics.TOKEN_HEX", lambda _: "foo")
512 AssertionError, match="Black was not able to replace IPython magic"
514 format_cell(src, fast=True, mode=JUPYTER_MODE)