首页 文章

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

提问于
浏览
504

IPython笔记本电脑置于版本控制之下的好策略是什么?

笔记本格式非常适合版本控制:如果想要版本控制笔记本和输出,那么这非常有效 . 当人们只想对输入进行版本控制时,就会产生烦恼,排除可能是大型二进制blob的单元输出(也就是“构建产品”),特别是对于电影和情节 . 特别是,我试图找到一个良好的工作流程:

  • 允许我在包含或排除输出之间进行选择,
    如果我不想要它,

  • 会阻止我意外提交输出,

  • 允许我保持本地版本的输出,

  • 允许我查看当我使用我的版本控制系统更改输入时(即如果我只对版本控制输入但我的本地文件有输出,那么我希望能够看到输入是否已更改(需要使用版本控制状态命令将始终注册差异,因为本地文件有输出 . )

  • 允许我从更新的干净笔记本更新我的工作笔记本(包含输出) . (update)

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

  • 我意外地提交了一个带有输出的版本,从而污染了我的存储库 .

  • 我清除输出以使用版本控制,但实际上宁愿将输出保留在我的本地副本中(例如,有时需要一段时间才能重现) .

  • 一些剥离输出的脚本与 Cell/All Output/Clear 菜单选项相比稍微改变了格式,从而在差异中产生了不必要的噪声 . 这可以通过一些答案来解决 .

  • 当将更改提取到文件的干净版本时,我需要找到一些方法将这些更改合并到我的工作笔记本中,而无需重新运行所有内容 . (update)

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

这个问题已经多次讨论过,但从用户的角度来看,没有明确或明确的解决方案 . 这个问题的答案应该提供明确的策略 . 如果它需要最近的(甚至开发)版本的IPython或一个易于安装的扩展,那就没问题了 .

Update: 我一直在玩my modified notebook版本,可选择使用Gregory Crosswhite's suggestions保存每个保存的 .clean 版本 . 这满足了我的大多数约束,但是仍然没有解决以下问题:

  • 这还不是一个标准的解决方案(需要修改ipython源 . 有没有办法通过简单的扩展来实现这种行为?需要某种on-save钩子 .

  • 我对当前工作流程的一个问题是拉动变化 . 这些将进入 .clean 文件,然后需要以某种方式集成到我的工作版本中 . (当然,我总是可以重新执行笔记本,但这可能会很痛苦,特别是如果某些结果取决于长时间的计算,并行计算等) . 我还不知道如何解决这个问题 . 也许涉及像ipycache这样的扩展的工作流可能会起作用,但这似乎有点过于复杂 .

注意事项

删除(剥离)输出

  • 当笔记本电脑运行时,可以使用 Cell/All Output/Clear 菜单选项删除输出 .

  • 有一些用于删除输出的脚本,例如删除输出的脚本nbstripout.py,但不会产生与使用笔记本界面相同的输出 . 这最终包含在ipython/nbconvert repo中,但是已经关闭,说明这些更改现在包含在ipython/ipython中,但相应的功能似乎尚未包括在内 . (update) 话虽如此,Gregory Crosswhite's solution表明这很容易做到,即使没有调用ipython/nbconvert,所以如果可以正确地挂钩,这种方法可能是可行的 . (但是,将它附加到每个版本控制系统,似乎不是好主意 - 这应该以某种方式挂钩到笔记本机制 . )

新闻组

问题

拉请求

17 回答

  • 2

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

    https://github.com/brookisme/gitnb

    它提供了一个带有git启发语法的CLI,用于在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 repo . 它本质上是一个包装

    # 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"
    

    还有几种方法,并且可以进行配置,以便在每个阶段需要或多或少的用户输入,但这是一般的想法 .

  • 3

    在下面的帖子中讨论的想法如何,应该保留笔记本的输出,并且可能需要很长时间来生成它,并且它很方便,因为GitHub现在可以渲染笔记本 . 为导出.py文件添加了自动保存挂钩,用于差异和.html与不使用笔记本或git的团队成员共享 .

    https://towardsdatascience.com/version-control-for-jupyter-notebook-3e6cef13392d

  • 6

    (2017-02)

    strategies

    • on_commit():

    • 剥离输出> name.ipynb( nbstripout ,)

    • 剥离输出> name.clean.ipynb( nbstripout ,)

    • 总是 nbconvert 到python:name.ipynb.py( nbconvert

    • 始终转换为markdown:name.ipynb.md( nbconvertipymd

    • vcs.configure():

    • git difftool,mergetool:来自nbdime的nbdiff和nbmerge

    tools

  • 13

    要跟进Pietro Battiston的优秀脚本,如果你得到这样的Unicode解析错误:

    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')
    
  • 11

    好的,所以看起来当前最好的解决方案,根据讨论here,是使git过滤器在提交时自动剥离ipynb文件的输出 .

    以下是我为使其工作所做的工作(从该讨论中复制):

    我修改了cfriedline 's nbstripout file slightly to give an informative error when you can' t导入最新的IPython:https://github.com/petered/plato/blob/fb2f4e252f50c79768920d0e47b870a8d799e92b/notebooks/config/strip_notebook_output并将其添加到我的仓库中,让我们说 ./relative/path/to/strip_notebook_output

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

    *.ipynb filter=stripoutput
    

    并创建了一个 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)机器上找到你的repo的本地路径 .

  • 55

    这是来自Cyrille Rossant的IPython 3.0的新解决方案,它坚持使用markdown文件而不是基于json的ipymd文件:

    https://github.com/rossant/ipymd

  • 0

    只是遇到“jupytext”,看起来像一个完美的解决方案 . 它从笔记本生成一个.py文件,然后保持同步 . 您可以通过.py文件对版本控制,差异和合并输入进行版本控制,而不会丢失输出 . 当您打开笔记本时,它使用.py作为输入单元格,使用.ipynb作为输出 . 如果你想在git中包含输出,那么你可以添加ipynb .

    https://github.com/mwouts/jupytext

  • 108

    不幸的是,我对Mercurial了解不多,但是我可以给你一个可以与Git一起使用的解决方案,希望你能够将我的Git命令翻译成他们的Mercurial等价物 .

    对于后台,在Git中, add 命令将对文件所做的更改存储到暂存区域 . 完成此操作后,Git将忽略对该文件的任何后续更改,除非您告诉它也将其暂存 . 因此,以下脚本,对于每个给定文件,剥离所有 outputsprompt_number sections ,对剥离文件进行分阶段,然后恢复原始文件:

    NOTE: 如果运行此命令会收到类似 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 .

  • 3

    正如所指出的, --script3.x 中已弃用 . 可以通过应用post-save-hook来使用此方法 . 特别是,将以下内容添加到 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 .

  • 35

    我创建了nbstripout,基于MinRKs gist,它支持Git和Mercurial(感谢mforbes) . 它既可以在命令行中单独使用,也可以作为过滤器使用,可以通过 nbstripout install / nbstripout uninstall 轻松(非)安装在当前存储库中 .

    PyPI或简单地获取它

    pip install nbstripout
    
  • 0

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

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

    For Jupyter notebook servers ,这可以通过添加行来完成

    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 文件并重新启动笔记本服务器 .

    如果您不确定在哪个目录中找到 jupyter_notebook_config.py 文件,可以键入 jupyter --config-dir ,如果在那里找不到该文件,可以通过键入 jupyter notebook --generate-config 来创建它 .

    For Ipython 3 notebook servers ,这可以通过添加行来完成

    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 provided和@dror也包含在他的SO答案中 .

    For Ipython 2 notebook servers ,这可以通过使用以下命令启动服务器来完成:

    ipython notebook --script
    

    或者添加该行

    c.FileNotebookManager.save_script = True
    

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

    如果您不确定在哪个目录中找到 ipython_notebook_config.py 文件,可以键入 ipython locate profile default ,如果在那里找不到该文件,可以通过键入 ipython profile create 来创建它 .

    这是our project on github that is using this approach:这是一个github example of exploring recent changes to a notebook .

    我们对此非常满意 .

  • 0

    I finally found a productive and simple way to make Jupyter and Git play nicely together. 我还在迈出第一步,但我已经认为它比其他所有复杂的解决方案都要好得多 .

    Visual Studio Code是Microsoft的酷炫开源代码编辑器 . 它有一个很好的Python扩展,现在允许你import a Jupyter Notebook作为python代码 .

    将笔记本导入python文件后,所有代码和markdown将一起放在普通的python文件中,注释中带有特殊标记 . 您可以在下图中看到:

    VSCode editor with a notebook converted to python

    你的python文件只包含笔记本输入单元格的内容 . 输出将在拆分窗口中生成 . 你在笔记本中有纯粹的代码,当你执行它时它不会改变 . 没有与您的代码混合输出 . 没有奇怪的Json难以理解的格式来分析你的差异 .

    只需纯Python代码,您可以轻松识别每个差异 .

    我甚至不需要再编辑我的 .ipynb 文件了 . 我可以在 .gitignore 中添加 *.ipynb 行 .

    需要生成笔记本才能发布或与他人分享?没问题,只是在交互式python窗口中click the export button

    Exporting a python file to Notebook format

    我一直在使用它只有一天,但最后我可以愉快地使用Jupyter与Git .

    P.S . :VSCode代码完成比Jupyter好很多 .

  • 2

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

    虽然这可能适用于其他VCS,但我知道它不能满足您的要求(至少VSC不可知) . 尽管如此,它对我来说还是完美的,虽然没有什么特别精彩,很多人可能已经使用过它,但我没有找到关于如何通过Google搜索来实现它的明确指示 . 所以它可能对其他人有用 .

    • 在某处保存this content的文件(对于以下内容,我们假设 ~/bin/ipynb_output_filter.py

    • 使其可执行( chmod +x ~/bin/ipynb_output_filter.py

    • 使用以下内容创建文件 ~/.gitattributes

    *.ipynb    filter=dropoutput_ipynb
    
    • 运行以下命令:
    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
    

    完成!

    Limitations:

    • 它只适用于git
      在git中

    • ,如果你在分支 somebranch 并且你做 git checkout otherbranch; git checkout somebranch ,你通常希望工作树不变 . 相反,您将丢失两个分支之间源不同的笔记本的输出和单元格编号 .
      __一般来说,输出没有版本化,就像Gregory的解决方案一样 . 为了不是每次你做任何涉及结账的事情都把它扔掉,可以通过将它存储在单独的文件中来改变方法(但请注意,在运行上面的代码时,提交id是未知的!),并且可能对它们进行版本控制(但请注意,这需要的不仅仅是 git commit notebook_file.ipynb ,尽管它至少可以保持 git diff notebook_file.ipynb 免受base64垃圾的影响) .

    • 表示,顺便说一句,如果你确实拉代码(即由不使用此方法的其他人提交)包含一些输出,则输出会正常检出 . 只丢失本地产生的输出 .

    我的解决方案反映了这样一个事实,即我个人不喜欢将生成的内容保留为版本 - 请注意,涉及输出的合并几乎可以保证输出或 生产环境 力无效或两者兼而有之 .

    EDIT:

    • 如果你按照我的建议采用了解决方案 - 也就是说,全局 - 你会遇到麻烦,以防某些git repo你 want 到版本输出 . 因此,如果要禁用特定git存储库的输出过滤,只需创建在它里面有一个.git / info / attributes文件

    ** . ipynb filter =

    作为内容 . 显然,以相同的方式可以执行相反的操作:仅针对特定存储库启用过滤 .

    • 代码现在保存在自己的git repo

    • 如果上述说明导致ImportErrors,请尝试在脚本路径前添加“ipython”:

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

    EDIT :2016年5月(2017年2月更新):我的脚本有几种替代方案 - 为了完整性,这里列出了我所知道的:nbstripoutother variants),nbstripjq .

  • 8

    这个jupyter扩展使用户可以将jupyter笔记本直接推送到github .

    请看这里

    https://github.com/sat28/githubcommit

  • 6

    我做了Albert&Rich做的事情 - 不要版本.ipynb文件(因为这些文件可能包含混乱的图像) . 相反,要么始终运行 ipython notebook --script ,要么将 c.FileNotebookManager.save_script = True 放在配置文件中,以便在保存笔记本时始终创建(可版本化的) .py 文件 .

    要重新生成笔记本(在签出仓库或切换分支后),我将脚本py_file_to_notebooks.py放在我存储笔记本的目录中 .

    现在,在签出一个repo之后,只需运行 python py_file_to_notebooks.py 来生成ipynb文件 . 切换分支后,您可能必须运行 python py_file_to_notebooks.py -ov 来覆盖现有的ipynb文件 .

    为了安全起见,最好还将 *.ipynb 添加到 .gitignore 文件中 .

    编辑:我不再这样做了,因为(A)每次检查分支时都必须从py文件中重新生成笔记本,并且(B)还有其他东西,例如笔记本中的降价丢失 . 我改为使用git过滤器从笔记本中删除输出 . 关于如何做到这一点的讨论是here .

  • 2

    在挖掘之后,我终于找到了this relatively simple pre-save hook on the Jupyter docs . 它剥离单元输出数据 . 您必须将其粘贴到 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's answer

    如果您不确定在哪个目录中找到jupyter_notebook_config.py文件,可以键入jupyter --config-dir [进入命令提示符/终端],如果在那里找不到该文件,则可以创建它通过输入jupyter notebook --generate-config .

  • 2

    我用一种非常务实的方法;它适用于几个笔记本电脑,在几个方面 . 它甚至可以让我周围的笔记本电脑 . 它适用于Windows作为Unix / MacOS .
    Al认为很简单,就是解决上面的问题......

    概念

    基本上, not 跟踪 .ipnyb -files,只跟踪相应的 .py -files .
    通过使用 --script 选项启动笔记本电脑 - 服务器,保存笔记本时会自动创建/保存该文件 .

    那些 .py -files确实包含所有输入;非代码保存到注释中,单元格边框也是如此 . 可以将这些文件读取/导入(并拖动)到笔记本服务器中以(重新)创建笔记本 . 只有输出消失了;直到重新运行 .

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

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

    提示与技巧

    • 将* .ipynb添加到' .hgignore ',因此Mercurial知道它可以忽略这些文件

    • 创建一个(bash)脚本来启动服务器(使用 --script 选项)并对其进行版本跟踪

    • 保存笔记本会保存 .py 文件,但不会将其签入 .

    • 这是一个 drawback :人们可以忘记这一点

    • 这也是一个 feature :可以保存笔记本(并在以后继续)而无需集群存储库历史记录 .

    祝福

    • 在笔记本电脑仪表板上有一个用于登记/添加/等的按钮会很不错

    • 结账(例如) file@date+rev.py )应该会有所帮助 . 添加它会有很多工作要做;也许我会这样做一次 . 到现在为止,我只是手工完成 .

相关问题