Как использовать nbconvert в качестве драйвера git textconv для эффективного управления версиями ноутбуков Jupyter

То, что я пытаюсь сделать и как оно отличается от аналогичных проблем

Я хотел бы управлять версиями Jupyter Notebooks с помощью Git. К сожалению, по умолчанию Git и Jupyter Notebooks не играют хорошо. Файл .ipynb - это .json файл, содержащий не только сам код Python, но и множество метаданных (например, счетчик выполнения ячейки) и вывод ячейки.

Большинство существующих решений (например, использование ноутбуков IPython под управлением версий) основаны на удалении вывода и метаданных из ноутбука. Это (i) по-прежнему поддерживает файловую структуру .json при различении, которая является болью для чтения, и (ii) означает, что такие функции, как отображение вывода в Github, не могут использоваться, поскольку выход удаляется перед фиксацией.

Моя идея такова: всякий раз, когда я запускаю git diff, Git автоматически использует jupyter nbconvert --to python filename.ipynb для преобразования из исходных файлов *.ipynb в *.py простые файлы python. Затем он должен обнаруживать изменения, которые влияют на сам код (не выполнение отсчетов и вывод, поскольку они удаляются nbconvert), не удаляя их фактически, и это должно сделать мои отличия более читабельными, чем для неконвертированных файлов .ipynb. Я не хочу, чтобы .py версия файла сохранялась навсегда; его следует использовать только для git diff. Я понимаю, что это должно быть возможно, просто указав nbconvert качестве nbconvert [diff] textconv, но я не смог заставить его работать.

Шаги, которые я выполнил до сих пор

Я создал файл с именем ipynb2py в /usr/local/bin содержащий

#!/bin/bash
jupyter nbconvert --to python $1

Я добавил следующее в файл .gitconfig

[diff "ipynb"]
    textconv = ipynb2py

и следующее в мой файл .gitattributes

*.ipynb diff=ipynb

назначить драйвер ipynb textconv всем файлам формата .ipynb.

Теперь я ожидал бы, что git diff автоматически выполнит преобразование (я знаю, что это будет существенно замедляться, но стоит иметь жизнеспособный вариант для ноутбуков VCing) каждый раз, когда я его запускаю, а затем показываю хороший читаемый diff, основанный только на различии между состояния ноутбука после преобразования.

Когда я делаю git diff, он сначала говорит [NbConvertApp] Converting notebook, который говорит мне, что Git запускает преобразование, как ожидалось. Однако преобразование завершается неудачно после длинной трассировки Python, заканчивающейся fatal: unable to read files to diff.

Непосредственно перед fatal сообщением об ошибке я получаю следующее

nbformat.reader.NotJSONError: Notebook does not appear to be JSON: '\n# coding: utf-8\n\n# In[ ]:\n\nimport...

Конечно, я подозревал, что там была проблема с тем, как мой ipynb2py сценарий ссылающегося nbconvert, но работает ipynb2py notebook.ipynb в моем репо работает отлично, так что не может быть причиной.

Что может вызвать эту ошибку? Каковы требования для действительного драйвера textconv отличного от возврата текстового файла?

Полная трассировка

git diff
[NbConvertApp] Converting notebook /var/folders/9t/p55_4b9971j4wwp14_45wy900000gn/T//lR5q08_notebook.ipynb to python
Traceback (most recent call last):
File "/Users/user/anaconda/lib/python3.6/site-packages/nbformat/reader.py", line 14, in parse_json
nb_dict = json.loads(s, **kwargs)
File "/Users/user/anaconda/lib/python3.6/json/__init__.py", line 354, in loads
return _default_decoder.decode(s)
File "/Users/user/anaconda/lib/python3.6/json/decoder.py", line 339, in decode
obj, end = self.raw_decode(s, idx=_w(s, 0).end())
File "/Users/user/anaconda/lib/python3.6/json/decoder.py", line 357, in raw_decode
raise JSONDecodeError("Expecting value", s, err.value) from None
json.decoder.JSONDecodeError: Expecting value: line 2 column 1 (char 1)

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
File "/Users/user/anaconda/bin/jupyter-nbconvert", line 11, in <module>
load_entry_point('nbconvert==5.1.1', 'console_scripts', 'jupyter-nbconvert')()
File "/Users/user/anaconda/lib/python3.6/site-packages/jupyter_core/application.py", line 266, in launch_instance
return super(JupyterApp, cls).launch_instance(argv=argv, **kwargs)
File "/Users/user/anaconda/lib/python3.6/site-packages/traitlets/config/application.py", line 658, in launch_instance
app.start()
File "/Users/user/anaconda/lib/python3.6/site-packages/nbconvert/nbconvertapp.py", line 305, in start
self.convert_notebooks()
File "/Users/user/anaconda/lib/python3.6/site-packages/nbconvert/nbconvertapp.py", line 473, in convert_notebooks
self.convert_single_notebook(notebook_filename)
File "/Users/user/anaconda/lib/python3.6/site-packages/nbconvert/nbconvertapp.py", line 444, in convert_single_notebook
output, resources = self.export_single_notebook(notebook_filename, resources, input_buffer=input_buffer)
File "/Users/user/anaconda/lib/python3.6/site-packages/nbconvert/nbconvertapp.py", line 373, in export_single_notebook
output, resources = self.exporter.from_filename(notebook_filename, resources=resources)
File "/Users/user/anaconda/lib/python3.6/site-packages/nbconvert/exporters/exporter.py", line 171, in from_filename
return self.from_file(f, resources=resources, **kw)
File "/Users/user/anaconda/lib/python3.6/site-packages/nbconvert/exporters/exporter.py", line 189, in from_file
return self.from_notebook_node(nbformat.read(file_stream, as_version=4), resources=resources, **kw)
File "/Users/user/anaconda/lib/python3.6/site-packages/nbformat/__init__.py", line 141, in read
return reads(fp.read(), as_version, **kwargs)
File "/Users/user/anaconda/lib/python3.6/site-packages/nbformat/__init__.py", line 74, in reads
nb = reader.reads(s, **kwargs)
File "/Users/user/anaconda/lib/python3.6/site-packages/nbformat/reader.py", line 58, in reads
nb_dict = parse_json(s, **kwargs)
File "/Users/user/anaconda/lib/python3.6/site-packages/nbformat/reader.py", line 17, in parse_json
raise NotJSONError(("Notebook does not appear to be JSON: %r" % s)[:77] + "...")
nbformat.reader.NotJSONError: Notebook does not appear to be JSON: '\n# coding: utf-8\n\n# In[ ]:\n\nimport...
fatal: unable to read files to diff

Ответ 1

Если вы внимательно прочитаете документацию по gitattributes (где textconv вариант конфигурации textconv), вы заметите, что программа преобразователя должна отправить вывод на стандартный вывод:

...

Выполнение текстовых различий двоичных файлов

Иногда желательно видеть разницу в текстовой версии некоторых двоичных файлов. Например, документ текстового процессора может быть преобразован в текстовое представление ASCII и отличается от отображаемого текста. Несмотря на то, что это преобразование теряет некоторую информацию, полученный diff полезен для просмотра человеком (но не может быть применен напрямую).

Параметр textconv config используется для определения программы для выполнения такого преобразования. Программа должна принимать один аргумент, имя файла для преобразования и создавать результирующий текст на stdout.

...

Поэтому вы должны добавить параметр --stdout в команду преобразования:

ipynb2py

#!/bin/bash
jupyter nbconvert --to python --stdout "$1"