在版本控制下使用IPython笔记本

在版本控制下保持IPython笔记本的好策略是什么?

笔记本电脑的格式对于版本控制来说是非常合适的:如果想要对笔记本电脑和输出进行版本控制,那么这个工作起来相当好。 当人们只想控制input的版本,排除单元输出(也就是“构build产品”),这可能是大的二进制blob,尤其是对于电影和情节。 特别是,我试图find一个好的工作stream程:

  • 允许我在包含或排除输出之间进行select,
  • 如果我不想要,可以防止我意外地提交输出,
  • 允许我保持输出在我的本地版本,
  • 允许我使用我的版本控制系统查看input更改的时间(例如,如果我只是版本控制input,但是本地文件有输出,那么我希望能够看到input是否已经改变(需要提交)。使用版本控制状态命令将始终注册一个差异,因为本地文件有输出。)
  • 允许我从更新的清洁笔记本更新我的工作笔记本(其中包含输出)。 (更新)

如前所述,如果我select包括输出(例如,当使用nbviewer时,这是可取的),那么一切都很好。 问题是当我不想版本控制输出。 有一些工具和脚本剥离笔记本的输出,但是我经常遇到以下问题:

  1. 我不小心提交了一个版本的输出,从而污染我的存储库。
  2. 我清除输出使用版本控制,但真的宁愿输出在我的本地副本(有时需要一段时间重现例如)。
  3. Cell/All Output/Clear菜单选项相比,某些剥离输出的脚本会略微改变格式,从而在差异中产生不需要的噪音。 这是通过一些答案解决的。
  4. 当更改为干净版本的文件时,我需要find一些方法将这些更改合并到工作笔记本中,而无需重新运行所有内容。 (更新)

我已经考虑了几个我将在下面讨论的选项,但还没有find一个好的综合解决scheme。 完整的解决scheme可能需要对IPython进行一些更改,或者可能依赖于一些简单的外部脚本。 我目前使用的是mercurial ,但是想要一个也适用于git的解决scheme:理想的解决scheme是版本控制不可知的。

这个问题已经被讨论了很多次,但是从用户的angular度来看,并没有明确或清晰的解决scheme。 这个问题的答案应该提供明确的策略。 如果需要IPython的最新版本(甚至是开发版本)或简单安装的扩展,那就好了。

