]> git.madduck.net Git - etc/vim.git/blob - tests/test_ipynb.py

madduck's git repository

Every one of the projects in this repository is available at the canonical URL git://git.madduck.net/madduck/pub/<projectpath> — see each project's metadata for the exact URL.

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.

SSH access, as well as push access can be individually arranged.

If you use my repositories frequently, consider adding the following snippet to ~/.gitconfig and using the third clone URL listed for each project:

[url "git://git.madduck.net/madduck/"]
  insteadOf = madduck:

Support pytest 7 by fixing broken imports (GH-2705)
[etc/vim.git] / tests / test_ipynb.py
1 import pathlib
2 import re
3
4 from click.testing import CliRunner
5 from black.handle_ipynb_magics import jupyter_dependencies_are_installed
6 from black import (
7     main,
8     NothingChanged,
9     format_cell,
10     format_file_contents,
11     format_file_in_place,
12 )
13 import pytest
14 from black import Mode
15 from _pytest.monkeypatch import MonkeyPatch
16 from tests.util import DATA_DIR
17
18 pytestmark = pytest.mark.jupyter
19 pytest.importorskip("IPython", reason="IPython is an optional dependency")
20 pytest.importorskip("tokenize_rt", reason="tokenize-rt is an optional dependency")
21
22 JUPYTER_MODE = Mode(is_ipynb=True)
23
24 runner = CliRunner()
25
26
27 def test_noop() -> None:
28     src = 'foo = "a"'
29     with pytest.raises(NothingChanged):
30         format_cell(src, fast=True, mode=JUPYTER_MODE)
31
32
33 @pytest.mark.parametrize("fast", [True, False])
34 def test_trailing_semicolon(fast: bool) -> None:
35     src = 'foo = "a" ;'
36     result = format_cell(src, fast=fast, mode=JUPYTER_MODE)
37     expected = 'foo = "a";'
38     assert result == expected
39
40
41 def test_trailing_semicolon_with_comment() -> None:
42     src = 'foo = "a" ;  # bar'
43     result = format_cell(src, fast=True, mode=JUPYTER_MODE)
44     expected = 'foo = "a";  # bar'
45     assert result == expected
46
47
48 def test_trailing_semicolon_with_comment_on_next_line() -> None:
49     src = "import black;\n\n# this is a comment"
50     with pytest.raises(NothingChanged):
51         format_cell(src, fast=True, mode=JUPYTER_MODE)
52
53
54 def test_trailing_semicolon_indented() -> None:
55     src = "with foo:\n    plot_bar();"
56     with pytest.raises(NothingChanged):
57         format_cell(src, fast=True, mode=JUPYTER_MODE)
58
59
60 def test_trailing_semicolon_noop() -> None:
61     src = 'foo = "a";'
62     with pytest.raises(NothingChanged):
63         format_cell(src, fast=True, mode=JUPYTER_MODE)
64
65
66 def test_cell_magic() -> None:
67     src = "%%time\nfoo =bar"
68     result = format_cell(src, fast=True, mode=JUPYTER_MODE)
69     expected = "%%time\nfoo = bar"
70     assert result == expected
71
72
73 def test_cell_magic_noop() -> None:
74     src = "%%time\n2 + 2"
75     with pytest.raises(NothingChanged):
76         format_cell(src, fast=True, mode=JUPYTER_MODE)
77
78
79 @pytest.mark.parametrize(
80     "src, expected",
81     (
82         pytest.param("ls =!ls", "ls = !ls", id="System assignment"),
83         pytest.param("!ls\n'foo'", '!ls\n"foo"', id="System call"),
84         pytest.param("!!ls\n'foo'", '!!ls\n"foo"', id="Other system call"),
85         pytest.param("?str\n'foo'", '?str\n"foo"', id="Help"),
86         pytest.param("??str\n'foo'", '??str\n"foo"', id="Other help"),
87         pytest.param(
88             "%matplotlib inline\n'foo'",
89             '%matplotlib inline\n"foo"',
90             id="Line magic with argument",
91         ),
92         pytest.param("%time\n'foo'", '%time\n"foo"', id="Line magic without argument"),
93         pytest.param(
94             "env =  %env var", "env = %env var", id="Assignment to environment variable"
95         ),
96         pytest.param("env =  %env", "env = %env", id="Assignment to magic"),
97     ),
98 )
99 def test_magic(src: str, expected: str) -> None:
100     result = format_cell(src, fast=True, mode=JUPYTER_MODE)
101     assert result == expected
102
103
104 @pytest.mark.parametrize(
105     "src",
106     (
107         "%%bash\n2+2",
108         "%%html --isolated\n2+2",
109         "%%writefile e.txt\n  meh\n meh",
110     ),
111 )
112 def test_non_python_magics(src: str) -> None:
113     with pytest.raises(NothingChanged):
114         format_cell(src, fast=True, mode=JUPYTER_MODE)
115
116
117 def test_set_input() -> None:
118     src = "a = b??"
119     with pytest.raises(NothingChanged):
120         format_cell(src, fast=True, mode=JUPYTER_MODE)
121
122
123 def test_input_already_contains_transformed_magic() -> None:
124     src = '%time foo()\nget_ipython().run_cell_magic("time", "", "foo()\\n")'
125     with pytest.raises(NothingChanged):
126         format_cell(src, fast=True, mode=JUPYTER_MODE)
127
128
129 def test_magic_noop() -> None:
130     src = "ls = !ls"
131     with pytest.raises(NothingChanged):
132         format_cell(src, fast=True, mode=JUPYTER_MODE)
133
134
135 def test_cell_magic_with_magic() -> None:
136     src = "%%timeit -n1\nls =!ls"
137     result = format_cell(src, fast=True, mode=JUPYTER_MODE)
138     expected = "%%timeit -n1\nls = !ls"
139     assert result == expected
140
141
142 def test_cell_magic_nested() -> None:
143     src = "%%time\n%%time\n2+2"
144     result = format_cell(src, fast=True, mode=JUPYTER_MODE)
145     expected = "%%time\n%%time\n2 + 2"
146     assert result == expected
147
148
149 def test_cell_magic_with_magic_noop() -> None:
150     src = "%%t -n1\nls = !ls"
151     with pytest.raises(NothingChanged):
152         format_cell(src, fast=True, mode=JUPYTER_MODE)
153
154
155 def test_automagic() -> None:
156     src = "pip install black"
157     with pytest.raises(NothingChanged):
158         format_cell(src, fast=True, mode=JUPYTER_MODE)
159
160
161 def test_multiline_magic() -> None:
162     src = "%time 1 + \\\n2"
163     with pytest.raises(NothingChanged):
164         format_cell(src, fast=True, mode=JUPYTER_MODE)
165
166
167 def test_multiline_no_magic() -> None:
168     src = "1 + \\\n2"
169     result = format_cell(src, fast=True, mode=JUPYTER_MODE)
170     expected = "1 + 2"
171     assert result == expected
172
173
174 def test_cell_magic_with_invalid_body() -> None:
175     src = "%%time\nif True"
176     with pytest.raises(NothingChanged):
177         format_cell(src, fast=True, mode=JUPYTER_MODE)
178
179
180 def test_empty_cell() -> None:
181     src = ""
182     with pytest.raises(NothingChanged):
183         format_cell(src, fast=True, mode=JUPYTER_MODE)
184
185
186 def test_entire_notebook_empty_metadata() -> None:
187     with open(DATA_DIR / "notebook_empty_metadata.ipynb", "rb") as fd:
188         content_bytes = fd.read()
189     content = content_bytes.decode()
190     result = format_file_contents(content, fast=True, mode=JUPYTER_MODE)
191     expected = (
192         "{\n"
193         ' "cells": [\n'
194         "  {\n"
195         '   "cell_type": "code",\n'
196         '   "execution_count": null,\n'
197         '   "metadata": {\n'
198         '    "tags": []\n'
199         "   },\n"
200         '   "outputs": [],\n'
201         '   "source": [\n'
202         '    "%%time\\n",\n'
203         '    "\\n",\n'
204         '    "print(\\"foo\\")"\n'
205         "   ]\n"
206         "  },\n"
207         "  {\n"
208         '   "cell_type": "code",\n'
209         '   "execution_count": null,\n'
210         '   "metadata": {},\n'
211         '   "outputs": [],\n'
212         '   "source": []\n'
213         "  }\n"
214         " ],\n"
215         ' "metadata": {},\n'
216         ' "nbformat": 4,\n'
217         ' "nbformat_minor": 4\n'
218         "}\n"
219     )
220     assert result == expected
221
222
223 def test_entire_notebook_trailing_newline() -> None:
224     with open(DATA_DIR / "notebook_trailing_newline.ipynb", "rb") as fd:
225         content_bytes = fd.read()
226     content = content_bytes.decode()
227     result = format_file_contents(content, fast=True, mode=JUPYTER_MODE)
228     expected = (
229         "{\n"
230         ' "cells": [\n'
231         "  {\n"
232         '   "cell_type": "code",\n'
233         '   "execution_count": null,\n'
234         '   "metadata": {\n'
235         '    "tags": []\n'
236         "   },\n"
237         '   "outputs": [],\n'
238         '   "source": [\n'
239         '    "%%time\\n",\n'
240         '    "\\n",\n'
241         '    "print(\\"foo\\")"\n'
242         "   ]\n"
243         "  },\n"
244         "  {\n"
245         '   "cell_type": "code",\n'
246         '   "execution_count": null,\n'
247         '   "metadata": {},\n'
248         '   "outputs": [],\n'
249         '   "source": []\n'
250         "  }\n"
251         " ],\n"
252         ' "metadata": {\n'
253         '  "interpreter": {\n'
254         '   "hash": "e758f3098b5b55f4d87fe30bbdc1367f20f246b483f96267ee70e6c40cb185d8"\n'  # noqa:B950
255         "  },\n"
256         '  "kernelspec": {\n'
257         '   "display_name": "Python 3.8.10 64-bit (\'black\': venv)",\n'
258         '   "name": "python3"\n'
259         "  },\n"
260         '  "language_info": {\n'
261         '   "name": "python",\n'
262         '   "version": ""\n'
263         "  }\n"
264         " },\n"
265         ' "nbformat": 4,\n'
266         ' "nbformat_minor": 4\n'
267         "}\n"
268     )
269     assert result == expected
270
271
272 def test_entire_notebook_no_trailing_newline() -> None:
273     with open(DATA_DIR / "notebook_no_trailing_newline.ipynb", "rb") as fd:
274         content_bytes = fd.read()
275     content = content_bytes.decode()
276     result = format_file_contents(content, fast=True, mode=JUPYTER_MODE)
277     expected = (
278         "{\n"
279         ' "cells": [\n'
280         "  {\n"
281         '   "cell_type": "code",\n'
282         '   "execution_count": null,\n'
283         '   "metadata": {\n'
284         '    "tags": []\n'
285         "   },\n"
286         '   "outputs": [],\n'
287         '   "source": [\n'
288         '    "%%time\\n",\n'
289         '    "\\n",\n'
290         '    "print(\\"foo\\")"\n'
291         "   ]\n"
292         "  },\n"
293         "  {\n"
294         '   "cell_type": "code",\n'
295         '   "execution_count": null,\n'
296         '   "metadata": {},\n'
297         '   "outputs": [],\n'
298         '   "source": []\n'
299         "  }\n"
300         " ],\n"
301         ' "metadata": {\n'
302         '  "interpreter": {\n'
303         '   "hash": "e758f3098b5b55f4d87fe30bbdc1367f20f246b483f96267ee70e6c40cb185d8"\n'  # noqa: B950
304         "  },\n"
305         '  "kernelspec": {\n'
306         '   "display_name": "Python 3.8.10 64-bit (\'black\': venv)",\n'
307         '   "name": "python3"\n'
308         "  },\n"
309         '  "language_info": {\n'
310         '   "name": "python",\n'
311         '   "version": ""\n'
312         "  }\n"
313         " },\n"
314         ' "nbformat": 4,\n'
315         ' "nbformat_minor": 4\n'
316         "}"
317     )
318     assert result == expected
319
320
321 def test_entire_notebook_without_changes() -> None:
322     with open(DATA_DIR / "notebook_without_changes.ipynb", "rb") as fd:
323         content_bytes = fd.read()
324     content = content_bytes.decode()
325     with pytest.raises(NothingChanged):
326         format_file_contents(content, fast=True, mode=JUPYTER_MODE)
327
328
329 def test_non_python_notebook() -> None:
330     with open(DATA_DIR / "non_python_notebook.ipynb", "rb") as fd:
331         content_bytes = fd.read()
332     content = content_bytes.decode()
333     with pytest.raises(NothingChanged):
334         format_file_contents(content, fast=True, mode=JUPYTER_MODE)
335
336
337 def test_empty_string() -> None:
338     with pytest.raises(NothingChanged):
339         format_file_contents("", fast=True, mode=JUPYTER_MODE)
340
341
342 def test_unparseable_notebook() -> None:
343     path = DATA_DIR / "notebook_which_cant_be_parsed.ipynb"
344     msg = rf"File '{re.escape(str(path))}' cannot be parsed as valid Jupyter notebook\."
345     with pytest.raises(ValueError, match=msg):
346         format_file_in_place(path, fast=True, mode=JUPYTER_MODE)
347
348
349 def test_ipynb_diff_with_change() -> None:
350     result = runner.invoke(
351         main,
352         [
353             str(DATA_DIR / "notebook_trailing_newline.ipynb"),
354             "--diff",
355         ],
356     )
357     expected = "@@ -1,3 +1,3 @@\n %%time\n \n-print('foo')\n" '+print("foo")\n'
358     assert expected in result.output
359
360
361 def test_ipynb_diff_with_no_change() -> None:
362     result = runner.invoke(
363         main,
364         [
365             str(DATA_DIR / "notebook_without_changes.ipynb"),
366             "--diff",
367         ],
368     )
369     expected = "1 file would be left unchanged."
370     assert expected in result.output
371
372
373 def test_cache_isnt_written_if_no_jupyter_deps_single(
374     monkeypatch: MonkeyPatch, tmp_path: pathlib.Path
375 ) -> None:
376     # Check that the cache isn't written to if Jupyter dependencies aren't installed.
377     jupyter_dependencies_are_installed.cache_clear()
378     nb = DATA_DIR / "notebook_trailing_newline.ipynb"
379     tmp_nb = tmp_path / "notebook.ipynb"
380     with open(nb) as src, open(tmp_nb, "w") as dst:
381         dst.write(src.read())
382     monkeypatch.setattr(
383         "black.jupyter_dependencies_are_installed", lambda verbose, quiet: False
384     )
385     result = runner.invoke(main, [str(tmp_path / "notebook.ipynb")])
386     assert "No Python files are present to be formatted. Nothing to do" in result.output
387     jupyter_dependencies_are_installed.cache_clear()
388     monkeypatch.setattr(
389         "black.jupyter_dependencies_are_installed", lambda verbose, quiet: True
390     )
391     result = runner.invoke(main, [str(tmp_path / "notebook.ipynb")])
392     assert "reformatted" in result.output
393
394
395 def test_cache_isnt_written_if_no_jupyter_deps_dir(
396     monkeypatch: MonkeyPatch, tmp_path: pathlib.Path
397 ) -> None:
398     # Check that the cache isn't written to if Jupyter dependencies aren't installed.
399     jupyter_dependencies_are_installed.cache_clear()
400     nb = DATA_DIR / "notebook_trailing_newline.ipynb"
401     tmp_nb = tmp_path / "notebook.ipynb"
402     with open(nb) as src, open(tmp_nb, "w") as dst:
403         dst.write(src.read())
404     monkeypatch.setattr(
405         "black.files.jupyter_dependencies_are_installed", lambda verbose, quiet: False
406     )
407     result = runner.invoke(main, [str(tmp_path)])
408     assert "No Python files are present to be formatted. Nothing to do" in result.output
409     jupyter_dependencies_are_installed.cache_clear()
410     monkeypatch.setattr(
411         "black.files.jupyter_dependencies_are_installed", lambda verbose, quiet: True
412     )
413     result = runner.invoke(main, [str(tmp_path)])
414     assert "reformatted" in result.output
415
416
417 def test_ipynb_flag(tmp_path: pathlib.Path) -> None:
418     nb = DATA_DIR / "notebook_trailing_newline.ipynb"
419     tmp_nb = tmp_path / "notebook.a_file_extension_which_is_definitely_not_ipynb"
420     with open(nb) as src, open(tmp_nb, "w") as dst:
421         dst.write(src.read())
422     result = runner.invoke(
423         main,
424         [
425             str(tmp_nb),
426             "--diff",
427             "--ipynb",
428         ],
429     )
430     expected = "@@ -1,3 +1,3 @@\n %%time\n \n-print('foo')\n" '+print("foo")\n'
431     assert expected in result.output
432
433
434 def test_ipynb_and_pyi_flags() -> None:
435     nb = DATA_DIR / "notebook_trailing_newline.ipynb"
436     result = runner.invoke(
437         main,
438         [
439             str(nb),
440             "--pyi",
441             "--ipynb",
442             "--diff",
443         ],
444     )
445     assert isinstance(result.exception, SystemExit)
446     expected = "Cannot pass both `pyi` and `ipynb` flags!\n"
447     assert result.output == expected
448
449
450 def test_unable_to_replace_magics(monkeypatch: MonkeyPatch) -> None:
451     src = "%%time\na = 'foo'"
452     monkeypatch.setattr("black.handle_ipynb_magics.TOKEN_HEX", lambda _: "foo")
453     with pytest.raises(
454         AssertionError, match="Black was not able to replace IPython magic"
455     ):
456         format_cell(src, fast=True, mode=JUPYTER_MODE)