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

Pin regex in docker to 2021.10.8 (GH-2579)
[etc/vim.git] / tests / test_ipynb.py
1 import re
2
3 from click.testing import CliRunner
4 from black.handle_ipynb_magics import jupyter_dependencies_are_installed
5 from black import (
6     main,
7     NothingChanged,
8     format_cell,
9     format_file_contents,
10     format_file_in_place,
11 )
12 import pytest
13 from black import Mode
14 from _pytest.monkeypatch import MonkeyPatch
15 from py.path import local
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     ),
94 )
95 def test_magic(src: str, expected: str) -> None:
96     result = format_cell(src, fast=True, mode=JUPYTER_MODE)
97     assert result == expected
98
99
100 @pytest.mark.parametrize(
101     "src",
102     (
103         "%%bash\n2+2",
104         "%%html --isolated\n2+2",
105     ),
106 )
107 def test_non_python_magics(src: str) -> None:
108     with pytest.raises(NothingChanged):
109         format_cell(src, fast=True, mode=JUPYTER_MODE)
110
111
112 def test_set_input() -> None:
113     src = "a = b??"
114     with pytest.raises(NothingChanged):
115         format_cell(src, fast=True, mode=JUPYTER_MODE)
116
117
118 def test_input_already_contains_transformed_magic() -> None:
119     src = '%time foo()\nget_ipython().run_cell_magic("time", "", "foo()\\n")'
120     with pytest.raises(NothingChanged):
121         format_cell(src, fast=True, mode=JUPYTER_MODE)
122
123
124 def test_magic_noop() -> None:
125     src = "ls = !ls"
126     with pytest.raises(NothingChanged):
127         format_cell(src, fast=True, mode=JUPYTER_MODE)
128
129
130 def test_cell_magic_with_magic() -> None:
131     src = "%%t -n1\nls =!ls"
132     result = format_cell(src, fast=True, mode=JUPYTER_MODE)
133     expected = "%%t -n1\nls = !ls"
134     assert result == expected
135
136
137 def test_cell_magic_nested() -> None:
138     src = "%%time\n%%time\n2+2"
139     result = format_cell(src, fast=True, mode=JUPYTER_MODE)
140     expected = "%%time\n%%time\n2 + 2"
141     assert result == expected
142
143
144 def test_cell_magic_with_magic_noop() -> None:
145     src = "%%t -n1\nls = !ls"
146     with pytest.raises(NothingChanged):
147         format_cell(src, fast=True, mode=JUPYTER_MODE)
148
149
150 def test_automagic() -> None:
151     src = "pip install black"
152     with pytest.raises(NothingChanged):
153         format_cell(src, fast=True, mode=JUPYTER_MODE)
154
155
156 def test_multiline_magic() -> None:
157     src = "%time 1 + \\\n2"
158     with pytest.raises(NothingChanged):
159         format_cell(src, fast=True, mode=JUPYTER_MODE)
160
161
162 def test_multiline_no_magic() -> None:
163     src = "1 + \\\n2"
164     result = format_cell(src, fast=True, mode=JUPYTER_MODE)
165     expected = "1 + 2"
166     assert result == expected
167
168
169 def test_cell_magic_with_invalid_body() -> None:
170     src = "%%time\nif True"
171     with pytest.raises(NothingChanged):
172         format_cell(src, fast=True, mode=JUPYTER_MODE)
173
174
175 def test_empty_cell() -> None:
176     src = ""
177     with pytest.raises(NothingChanged):
178         format_cell(src, fast=True, mode=JUPYTER_MODE)
179
180
181 def test_entire_notebook_empty_metadata() -> None:
182     with open(DATA_DIR / "notebook_empty_metadata.ipynb", "rb") as fd:
183         content_bytes = fd.read()
184     content = content_bytes.decode()
185     result = format_file_contents(content, fast=True, mode=JUPYTER_MODE)
186     expected = (
187         "{\n"
188         ' "cells": [\n'
189         "  {\n"
190         '   "cell_type": "code",\n'
191         '   "execution_count": null,\n'
192         '   "metadata": {\n'
193         '    "tags": []\n'
194         "   },\n"
195         '   "outputs": [],\n'
196         '   "source": [\n'
197         '    "%%time\\n",\n'
198         '    "\\n",\n'
199         '    "print(\\"foo\\")"\n'
200         "   ]\n"
201         "  },\n"
202         "  {\n"
203         '   "cell_type": "code",\n'
204         '   "execution_count": null,\n'
205         '   "metadata": {},\n'
206         '   "outputs": [],\n'
207         '   "source": []\n'
208         "  }\n"
209         " ],\n"
210         ' "metadata": {},\n'
211         ' "nbformat": 4,\n'
212         ' "nbformat_minor": 4\n'
213         "}\n"
214     )
215     assert result == expected
216
217
218 def test_entire_notebook_trailing_newline() -> None:
219     with open(DATA_DIR / "notebook_trailing_newline.ipynb", "rb") as fd:
220         content_bytes = fd.read()
221     content = content_bytes.decode()
222     result = format_file_contents(content, fast=True, mode=JUPYTER_MODE)
223     expected = (
224         "{\n"
225         ' "cells": [\n'
226         "  {\n"
227         '   "cell_type": "code",\n'
228         '   "execution_count": null,\n'
229         '   "metadata": {\n'
230         '    "tags": []\n'
231         "   },\n"
232         '   "outputs": [],\n'
233         '   "source": [\n'
234         '    "%%time\\n",\n'
235         '    "\\n",\n'
236         '    "print(\\"foo\\")"\n'
237         "   ]\n"
238         "  },\n"
239         "  {\n"
240         '   "cell_type": "code",\n'
241         '   "execution_count": null,\n'
242         '   "metadata": {},\n'
243         '   "outputs": [],\n'
244         '   "source": []\n'
245         "  }\n"
246         " ],\n"
247         ' "metadata": {\n'
248         '  "interpreter": {\n'
249         '   "hash": "e758f3098b5b55f4d87fe30bbdc1367f20f246b483f96267ee70e6c40cb185d8"\n'  # noqa:B950
250         "  },\n"
251         '  "kernelspec": {\n'
252         '   "display_name": "Python 3.8.10 64-bit (\'black\': venv)",\n'
253         '   "name": "python3"\n'
254         "  },\n"
255         '  "language_info": {\n'
256         '   "name": "python",\n'
257         '   "version": ""\n'
258         "  }\n"
259         " },\n"
260         ' "nbformat": 4,\n'
261         ' "nbformat_minor": 4\n'
262         "}\n"
263     )
264     assert result == expected
265
266
267 def test_entire_notebook_no_trailing_newline() -> None:
268     with open(DATA_DIR / "notebook_no_trailing_newline.ipynb", "rb") as fd:
269         content_bytes = fd.read()
270     content = content_bytes.decode()
271     result = format_file_contents(content, fast=True, mode=JUPYTER_MODE)
272     expected = (
273         "{\n"
274         ' "cells": [\n'
275         "  {\n"
276         '   "cell_type": "code",\n'
277         '   "execution_count": null,\n'
278         '   "metadata": {\n'
279         '    "tags": []\n'
280         "   },\n"
281         '   "outputs": [],\n'
282         '   "source": [\n'
283         '    "%%time\\n",\n'
284         '    "\\n",\n'
285         '    "print(\\"foo\\")"\n'
286         "   ]\n"
287         "  },\n"
288         "  {\n"
289         '   "cell_type": "code",\n'
290         '   "execution_count": null,\n'
291         '   "metadata": {},\n'
292         '   "outputs": [],\n'
293         '   "source": []\n'
294         "  }\n"
295         " ],\n"
296         ' "metadata": {\n'
297         '  "interpreter": {\n'
298         '   "hash": "e758f3098b5b55f4d87fe30bbdc1367f20f246b483f96267ee70e6c40cb185d8"\n'  # noqa: B950
299         "  },\n"
300         '  "kernelspec": {\n'
301         '   "display_name": "Python 3.8.10 64-bit (\'black\': venv)",\n'
302         '   "name": "python3"\n'
303         "  },\n"
304         '  "language_info": {\n'
305         '   "name": "python",\n'
306         '   "version": ""\n'
307         "  }\n"
308         " },\n"
309         ' "nbformat": 4,\n'
310         ' "nbformat_minor": 4\n'
311         "}"
312     )
313     assert result == expected
314
315
316 def test_entire_notebook_without_changes() -> None:
317     with open(DATA_DIR / "notebook_without_changes.ipynb", "rb") as fd:
318         content_bytes = fd.read()
319     content = content_bytes.decode()
320     with pytest.raises(NothingChanged):
321         format_file_contents(content, fast=True, mode=JUPYTER_MODE)
322
323
324 def test_non_python_notebook() -> None:
325     with open(DATA_DIR / "non_python_notebook.ipynb", "rb") as fd:
326         content_bytes = fd.read()
327     content = content_bytes.decode()
328     with pytest.raises(NothingChanged):
329         format_file_contents(content, fast=True, mode=JUPYTER_MODE)
330
331
332 def test_empty_string() -> None:
333     with pytest.raises(NothingChanged):
334         format_file_contents("", fast=True, mode=JUPYTER_MODE)
335
336
337 def test_unparseable_notebook() -> None:
338     path = DATA_DIR / "notebook_which_cant_be_parsed.ipynb"
339     msg = rf"File '{re.escape(str(path))}' cannot be parsed as valid Jupyter notebook\."
340     with pytest.raises(ValueError, match=msg):
341         format_file_in_place(path, fast=True, mode=JUPYTER_MODE)
342
343
344 def test_ipynb_diff_with_change() -> None:
345     result = runner.invoke(
346         main,
347         [
348             str(DATA_DIR / "notebook_trailing_newline.ipynb"),
349             "--diff",
350         ],
351     )
352     expected = "@@ -1,3 +1,3 @@\n %%time\n \n-print('foo')\n" '+print("foo")\n'
353     assert expected in result.output
354
355
356 def test_ipynb_diff_with_no_change() -> None:
357     result = runner.invoke(
358         main,
359         [
360             str(DATA_DIR / "notebook_without_changes.ipynb"),
361             "--diff",
362         ],
363     )
364     expected = "1 file would be left unchanged."
365     assert expected in result.output
366
367
368 def test_cache_isnt_written_if_no_jupyter_deps_single(
369     monkeypatch: MonkeyPatch, tmpdir: local
370 ) -> None:
371     # Check that the cache isn't written to if Jupyter dependencies aren't installed.
372     jupyter_dependencies_are_installed.cache_clear()
373     nb = DATA_DIR / "notebook_trailing_newline.ipynb"
374     tmp_nb = tmpdir / "notebook.ipynb"
375     with open(nb) as src, open(tmp_nb, "w") as dst:
376         dst.write(src.read())
377     monkeypatch.setattr(
378         "black.jupyter_dependencies_are_installed", lambda verbose, quiet: False
379     )
380     result = runner.invoke(main, [str(tmpdir / "notebook.ipynb")])
381     assert "No Python files are present to be formatted. Nothing to do" in result.output
382     jupyter_dependencies_are_installed.cache_clear()
383     monkeypatch.setattr(
384         "black.jupyter_dependencies_are_installed", lambda verbose, quiet: True
385     )
386     result = runner.invoke(main, [str(tmpdir / "notebook.ipynb")])
387     assert "reformatted" in result.output
388
389
390 def test_cache_isnt_written_if_no_jupyter_deps_dir(
391     monkeypatch: MonkeyPatch, tmpdir: local
392 ) -> None:
393     # Check that the cache isn't written to if Jupyter dependencies aren't installed.
394     jupyter_dependencies_are_installed.cache_clear()
395     nb = DATA_DIR / "notebook_trailing_newline.ipynb"
396     tmp_nb = tmpdir / "notebook.ipynb"
397     with open(nb) as src, open(tmp_nb, "w") as dst:
398         dst.write(src.read())
399     monkeypatch.setattr(
400         "black.files.jupyter_dependencies_are_installed", lambda verbose, quiet: False
401     )
402     result = runner.invoke(main, [str(tmpdir)])
403     assert "No Python files are present to be formatted. Nothing to do" in result.output
404     jupyter_dependencies_are_installed.cache_clear()
405     monkeypatch.setattr(
406         "black.files.jupyter_dependencies_are_installed", lambda verbose, quiet: True
407     )
408     result = runner.invoke(main, [str(tmpdir)])
409     assert "reformatted" in result.output
410
411
412 def test_ipynb_flag(tmpdir: local) -> None:
413     nb = DATA_DIR / "notebook_trailing_newline.ipynb"
414     tmp_nb = tmpdir / "notebook.a_file_extension_which_is_definitely_not_ipynb"
415     with open(nb) as src, open(tmp_nb, "w") as dst:
416         dst.write(src.read())
417     result = runner.invoke(
418         main,
419         [
420             str(tmp_nb),
421             "--diff",
422             "--ipynb",
423         ],
424     )
425     expected = "@@ -1,3 +1,3 @@\n %%time\n \n-print('foo')\n" '+print("foo")\n'
426     assert expected in result.output
427
428
429 def test_ipynb_and_pyi_flags() -> None:
430     nb = DATA_DIR / "notebook_trailing_newline.ipynb"
431     result = runner.invoke(
432         main,
433         [
434             str(nb),
435             "--pyi",
436             "--ipynb",
437             "--diff",
438         ],
439     )
440     assert isinstance(result.exception, SystemExit)
441     expected = "Cannot pass both `pyi` and `ipynb` flags!\n"
442     assert result.output == expected
443
444
445 def test_unable_to_replace_magics(monkeypatch: MonkeyPatch) -> None:
446     src = "%%time\na = 'foo'"
447     monkeypatch.setattr("black.handle_ipynb_magics.TOKEN_HEX", lambda _: "foo")
448     with pytest.raises(
449         AssertionError, match="Black was not able to replace IPython magic"
450     ):
451         format_cell(src, fast=True, mode=JUPYTER_MODE)