Git für Jupyter Notebooks konfigurieren

Im Notebook-Dateiformat nbformat können auch die Ergebnisse der Berechnungen gespeichert werden. Dies können auch Base-64-codierte Blobs für Bilder und andere Binärdaten sein, die üblicherweise nicht in eine Versionsverwaltung übernommen werden sollen. Diese können zwar manuell entfernt werden mit Cell ‣ All Output ‣ Clear, ihr müsst diese Schritte jedoch vor jedem git add ausführen, und es löst auch eine zweite Ursache für das Rauschen in git diff nicht, nämlich dasjeinige in den Metadaten.

Um nun systematisch vergleichbare Versionen von Notebooks in der Versionsverwaltung zu erhalten, können wir jq verwenden, einen leichtgewichtigen JSON-Prozessor. Zwar benötigt man einige Zeit um jq einzurichten da es eine eigene eine eigene Abfrage-/Filtersprache mitbringt, aber meist sind schon die Standardeinstellungen gut gewählt.

Installation

jq für Debian/Ubuntu kann installiert werden mit:

$ sudo apt install jq

oder für macOS mit:

$ brew install jq

Beispiel

Ein typischer Aufruf ist:

jq --indent 1  \
  '(.cells [] | select (has ("output")) | .outputs) = []
  | (.cells [] | select (has ("execution_count")) | .execution_count) = null
  | .metadata = {"language_info": {"name": "python", "pygments_lexer": "ipython3"}}
  | .Cells []. Metadaten = {}
  '  example.ipynb

Jede Zeile innerhalb der einfachen Anführungszeichen definiert einen Filter – die erste wählt alle Einträge aus der Liste cells aus und löscht die Ausgaben. Der nächste Eintrag setzt alle Ausgaben zurück. Der dritte Schritt löscht die Metadaten des Notebooks und ersetzt sie durch ein Minimum an erforderlichen Informationen, damit das Notebook noch ohne Beanstandungen ausgeführt werden kann, folgendes eingeben:wenn es mit nbsphinx formatiert sind. Die vierte Filterzeile, .cells []. metadata = {}, löscht alle Metainformationen. Falls ihr bestimmte Metainformationen beibehalten wollt, könnt ihr dies hier angeben.

Einrichten

  1. Um euch die Arbeit zu erleichtern, könnt ihr einen Alias in der ~/.bashrc-Datei anlegen:

    alias nbstrip_jq="jq --indent 1 \
        '(.cells[] | select(has(\"outputs\")) | .outputs) = []  \
        | (.cells[] | select(has(\"execution_count\")) | .execution_count) = null  \
        | .metadata = {\"language_info\": {\"name\": \"python\", \"pygments_lexer\": \"ipython3\"}} \
        | .cells[].metadata = {} \
        '"
    
  2. Anschließend könnt ihr bequem im Terminal folgendes eingeben:

    $ nbstrip_jq example.ipynb > stripped.ipynb
    
  3. Wenn ihr von einem bereits vorhandenen Notebook ausgeht, solltet ihr zunächst einen filter-Commit hinzufügen, indem ihr einfach die neu gefilterte Version eures Notebooks ohne die unerwünschten Metadaten einlest. Nachdem ihr mit git add das Notebook hinzugefügt habt, könnt ihr mit git diff --cached schauen, ob der Filter auch wirklich gewirkt hat bevor ihr dann git commit -m 'filter' angebt.

  4. Wenn ihr diesen Filter für alle Git-Repositories verwenden wollt, könnt ihr euer Git auch global konfigurieren:

    1. Zunächst fügt ihr in ~/.gitconfig folgendes hinzu:

      [core]
      attributesfile = ~/.gitattributes
      
      [filter "nbstrip_jq"]
      clean = "jq --indent 1 \
              '(.cells[] | select(has(\"outputs\")) | .outputs) = []  \
              | (.cells[] | select(has(\"execution_count\")) | .execution_count) = null  \
              | .metadata = {\"language_info\": {\"name\": \"python\", \"pygments_lexer\": \"ipython3\"}} \
              | .cells[].metadata = {} \
              '"
      smudge = cat
      required = true
      
    2. Anschließend müsst ihr in ~/.gitattribute nur noch folgendes angeben:

      *.ipynb filter=nbstrip_jq
      

      Warnung

      Wenn ihr git rebase durchführen wollt, solltet ihr vorher die Zeile deaktivieren.

  5. Dennoch bleibt das Problem, dass git status Änderungen an Dateien anzeigt wenn die Zellen eines Notebook ausgeführt wurden, und dies obwohl git diff weiterhin keine Änderungen anzeigt. Daher sollte in der ~/.bashrc-Datei folgendes eingetragen um schnell das jeweilige Arbeitsverzeichnis reinigen zu können:

    function nbstrip_all_cwd {
        for nbfile in *.ipynb; do
            echo "$( nbstrip_jq $nbfile )" > $nbfile
        done
        unset nbfile
    }