更新:我一直在玩我的修改后的笔记本版本,可以select使用Gregory Crosswhite的build议保存.clean版本。 这满足了我的大部分约束条件,但还是留下了以下内容:

  1. 这还不是一个标准的解决scheme(需要修改ipython源代码,是否有一种方法可以通过简单的扩展来实现这种行为?需要某种保存钩子。
  2. 我在当前工作stream程中遇到的一个问题是拉动变化。 这些将进入.clean文件,然后需要以某种方式集成到我的工作版本中。 (当然,我总是可以重新执行笔记本,但是这可能会很痛苦,特别是如果某些结果依赖于长时间的计算,并行计算等等)。我对如何解决这个问题还没有一个好主意。 也许像ipycache这样的扩展工作stream可能会工作,但这似乎有点太复杂。

笔记

删除(剥离)输出

  • 当笔记本正在运行时,可以使用Cell/All Output/Clear菜单选项来删除输出。
  • 有一些用于删除输出的脚本,例如脚本nbstripout.py删除输出,但不会产生与使用笔记本界面相同的输出。 这最终被包含在ipython / nbconvert repo中,但是这已经被封闭,声称现在包含在ipython / ipython中的更改 ,但相应的function似乎还没有包括在内。 (更新)那么, Gregory Crosswhite的解决scheme表明,即使不调用ipython / nbconvert ,这个方法也很容易实现 ,所以如果可以正确挂接,这种方法可能是可行的。(附加到每个版本控制系统上,似乎不是一个好主意 – 这应该挂钩到笔记本电脑机制。)

新闻组

  • 关于版本控制笔记本格式的思考 。

问题

  • 977:笔记本function请求(打开) 。
  • 1280:清除全部保存选项(打开) 。 (从这个讨论开始 )
  • 3295:自动导出的笔记本电脑:只导出明确标记的单元格(closures) 。 解决扩展11添加writeandexecute魔术(合并) 。

拉取请求

  • 1621:清除“清除所有输出”(合并)中的[]提示号码 。 (另见2519(合并)) 。
  • 1563:clear_output改进(合并) 。
  • 3065:笔记本电脑的差异化(closures) 。
  • 3291:添加选项以在保存时跳过输出单元格。 (closures) 。 这似乎是非常相关的,但是closures使用“干净/污点”filter的build议。 一个相关的问题,如果你想在运行git diff之前剥离输出,你可以使用什么? 似乎没有得到回答。
  • WIP:笔记本保存挂钩(closures) 。
  • 3747:ipynb – > ipynb变压器(closures) 。 这是重新在4175年 。
  • 4175:nbconvert:Jinjaless出口商基地(合并) 。
  • 142:如果没有input(打开),则在nbstripout中使用STDIN 。

这是我用git的解决scheme。 它允许你像往常一样添加和提交(和差异):这些操作不会改变你的工作树,同时(重新)运行笔记本不会改变你的git历史。

虽然这可能适用于其他VCS,但我知道它不能满足您的要求(至lessVSC不可知论)。 尽pipe这对我来说是完美的,虽然没有什么特别的辉煌,而且很多人可能已经在使用它了,但是我没有find明确的指示来说明如何通过search来实现它。 所以这可能对其他人有用。

  1. 将某个文件保存在某个地方(对于以下内容,我们假设~/bin/ipynb_output_filter.py
  2. 使其可执行( chmod +x ~/bin/ipynb_output_filter.py
  3. 使用以下内容创build文件~/.gitattributes

     *.ipynb filter=dropoutput_ipynb 
  4. 运行以下命令:

     git config --global core.attributesfile ~/.gitattributes git config --global filter.dropoutput_ipynb.clean ~/bin/ipynb_output_filter.py git config --global filter.dropoutput_ipynb.smudge cat 

完成!

限制:

  • 它只适用于git
  • 在混帐,如果你在分支somebranch ,你做git checkout otherbranch; git checkout somebranch git checkout otherbranch; git checkout somebranch ,你通常期望工作树不变。 在这里,您将失去两个分支之间信号源不同的笔记本的输出和单元编号。
  • 更一般地说,输出并不是版本化的,就像Gregory的解决scheme一样。 为了不会在每次执行任何涉及签出的操作时将其扔掉,可以通过将其存储在单独的文件中来更改该方法(但请注意,在上面的代码运行时,提交ID是未知的!),并可能版本化(但注意这将需要比git commit notebook_file.ipynb更多的东西,但它至less会保持免费的base64垃圾git diff notebook_file.ipynb )。
  • 说,顺便说一句,如果你拉代码(即由别人提交不使用这种方法),其中包含一些输出,输出正常签出。 只有当地生产的产品丢失了。

我的解决scheme反映了我个人不喜欢保留生成的版本的事实 – 注意,执行涉及输出的合并几乎可以保证使输出您的生产力无效两者兼而有之。

编辑:

  • 如果你按照我的build议采用了解决scheme – 也就是全局的话 – 你将会遇到一些你想要版本输出的git仓库的麻烦。 所以,如果你想禁用特定的git仓库的输出过滤,只需在其中创build一个文件.git / info / attributes

    **。ipynb filter =

作为内容。 显然,以相同的方式可以做相反的事情: 为特定的存储库启用过滤。

  • 现在代码保存在它自己的git仓库中

  • 如果上面的说明导致ImportErrors,请尝试在脚本的path之前添加“ipython”:

     git config --global filter.dropoutput_ipynb.clean ipython ~/bin/ipynb_output_filter.py 

编辑 :2016年5月(更新二月2017):有几个替代我的脚本 – 为了完整性,这里是我知道的那些列表: nbstripout ( 其他 变体 ), nbstrip , jq 。

我们有一个产品是Jupyter笔记本的合作项目,我们在过去的六个月里使用了一个很好的方法:我们激活自动保存.py文件并跟踪.ipynb文件和.py文件。

这样,如果有人想查看/下载最新的笔记本,他们可以通过github或nbviewer做到这一点,如果有人想看看笔记本代码是如何改变的,他们可以看看.py文件的变化。

对于Jupyter笔记本服务器 ,这可以通过添加这些行来完成

 import os from subprocess import check_call def post_save(model, os_path, contents_manager): """post-save hook for converting notebooks to .py scripts""" if model['type'] != 'notebook': return # only do this for notebooks d, fname = os.path.split(os_path) check_call(['jupyter', 'nbconvert', '--to', 'script', fname], cwd=d) c.FileContentsManager.post_save_hook = post_save 

jupyter_notebook_config.py文件并重新启动笔记本服务器。

如果您不确定在哪个目录中find您的jupyter_notebook_config.py文件,则可以键入jupyter --config-dir ,如果您没有在其中find该文件,则可以通过键入jupyter notebook --generate-config

对于Ipython 3笔记本服务器 ,这可以通过添加行来完成

 import os from subprocess import check_call def post_save(model, os_path, contents_manager): """post-save hook for converting notebooks to .py scripts""" if model['type'] != 'notebook': return # only do this for notebooks d, fname = os.path.split(os_path) check_call(['ipython', 'nbconvert', '--to', 'script', fname], cwd=d) c.FileContentsManager.post_save_hook = post_save 

ipython_notebook_config.py文件并重新启动笔记本服务器。 这些行来自github 提供的@minrk提供的答案, @ dror也包含在他的SO答案中。

对于Ipython 2笔记本服务器 ,这可以通过使用以下命令启动服务器来完成:

 ipython notebook --script 

或者通过添加该行

 c.FileNotebookManager.save_script = True 

ipython_notebook_config.py文件并重新启动笔记本服务器。

如果您不确定在哪个目录中find您的ipython_notebook_config.py文件,则可以键入ipython locate profile default ,如果您没有在其中find该文件,可以通过键入ipython profile create来创build它。

这是我们在github上使用这种方法的项目 :这里有一个github的例子,探讨笔记本最近的变化 。

我们对此非常满意。

我已经创build了基于MinRKs要点的nbstripout ,它支持Git和Mercurial(感谢mforbes)。 它可以在命令行上独立使用,也可以作为filter使用,通过nbstripout install / nbstripout uninstall可以轻松地(不)安装在当前的存储库中。

从PyPI或简单地获取它

 pip install nbstripout 

Cyrille Rossant为IPython 3.0提供了一个新的解决scheme,它保留了markdown文件而不是基于json的ipymd文件:

https://github.com/rossant/ipymd

正如所指出的那样,– --script3.x被弃用。 这种方法可以通过应用后保存钩子来使用。 具体而言,将以下内容添加到ipython_notebook_config.py

 import os from subprocess import check_call def post_save(model, os_path, contents_manager): """post-save hook for converting notebooks to .py scripts""" if model['type'] != 'notebook': return # only do this for notebooks d, fname = os.path.split(os_path) check_call(['ipython', 'nbconvert', '--to', 'script', fname], cwd=d) c.FileContentsManager.post_save_hook = post_save 

代码取自#8009 。

(2017-02)

策略

  • on_commit():
    • 剥离输出> name.ipynb( nbstripout ,)
    • 剥离输出> name.clean.ipynb( nbstripout ,)
    • 总是nbconvert到Python:name.ipynb.py( nbconvert
    • 总是转换为降价:name.ipynb.md( nbconvertipymd
  • vcs.configure():
    • git difftool,mergetool:nbdiff和nbdmer从nbdime

工具

  • nbstripoutnbstripout笔记本的输出
    • src: https : //gist.github.com/minrk/6176788
    • src: https : //github.com/kynan/nbstripout
      • pip install nbstripout; nbstripout install
  • ipynb_output_filteripynb_output_filter笔记本的输出
    • src: https : //github.com/toobaz/ipynb_output_filter/blob/master/ipynb_output_filter.py
  • ipymd :{Jupyter,Markdown,O'Reilly Atlas Markdown,OpenDocument,.py}之间的转换
    • src: https : //github.com/rossant/ipymd
  • nbdime :“Jupyter笔记本的差异和合并工具”。 (2015)
    • src: https : //github.com/jupyter/nbdime
    • 文档: http : //nbdime.readthedocs.io/
      • nbdiff :以对terminal友好的方式比较笔记本
        • nbdime nbdiff作为一个git diff工具 : https : //nbdime.readthedocs.io/en/latest/#git-integration-quickstart
      • nbmerge :笔记本自动冲突解决scheme的三方合并
        • nbdime nbmerge作为一个混合合并工具
      • nbdiff-web :向您显示笔记本的丰富渲染差异
      • nbmerge-web :为您提供一个基于Web的笔记本电脑的三路合并工具
      • nbshow :以terminal友好的方式呈现单个笔记本

不幸的是,我对Mercurial不太了解,但是我可以给你一个适用于Git的解决scheme,希望你能把我的Git命令翻译成他们的Mercurial等价物。

对于后台,在Git中, add命令将已经对文件进行的更改存储到临时区域中。 一旦你完成了这个任务,任何后续的文件修改都会被Git忽略,除非你也告诉它将它们分级。 因此,下面的脚本,对于每个给定的文件,将所有的outputsprompt_number sections去掉,对分离的文件进行prompt_number sections ,然后恢复原来的:

注意:如果运行这样会得到一个类似于ImportError: No module named IPython.nbformat的错误消息ImportError: No module named IPython.nbformat ,那么使用ipython来运行脚本而不是python

 from IPython.nbformat import current import io from os import remove, rename from shutil import copyfile from subprocess import Popen from sys import argv for filename in argv[1:]: # Backup the current file backup_filename = filename + ".backup" copyfile(filename,backup_filename) try: # Read in the notebook with io.open(filename,'r',encoding='utf-8') as f: notebook = current.reads(f.read(),format="ipynb") # Strip out all of the output and prompt_number sections for worksheet in notebook["worksheets"]: for cell in worksheet["cells"]: cell.outputs = [] if "prompt_number" in cell: del cell["prompt_number"] # Write the stripped file with io.open(filename, 'w', encoding='utf-8') as f: current.write(notebook,f,format='ipynb') # Run git add to stage the non-output changes print("git add",filename) Popen(["git","add",filename]).wait() finally: # Restore the original file; remove is needed in case # we are running in windows. remove(filename) rename(backup_filename,filename) 

一旦脚本运行在您想要提交更改的文件上,就运行git commit

我用一个非常实用的方法; 这对于几个笔记本电脑在几方面都很好。 它甚至可以让我“转移”笔记本电脑。 它适用于Windows作为Unix / MacOS。
艾尔认为这很简单,就是解决上面的问题…

概念

基本上,不要跟踪.ipnyb文件,只有相应的.py文件。
通过使用--script选项启动笔记本服务器 ,笔记本保存时会自动创build/保存该文件。

这些.py文件包含所有的input。 非代码和单元格边界一样被保存到注释中。 这些文件可以读取/导入(并拖动)到笔记本服务器(重新)创build一个笔记本。 只有产出不见了; 直到它重新运行。

我个人使用mercurial来版本跟踪.py文件; 并使用正常(命令行)命令来添加,检入(ect)。 大多数其他(D)VCS将允许这样做。

现在简单地跟踪历史; .py是小的,文字和简单的差异。 有一段时间,我们需要一个克隆(只是分支,在那里启动第二个笔记本服务器),或者一个旧版本(检出并导入笔记本服务器)等等。

提示与技巧

  • * .ipynb添加到' .hgignore ',所以Mercurial知道它可以忽略这些文件
  • 创build一个(bash)脚本启动服务器(使用--script选项)并进行版本跟踪
  • 保存一个笔记本不会保存.py文件,但不会将其检入。
    • 这是一个缺点 :人们可以忘记这一点
    • 这也是一个function :可以保存一个笔记本(并在以后继续),而不用集群存储库历史logging。

祝福

  • 在笔记本仪表板上有一个用于检入/添加/等的button将会很好
  • 结帐(例如) file@date+rev.py )应该是有帮助的这将是很多工作要补充的; 也许我会这样做一次。 到现在为止,我只是手工做的。

为了跟上Pietro Battiston的优秀脚本,如果你得到一个像这样的Unicodeparsing错误:

 Traceback (most recent call last): File "/Users/kwisatz/bin/ipynb_output_filter.py", line 33, in <module> write(json_in, sys.stdout, NO_CONVERT) File "/Users/kwisatz/anaconda/lib/python2.7/site-packages/IPython/nbformat/__init__.py", line 161, in write fp.write(s) UnicodeEncodeError: 'ascii' codec can't encode character u'\u2014' in position 11549: ordinal not in range(128) 

您可以在脚本的开头添加:

 reload(sys) sys.setdefaultencoding('utf8') 

我做了阿尔伯特和里奇所做的 – 不要版本.ipynb文件(因为这些文件可能包含图像,这将变得凌乱)。 相反,要么始终运行ipython notebook --script要么将c.FileNotebookManager.save_script = True放在configuration文件中,以便在保存笔记本时始终创build(可版本化的) .py文件。

为了重新生成笔记本(在签出回购或切换分支之后),我把脚本py_file_to_notebooks.py放到我存储笔记本的目录中。

现在,检出一个回购后,只需运行python py_file_to_notebooks.py来生成ipynb文件。 切换分支后,您可能需要运行python py_file_to_notebooks.py -ov来覆盖现有的ipynb文件。

为了安全起见,将*.ipynb添加到.gitignore文件也是很好的*.ipynb

编辑:我不再这样做,因为(一)你必须从py文件重新生成你的笔记本每次你检查一个分支和(乙)还有其他的东西,如降低笔记本电脑丢失。 我使用gitfilter从笔记本中除去输出。 讨论如何做到这一点是在这里 。

好吧,看起来目前最好的解决scheme,就像在这里讨论的一样,在gitfilter中,在提交时自动剥离ipynb文件的输出。

以下是我所做的工作(从该讨论中复制而来):

我修改了cfriedline的nbstripout文件,当你无法导入最新的IPython时,会给出一个信息错误: https : //github.com/petered/plato/blob/fb2f4e252f50c79768920d0e47b870a8d799e92b/notebooks/config/strip_notebook_output并将它添加到我的回购中,让我们在./relative/path/to/strip_notebook_output

还将文件.gitattributes文件添加到回购的根目录,其中包含:

 *.ipynb filter=stripoutput 

并创build了一个setup_git_filters.sh包含

 git config filter.stripoutput.clean "$(git rev-parse --show-toplevel)/relative/path/to/strip_notebook_output" git config filter.stripoutput.smudge cat git config filter.stripoutput.required true 

并运行source setup_git_filters.sh 。 花哨的$(git rev-parse …)是在任何(Unix)机器上find你的回购的本地path。

经过挖掘,我终于在Jupyter文档中发现了这个相对简单的预保存钩子 。 它剥离单元格输出数据。 您必须将其粘贴到jupyter_notebook_config.py文件(请参阅下面的说明)。

 def scrub_output_pre_save(model, **kwargs): """scrub output before saving notebooks""" # only run on notebooks if model['type'] != 'notebook': return # only run on nbformat v4 if model['content']['nbformat'] != 4: return for cell in model['content']['cells']: if cell['cell_type'] != 'code': continue cell['outputs'] = [] cell['execution_count'] = None # Added by binaryfunt: if 'collapsed' in cell['metadata']: cell['metadata'].pop('collapsed', 0) c.FileContentsManager.pre_save_hook = scrub_output_pre_save 

从Rich Signell的回答 :

如果您不确定在哪个目录中find您的jupyter_notebook_config.py文件,则可以键入jupyter --config-dir [到命令提示符/terminal],如果您没有在其中find该文件,则可以通过打字jupyter notebook --generate-config

我已经构build了python包来解决这个问题

https://github.com/brookisme/gitnb

它提供了一个用git启发的语法的CLI来跟踪/更新/ diff你的git仓库里的笔记本。

下面是一个例子

 # add a notebook to be tracked gitnb add SomeNotebook.ipynb # check the changes before commiting gitnb diff SomeNotebook.ipynb # commit your changes (to your git repo) gitnb commit -am "I fixed a bug" 

请注意,最后一步,我使用“gitnb commit”的地方是提交给你的git仓库。 它本质上是一个包装

 # get the latest changes from your python notebooks gitnb update # commit your changes ** this time with the native git commit ** git commit -am "I fixed a bug" 

还有更多的方法,并且可以进行configuration,使得每个阶段需要或多或less的用户input,但这是一般的想法。