├── .github └── workflows │ └── tests.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .pre-commit-hooks.yaml ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE.txt ├── MANIFEST.in ├── README.md ├── nbstripout ├── __init__.py ├── __main__.py ├── _nbstripout.py └── _utils.py ├── pre-commit ├── pyproject.toml ├── pytest.ini ├── setup.cfg ├── setup.py └── tests ├── conftest.py ├── e2e_notebooks ├── test_drop_empty_cells.ipynb ├── test_drop_empty_cells.ipynb.expected ├── test_drop_empty_cells_dontdrop.ipynb.expected ├── test_drop_tagged_cells.ipynb ├── test_drop_tagged_cells.ipynb.expected ├── test_drop_tagged_cells_dontdrop.ipynb.expected ├── test_empty_metadata.ipynb ├── test_empty_metadata.ipynb.expected ├── test_execution_timing.ipynb ├── test_execution_timing.ipynb.expected ├── test_invalid_json.ipynb ├── test_keep_metadata_keys.ipynb ├── test_keep_metadata_keys.ipynb.expected ├── test_max_size.ipynb ├── test_max_size.ipynb.expected ├── test_max_size.ipynb.expected_sequential_id ├── test_metadata.ipynb ├── test_metadata.ipynb.expected ├── test_metadata_exception.ipynb ├── test_metadata_extra_keys.ipynb.expected ├── test_metadata_keep_count.ipynb.expected ├── test_metadata_keep_output.ipynb.expected ├── test_metadata_keep_output_keep_count.ipynb.expected ├── test_metadata_notebook.ipynb ├── test_metadata_notebook.ipynb.expected ├── test_metadata_period.ipynb ├── test_metadata_period.ipynb.expected ├── test_missing_nbformat.ipynb ├── test_missing_nbformat.ipynb.expected ├── test_nbformat2.ipynb ├── test_nbformat2.ipynb.expected ├── test_nbformat45.ipynb ├── test_nbformat45.ipynb.expected ├── test_nbformat45.ipynb.expected_sequential_id ├── test_nochange.ipynb ├── test_strip_init_cells.ipynb ├── test_strip_init_cells.ipynb.expected ├── test_unicode.ipynb ├── test_unicode.ipynb.expected ├── test_widgets.ipynb ├── test_widgets.ipynb.expected ├── test_zeppelin.zpln └── test_zeppelin.zpln.expected ├── requirements.txt ├── test_diff.ipynb ├── test_diff_different.ipynb ├── test_diff_different_extrakeys.ipynb ├── test_diff_from_bash_subshells.py ├── test_diff_output.ipynb ├── test_end_to_end.py ├── test_git_integration.py ├── test_keep_output_tags.ipynb ├── test_keep_output_tags.py ├── test_keep_output_tags_exception.ipynb └── test_utils.py /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: nbstripout 2 | 3 | on: [push, pull_request, workflow_dispatch] 4 | 5 | jobs: 6 | test: 7 | runs-on: ${{ matrix.os }} 8 | strategy: 9 | matrix: 10 | os: [ubuntu-latest, macos-latest, windows-latest] 11 | python-version: ['3.8', '3.9', '3.10', '3.11', '3.12'] 12 | 13 | steps: 14 | - uses: actions/checkout@v4 15 | - name: Set up Python ${{ matrix.python-version }} 16 | uses: actions/setup-python@v5 17 | with: 18 | python-version: ${{ matrix.python-version }} 19 | - name: Install dependencies 20 | run: | 21 | python -m pip install wheel 22 | python -m pip install -r tests/requirements.txt 23 | python -m pip install . 24 | - name: Configure Git 25 | run: git config --global init.defaultBranch main 26 | - name: Run tests (Linux) 27 | if: matrix.os == 'ubuntu-latest' 28 | shell: bash 29 | run: | 30 | pytest 31 | - name: Run tests (MacOS) 32 | if: matrix.os == 'macos-latest' 33 | shell: bash 34 | run: | 35 | pytest 36 | - name: Run tests (Windows) 37 | if: matrix.os == 'windows-latest' 38 | shell: bash 39 | env: 40 | NBSTRIPOUT_EXE: ${{ env.pythonLocation }}\Scripts\nbstripout.exe 41 | run: | 42 | git config --global core.autocrlf true 43 | pytest 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 98 | __pypackages__/ 99 | 100 | # Celery stuff 101 | celerybeat-schedule 102 | celerybeat.pid 103 | 104 | # SageMath parsed files 105 | *.sage.py 106 | 107 | # Environments 108 | .env 109 | .venv 110 | env/ 111 | venv/ 112 | ENV/ 113 | env.bak/ 114 | venv.bak/ 115 | 116 | # Spyder project settings 117 | .spyderproject 118 | .spyproject 119 | 120 | # Rope project settings 121 | .ropeproject 122 | 123 | # mkdocs documentation 124 | /site 125 | 126 | # mypy 127 | .mypy_cache/ 128 | .dmypy.json 129 | dmypy.json 130 | 131 | # Pyre type checker 132 | .pyre/ 133 | 134 | # pytype static type analyzer 135 | .pytype/ 136 | 137 | # Cython debug symbols 138 | cython_debug/ 139 | 140 | # tmp files 141 | *~ 142 | *.sw[op] 143 | 144 | # PyCharm 145 | .idea 146 | 147 | # VSCode 148 | .vscode -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/astral-sh/ruff-pre-commit 3 | # Ruff version. 4 | rev: v0.9.2 5 | hooks: 6 | # Run the linter. 7 | - id: ruff 8 | types_or: [ python, pyi ] 9 | # Run the formatter. 10 | - id: ruff-format 11 | types_or: [ python, pyi ] 12 | args: [--check] -------------------------------------------------------------------------------- /.pre-commit-hooks.yaml: -------------------------------------------------------------------------------- 1 | - id: nbstripout 2 | name: nbstripout 3 | description: 'nbstripout: strip output from Jupyter and IPython notebooks' 4 | entry: nbstripout 5 | language: python 6 | types: [jupyter] 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 0.8.1 - 2024-11-17 4 | 5 | - In dry run mode, only print output if there would have been a change (#152). 6 | - Declare git filter to be `required` when installing, such that it *must* 7 | succeed (#191). 8 | 9 | ## 0.8.0 - 2024-11-03 10 | 11 | - Adds `--verify` flag, similar to `--dry-run` but returning 1 if any 12 | affected files would have changed (@jspaezp, #153, #195). 13 | - Adds script to apply `nbstripout` retroactively using `git-filter-repo` to 14 | README (@LunarLanding @tokleine, #194 #197). 15 | - Documents nbstripout on all files in the current directory and 16 | subdirectories recursively (#127). 17 | - Accepts notebooks without `nbformat` version specified. 18 | - Improves test coverage. 19 | 20 | ## 0.7.1 - 2024-02-04 21 | 22 | - Fix regression where input file was truncated before reading (#190). 23 | 24 | ## 0.7.0 - 2024-02-04 25 | 26 | - **This release has been yanked from PyPI due to a major regression (#190).** 27 | - Drop support for Python 3.7 (end of life 2023-06-27), require Python 3.8. 28 | - Add support for Python 3.12. 29 | - Drop backwards compability with IPython.nbformat for IPython <4. 30 | - Rename cell ids to be sequential by default. Disable by passing `--keep-id` 31 | (@JasonJooste, #184). 32 | - Improve documentation for notebook and cell metadata stripping (#187). 33 | - Switch from pytest-flake8 to pytest-ruff. 34 | - Convert all text assets from rST to Markdown format. 35 | 36 | ## 0.6.2 - 2024-02-03 37 | 38 | - Add `--python` option for `nbstripout --install` to allow overriding the 39 | Python interpreter specified in `.git/config` (@nobodyinperson, #181 #182). 40 | - Add option `--keep-metadata-keys` to keep specific metadata keys that are 41 | stripped by default (@davidxia, #78 #177). 42 | - Replace [Cram](https://bitheap.org/cram/) as test runner for integration 43 | tests with a custom framework which also supports testing on Windows 44 | (@arobrien, #176 #178). 45 | - Use `SystemExit` instead of `sys.exit` and do not exit from functions 46 | (@janosh, #173). 47 | 48 | ## 0.6.1 - 2022-09-24 49 | 50 | - Removed `setup_requires` and `tests_require` and no longer rely on 51 | `pytest-runner` for test execution but invoke `pytest` directly (#168). 52 | 53 | ## 0.6.0 - 2022-07-24 54 | 55 | - Support for stripping init cells (@Pugio, #157). 56 | - Added `--drop-tagged-cells="some tags"` option (@boun, #161). 57 | - Renamed `--strip-empty-cells` to `--drop-empty-cells`. 58 | - Dropped support for Python 3.5, added support for Python 3.10. 59 | 60 | ## 0.5.0 - 2021-06-28 61 | 62 | - Support only stripping outputs larger than a given size (@cjblocker, #135). 63 | - Support stripping output from Zeppelin Notebooks (@ankitrokdeonsns, #130). 64 | - Switch CI to GitHub actions (#151). 65 | - Support attributes file without leading path component (#155). 66 | 67 | ## 0.4.0 - 2021-04-25 68 | 69 | - Add support for system wide installation, `--system` flag 70 | (@PLPeeters, #149). 71 | - Use `~` instead of `$HOME` for config dir (#136). 72 | - Document stripping kernelspec (#141). 73 | - Add support for removing empty cells (#131). 74 | - Create directory for attributes file if needed (#139). 75 | - Add support for stripping metadata keys containing periods 76 | (@baldwint, #143). 77 | - Strip collapsible headings by default (@rpytel1, #142). 78 | 79 | ## 0.3.10 - 2021-04-24 80 | 81 | - Python 2.7 only release, to make `pip install nbstripout` work in Python 82 | 2.7. Previously, this was picking up 0.3.8 which is *not* Python 2.7 83 | compatible. 84 | - Drop Python 3.4 support, add support for Python 3.7, 3.8. 85 | - Windows compatibility: `""` quote Python interpreter path 86 | (@fcollonval, #115). 87 | - Add `--dry-run` flag (#122). 88 | - Support specifying `keep_output` as a cell tag (@scottcode, #117). 89 | 90 | ## 0.3.9 - 2020-06-28 91 | 92 | - Document Python 3 support only. Fail to install on Python 2 93 | (@casperdcl, #128) 94 | - Drop support for Python 3.4 (end of life 2019-03-18). 95 | - Ignore warnings from `nbformat.{read,write}`. 96 | - Support nbformat 2 notebooks without cell metadata. 97 | - Add `--extra-keys` flag to pass extra keys to strip (#119). 98 | - Apply pre-commit hook to files of type Jupyter. 99 | 100 | ## 0.3.8 - 2020-06-06 101 | 102 | - Drop Python 2 support. 103 | - Windows compatibility: `""` quote Python interpreter path 104 | (@fcollonval, #115). 105 | - Add `--dry-run` flag (#122). 106 | - Support specifying `keep_output` as a cell tag (@scottcode, #117). 107 | - Improved error handling for the case where git is not installed (#124). 108 | - Nicer error message when input file is not found. 109 | - Use universal newlines without conversion (@ooiM, #110, #126). 110 | - Strip execution timing from cell metadata (#118). 111 | - Document which metadata is stripped by default. 112 | - Make `--global` commands work outside of git repository (#123). 113 | 114 | ## 0.3.7 - 2020-01-05 115 | 116 | - Notebook-level `keep_output` (@jonashaag, #112). 117 | - Fix quoting of Python path and call module entrypoint (@jonashaag, #111). 118 | - Do not run `git add` in pre-commit hook (@SimonBiggs, #106). 119 | - Troubleshooting instructions (#65). 120 | - Exclusion instructions for folders (@jraviotta, #104). 121 | - Only remove `filter.nbstripout.{clean,smudge}` on `--uninstall`. 122 | - Remove unnecessary `filter.nbstripout.required` config setting. 123 | - pre-commit configuration (@Ohjeah, #79). 124 | 125 | ## 0.3.6 - 2019-07-18 126 | 127 | - Document global installation in README (#100). 128 | - Document how to exclude folders in README (#99). 129 | - Expand `~` when looking up attributes file. 130 | - Add `--global` flag for `--install` / `--uninstall` to write the filter 131 | config to `~/.gitconfig` (#98). 132 | 133 | ## 0.3.5 - 2019-04-02 134 | 135 | - Make nbstripout package executable and fix regression (#94). 136 | - Add package docstring. 137 | 138 | ## 0.3.4 - 2019-03-26 139 | 140 | - Fix `WindowsError` not defined on POSIX systems (#90). 141 | - Add support for blacklisting custom metadata fields (@casperdcl, #92). 142 | 143 | ## 0.3.3 - 2018-08-04 144 | 145 | - Distribute tests in source package (@jluttine, #73 #76). 146 | - Fix git diff tests for newer Git versions (@jluttine, #74 #76). 147 | - Install full path for diff.ipynb.textconv (@ibressler, #68 #82). 148 | - Make sure sys.stdin is not None before reading from it 149 | (@ibressler, #68 #82). 150 | 151 | ## 0.3.2 - 2018-07-09 152 | 153 | - Gracefully deal with empty/malformed input (#66). 154 | - Add Code of Conduct (#63). 155 | - Add MANIFEST.in (#64). 156 | - Document `git filter-branch` use case in README (@belteshassar, #28). 157 | - Flush output when using `-t` (@tnilanon, #67). 158 | - Add `nbformat` and `setuptools >= 30` to `setup_requires` (@tnilanon, #67). 159 | - Use `travis_retry` (@tnilanon, #67). 160 | - Drop support for Python 3.3 (no longer supported by setuptools). 161 | 162 | ## 0.3.1 - 2017-07-30 163 | 164 | - Add option `-t`/`--textconv` to write to stdout e.g. for use as diff filter 165 | (@utsekaj42, #53). 166 | - Flush output stream after write (@reidpr, #55). 167 | - Add options `--keep-count` and `--keep-output` to no strip execution counts 168 | and output (@jpeacock29, #56). 169 | - Fix shell pipeline documentation (@psthomas, #59). 170 | - Catch `WindowsError` when `git` is not found in PATH (@bdforbes, #62). 171 | 172 | ## 0.3.0 - 2017-02-23 173 | 174 | - Support whitespace in repository paths (@ehoepfner, #47 #48). 175 | - Also ignore `collapsed` and `scrolled` metadata (#34). 176 | - Define `NO_CONVERT` for IPython <3 import (#46). 177 | 178 | ## 0.2.9 - 2016-11-23 179 | 180 | - Strip `ExecuteTime` metadata (@jdriordan, #34 #39). 181 | - Fix Python 3.5 bug: open attributes file only once (#40). 182 | - Do not add blank line at beginning of attribute file. 183 | - Strip widget state from notebook metadata (#42). 184 | 185 | ## 0.2.8 - 2016-09-19 186 | 187 | - Drop support for Python 2.6, 3.2. 188 | - Add pip install instructions to README (@oogali, #32). 189 | - Write trailing newline to attributes file (#36). 190 | - Uninstall only removes ipynb filter (#37). 191 | 192 | ## 0.2.7 - 2016-07-30 193 | 194 | - If you set either the `"init_cell": true` or `"keep_output": true` in the 195 | cell metadata, then these cells will not be stripped out. The former works 196 | in conjunction with the `init_cell` nbextension (@mforbes, #17). 197 | - Fix encoding for Python 2 + 3 (#11). 198 | - Add `--is-installed` and `--status` options (#29). 199 | - Normalise cell output style, setting `scroll` and `collapsed` to False 200 | (@kdmurray91, #30). 201 | - Add screencast (#31). 202 | 203 | ## 0.2.6 - 2016-03-13 204 | 205 | - Use pytest-cram (@mforbes, #22). 206 | - Add further shields to README. 207 | - Use argparse for argument parsing. 208 | - Add `--attributes` option to specify attributes file (#25). 209 | 210 | ## 0.2.5 - 2016-03-03 211 | 212 | - Python 3 compatibility (@boeddeker, #16 #21). 213 | - Windows compatibility (@tt293, #18). 214 | - Add support for appveyor (#24). 215 | 216 | ## 0.2.4 - 2016-02-15 217 | 218 | - Add `__version__` and `version` command (#12). 219 | - Add bumpversion config. 220 | - Add contributing guidelines (#13). 221 | 222 | ## 0.2.3 - 2016-02-15 223 | 224 | - Use UTF8 writer for stdout and regression test (@geggo, #11). 225 | - Minor testing fixes. 226 | 227 | ## 0.2.2 - 2016-02-04 228 | 229 | - Add uninstall task (#8). 230 | - Minor testing fixes. 231 | 232 | ## 0.2.1 - 2016-01-27 233 | 234 | - Add Travis CI setup (#4). 235 | - Call decode on `git_dir` (@michaelaye, #5). 236 | - Add unit tests via Cram (@mforbes). 237 | 238 | ## 0.2.0 - 2016-01-24 239 | 240 | - Only process .ipynb files unless -f flag is used (@mforbes). 241 | - Process multiple files (@mforbes). 242 | - Add MIT License (@mforbes). 243 | 244 | ## 0.1.0 - not released 245 | 246 | - Based on Min RK's original [gist](https://gist.github.com/minrk/6176788) 247 | but supports multiple versions of IPython/Jupyter and also strips the 248 | execution count. 249 | - Add install option that fails sensibly if not in a git repository, does not 250 | clobber an existing attributes file and checks for an existing ipynb filter. 251 | - Works with both files and stdin / stdout. 252 | - Add README and documentation. 253 | - Add setup.py with script entry point. 254 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at florian.rathgeber+nbstripout@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to contribute 2 | 3 | Development of nbstripout happens on 4 | [GitHub](https://github.com/kynan/nbstripout) - 5 | [bug reports](https://github.com/kynan/nbstripout/issues) and 6 | [pull requests](https://github.com/kynan/nbstripout/pulls) welcome! 7 | 8 | # Releasing a new version 9 | 10 | To simplify updating the version number consistently across different files and 11 | creating the appropriate annotated tag, we use 12 | [bump-my-version](https://github.com/callowayproject/bump-my-version). For a new 13 | patch release, run 14 | 15 | bump-my-version bump patch 16 | 17 | and for a minor release, run 18 | 19 | bump-my-version bump minor 20 | 21 | Remember to also push the release tag with `git push --tags`. 22 | 23 | Use [twine](https://twine.readthedocs.io/en/latest/#using-twine) to upload the 24 | new release to PyPI: 25 | 26 | python -m build 27 | twine check dist/nbstripout-0.8.1* 28 | twine upload -r testpypi dist/nbstripout-0.8.1* 29 | twine upload dist/nbstripout-0.8.1* 30 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Min RK, Florian Rathgeber, Michael McNeil Forbes 2 | 2019 Casper da Costa-Luis 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining 5 | a copy of this software and associated documentation files (the 6 | "Software"), to deal in the Software without restriction, including 7 | without limitation the rights to use, copy, modify, merge, publish, 8 | distribute, sublicense, and/or sell copies of the Software, and to 9 | permit persons to whom the Software is furnished to do so, subject to 10 | the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 19 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 20 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 21 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | # Include the license file 2 | include LICENSE.txt 3 | 4 | # Include tests 5 | include tests/* 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [![tests](https://github.com/kynan/nbstripout/actions/workflows/tests.yml/badge.svg)](https://github.com/kynan/nbstripout/actions/workflows/tests.yml) 4 | [![downloads](https://img.shields.io/pypi/dm/nbstripout)](https://pypi.org/project/nbstripout) 5 | [![PyPI version](https://img.shields.io/pypi/v/nbstripout)](https://pypi.org/project/nbstripout) 6 | [![conda-forge version](https://img.shields.io/conda/vn/conda-forge/nbstripout)](https://anaconda.org/conda-forge/nbstripout) 7 | [![supported Python versions](https://img.shields.io/pypi/pyversions/nbstripout)](https://pypi.org/project/nbstripout) 8 | [![Python package formats](https://img.shields.io/pypi/format/nbstripout)](https://pypi.org/project/nbstripout) 9 | [![license](https://img.shields.io/pypi/l/nbstripout)](https://raw.githubusercontent.com/kynan/nbstripout/main/LICENSE.txt) 10 | [![GitHub stars](https://img.shields.io/github/stars/kynan/nbstripout?style=social)](https://github.com/kynan/nbstripout/stargazers) 11 | [![GitHub forks](https://img.shields.io/github/forks/kynan/nbstripout?style=social)](https://github.com/kynan/nbstripout/network/members) 12 | 13 | # nbstripout: strip output from Jupyter and IPython notebooks 14 | 15 | Reads a notebook from a file or stdin, strips output and some metadata, and 16 | writes the "cleaned" version of the notebook to the original file or stdout. 17 | 18 | Intended to be used as a Git filter or pre-commit hook for users who don't want 19 | to track output in Git. 20 | 21 | Roughly equivalent to the "Clear All Output" command in the notebook UI, but 22 | only "visible" to Git: keep your output in the file on disk, but don't commit 23 | the output to Git. This helps minimizing diffs and reduce file size. 24 | 25 | Originally based on . 26 | 27 | ## Python 3 only 28 | 29 | As of version 0.4.0, nbstripout supports Python 3 *only*. If you need to use 30 | Python 2.7, install nbstripout 0.3.10: 31 | 32 | pip install nbstripout==0.3.10 33 | 34 | ## Screencast 35 | 36 | This screencast demonstrates the use and working principles behind the 37 | nbstripout utility and how to use it as a Git filter: 38 | 39 | [![image](https://i.imgur.com/7oQHuJ5.png)](https://www.youtube.com/watch?v=BEMP4xacrVc) 40 | 41 | ## Installation 42 | 43 | You can download and install the latest version of `nbstripout` from the Python 44 | package index [PyPI](https://pypi.org/project/nbstripout/) as follows: 45 | 46 | pip install --upgrade nbstripout 47 | 48 | When using the [Anaconda](https://www.anaconda.com/download) Python 49 | distribution, install `nbstripout` via the [conda](https://docs.conda.io) 50 | package manager from [conda-forge](https://conda-forge.org): 51 | 52 | conda install -c conda-forge nbstripout 53 | 54 | ## Usage 55 | 56 | Strip output from IPython / Jupyter / Zeppelin notebook (modifies the file 57 | in-place): 58 | 59 | nbstripout FILE.ipynb [FILE2.ipynb ...] 60 | nbstripout FILE.zpln 61 | 62 | Force processing of non `.ipynb` files: 63 | 64 | nbstripout -f FILE.ipynb.bak 65 | 66 | For using Zeppelin mode while processing files with other extensions use: 67 | 68 | nbstripout -m zeppelin -f 69 | 70 | Write to stdout e.g. to use as part of a shell pipeline: 71 | 72 | cat FILE.ipynb | nbstripout > OUT.ipynb 73 | cat FILE.zpln | nbstripout -m zeppelin > OUT.zpln 74 | 75 | or 76 | 77 | nbstripout -t FILE.ipynb | other-command 78 | 79 | Do a dry run and only list which files would have been stripped: 80 | 81 | nbstripout --dry-run FILE.ipynb [FILE2.ipynb ...] 82 | 83 | or 84 | 85 | Do a verification run, which works like dry run but will fail 86 | if any files would have been stripped: 87 | 88 | nbstripout --verify FILE.ipynb [FILE2.ipynb ...] 89 | 90 | Operate on all `.ipynb` files in the current directory and subdirectories 91 | recursively: 92 | 93 | find . -name '*.ipynb' -exec nbstripout {} + 94 | 95 | Print the version: 96 | 97 | nbstripout --version 98 | 99 | Show help and usage instructions: 100 | 101 | nbstripout --help 102 | 103 | ### Using as a Git filter 104 | 105 | Set up the [git filter](https://git-scm.com/docs/gitattributes#_filter) and 106 | attributes as described in the manual installation instructions below: 107 | 108 | nbstripout --install 109 | 110 | Note: The filter is declared as `required`, meaning the filter *must* succeed. 111 | Failures and misconfigurations will not simply cause the filter to be ignored. 112 | 113 | Set up the git filter using `.gitattributes`: 114 | 115 | nbstripout --install --attributes .gitattributes 116 | 117 | Specify a different path to the Python interpreter to be used for the git 118 | filters (default is the path to the Python interpreter used when `nbstripout` is 119 | installed). This is useful if you have Python installed in different or unusual 120 | locations across machines, e.g. `/usr/bin/python3` on your machine vs 121 | `/usr/local/bin/python3` in a container or elsewhere. 122 | 123 | nbstripout --install --python python3 124 | 125 | Using just `python3` lets each machine find its Python itself. However, keep in 126 | mind that depending on your setup this might not be the Python version you want 127 | or even fail because an absolute path is required. 128 | 129 | Set up the git filter in your global `~/.gitconfig`: 130 | 131 | nbstripout --install --global 132 | 133 | Set up the git filter in your system-wide `$(prefix)/etc/gitconfig` (most 134 | installations will require you to `sudo`): 135 | 136 | [sudo] nbstripout --install --system 137 | 138 | Remove the git filter and attributes: 139 | 140 | nbstripout --uninstall 141 | 142 | Remove the git filter from your global `~/.gitconfig` and attributes: 143 | 144 | nbstripout --uninstall --global 145 | 146 | Remove the git filter from your system-wide `$(prefix)/etc/gitconfig` and 147 | attributes: 148 | 149 | [sudo] nbstripout --uninstall --system 150 | 151 | Remove the git filter and attributes from `.gitattributes`: 152 | 153 | nbstripout --uninstall --attributes .gitattributes 154 | 155 | Check if `nbstripout` is installed in the current repository (exits with code 0 156 | if installed, 1 otherwise): 157 | 158 | nbstripout --is-installed 159 | 160 | Print status of `nbstripout` installation in the current repository and 161 | configuration summary of filter and attributes if installed (exits with code 0 162 | if installed, 1 otherwise): 163 | 164 | nbstripout --status 165 | 166 | ### Configuration files 167 | 168 | The following table shows in which files the `nbstripout` filter and attribute 169 | configuration is written to for given extra flags to `--install` and 170 | `--uninstall`: 171 | 172 | | flags | filters | attributes | 173 | | ---------------------------------------- | --------------------------- | ------------------------------- | 174 | | none | `.git/config` | `.git/info/attributes` | 175 | | `--global` | `~/.gitconfig` | `~/.config/git/attributes` | 176 | | `--system` | `$(prefix)/etc/gitconfig` | `$(prefix)/etc/gitattributes` | 177 | | `--attributes=.gitattributes` | `.git/config` | `.gitattributes` | 178 | | `--global --attributes=.gitattributes` | `~/.gitconfig` | `.gitattributes` | 179 | 180 | ### Install globally 181 | 182 | Usually, `nbstripout` is installed per repository so you can choose where to use 183 | it or not. You can choose to set the attributes in `.gitattributes` and commit 184 | this file to your repository, however there is no way to have git set up the 185 | filters automatically when someone clones a repository. This is by design, to 186 | prevent you from executing arbitrary and potentially malicious code when cloning 187 | a repository. 188 | 189 | To install `nbstripout` for all your repositories such that you no longer need 190 | to run the installation once per repository, install as follows: 191 | 192 | mkdir -p ~/.config/git # This folder may not exist 193 | nbstripout --install --global --attributes=~/.config/git/attributes 194 | 195 | This will set up the filters and diff driver in your `~/.gitconfig` and instruct 196 | git to apply them to any `.ipynb` file in any repository. 197 | 198 | Note that you need to uninstall with the same flags: 199 | 200 | nbstripout --uninstall --global --attributes=~/.config/git/attributes 201 | 202 | ### Install system-wide 203 | 204 | To install `nbstripout` system-wide so that it applies to all repositories for 205 | all users, install as follows (most installations will require you to `sudo`): 206 | 207 | [sudo] nbstripout --install --system 208 | 209 | This will set up the filters and diff driver in `$(prefix)/etc/gitconfig` and 210 | instruct git to apply them to any `.ipynb` file in any repository for any user. 211 | 212 | Note that you need to uninstall with the same flags: 213 | 214 | [sudo] nbstripout --uninstall --system 215 | 216 | ### Apply retroactively 217 | 218 | `nbstripout` can be used to rewrite an existing Git repository using 219 | [`git filter-repo`](https://github.com/newren/git-filter-repo) to strip output 220 | from existing notebooks. This invocation operates on all ipynb files in the repo: 221 | 222 | #!/usr/bin/env bash 223 | # get lint-history with callback from https://github.com/newren/git-filter-repo/pull/542 224 | ./lint-history.py --relevant 'return filename.endswith(b".ipynb")' --callback ' 225 | import json, warnings, nbformat 226 | from nbstripout import strip_output 227 | from nbformat.reader import NotJSONError 228 | try: 229 | with warnings.catch_warnings(): 230 | warnings.simplefilter("ignore", category=UserWarning) 231 | notebook = nbformat.reads(blob.data, as_version=nbformat.NO_CONVERT) 232 | # customize to your needs 233 | strip_output(notebook, keep_output=False, keep_count=False, keep_id=False, extra_keys=["metadata.widgets","metadata.execution","cell.attachments"], drop_empty_cells=True, drop_tagged_cells=[],strip_init_cells=False, max_size=0) 234 | old_len = len(blob.data) 235 | blob.data = (nbformat.writes(notebook) + "\n").encode("utf-8") 236 | if old_len != len(blob.data): 237 | print(change.blob_id, change.filename, old_len, len(blob.data)) 238 | except NotJSONError as e: 239 | print("ERROR", type(e), change.blob_id, change.filename) 240 | ' 241 | 242 | ### Removing empty cells 243 | 244 | Drop empty cells i.e. cells where `source` is either empty or only contains 245 | whitespace: 246 | 247 | nbstripout --drop-empty-cells 248 | 249 | ### Removing [init]{.title-ref} cells 250 | 251 | By default `nbstripout` will keep cells with `init_cell: true` metadata. To 252 | disable this behavior use: 253 | 254 | nbstripout --strip-init-cells 255 | 256 | ### Removing entire cells 257 | 258 | In certain conditions it might be handy to remove not only the output, but the 259 | entire cell, e.g. when developing exercises. 260 | 261 | To drop all cells tagged with "solution" run: 262 | 263 | nbstripout --drop-tagged-cells="solution" 264 | 265 | The option accepts a list of tags separated by whitespace. 266 | 267 | ### Keeping some output 268 | 269 | Do not strip the execution count/prompt number: 270 | 271 | nbstripout --keep-count 272 | 273 | Do not strip outputs that are smaller that a given max size (useful for removing 274 | only large outputs like images): 275 | 276 | nbstripout --max-size 1k 277 | 278 | Do not strip the output, only metadata: 279 | 280 | nbstripout --keep-output 281 | 282 | Do not reassign the cell ids to be sequential (which is the default behavior): 283 | 284 | nbstripout --keep-id 285 | 286 | To mark special cells so that the output is not stripped, you can either: 287 | 288 | 1. Set the `keep_output` tag on the cell. To do this, enable the tags toolbar 289 | (View > Cell Toolbar > Tags) and then add the `keep_output` tag for each 290 | cell you would like to keep the output for. 291 | 292 | 2. Set the `"keep_output": true` metadata on the cell. To do this, select the 293 | "Edit Metadata" Cell Toolbar, and then use the "Edit Metadata" button on 294 | the desired cell to enter something like: 295 | 296 | { 297 | "keep_output": true, 298 | } 299 | 300 | You can also keep output for an entire notebook. This is useful if you want to 301 | strip output by default in an automated environment (e.g. CI pipeline), but want 302 | to be able to keep outputs for some notebooks. To do so, add the option above to 303 | the *notebook* metadata instead. (You can also explicitly remove outputs from a 304 | particular cell in these notebooks by adding a cell-level metadata entry.) 305 | 306 | Another use-case is to preserve initialization cells that might load customized 307 | CSS etc. critical for the display of the notebook. To support this, we also keep 308 | output for cells with: 309 | 310 | { 311 | "init_cell": true, 312 | } 313 | 314 | This is the same metadata used by the 315 | [init_cell nbextension](https://github.com/ipython-contrib/jupyter_contrib_nbextensions/tree/master/src/jupyter_contrib_nbextensions/nbextensions/init_cell). 316 | 317 | ### Stripping metadata 318 | 319 | The following metadata is stripped by default: 320 | 321 | - Notebook metadata: `signature`, `widgets` 322 | - Cell metadata: `ExecuteTime`, `collapsed`, `execution`, `heading_collapsed`, 323 | `hidden`, `scrolled` 324 | 325 | Additional metadata to be stripped can be configured via either 326 | 327 | - `git config (--global/--system) filter.nbstripout.extrakeys`, e.g. : 328 | 329 | git config --global filter.nbstripout.extrakeys ' 330 | metadata.celltoolbar 331 | metadata.kernelspec 332 | metadata.language_info.codemirror_mode.version 333 | metadata.language_info.pygments_lexer 334 | metadata.language_info.version 335 | metadata.toc 336 | metadata.notify_time 337 | metadata.varInspector 338 | cell.metadata.heading_collapsed 339 | cell.metadata.hidden 340 | cell.metadata.code_folding 341 | cell.metadata.tags 342 | cell.metadata.init_cell' 343 | 344 | - the `--extra-keys` flag, which takes a space-delimited string as an 345 | argument, e.g. : 346 | 347 | --extra-keys="metadata.celltoolbar cell.metadata.heading_collapsed" 348 | 349 | Note: Only notebook and cell metadata is currently supported and every key 350 | specified via `filter.nbstripout.extrakeys` or `--extra-keys` must start with 351 | `metadata.` for notebook and `cell.metadata.` for cell metadata. 352 | 353 | You can keep certain metadata that would be stripped by default with either 354 | 355 | - `git config (--global/--system) filter.nbstripout.keepmetadatakeys`, e.g.: 356 | 357 | git config --global filter.nbstripout.keepmetadatakeys ' 358 | cell.metadata.collapsed 359 | cell.metadata.scrolled' 360 | 361 | - the `--keep-metadata-keys` flag, which takes a space-delimited string as an 362 | argument, e.g.: 363 | 364 | --keep-metadata-keys="cell.metadata.collapsed cell.metadata.scrolled" 365 | 366 | Note: Previous versions of Jupyter used `metadata.kernel_spec` for kernel 367 | metadata. Prefer stripping `kernelspec` entirely: only stripping some attributes 368 | inside `kernelspec` may lead to errors when opening the notebook in Jupyter (see 369 | [#141](https://github.com/kynan/nbstripout/issues/141)). 370 | 371 | ### Excluding files and folders 372 | 373 | To exclude specific files or folders from being processed by the `nbstripout` 374 | filters, add the path and exception to your filter specifications defined in 375 | `.git/info/attributes` or `.gitattributes`: 376 | 377 | docs/** filter= diff= 378 | 379 | This will disable `nbstripout` for any file in the `docs` directory.: 380 | 381 | notebooks/Analysis.ipynb filter= diff= 382 | 383 | This will disable `nbstripout` for the file `Analysis.ipynb` located in the 384 | `notebooks` directory. 385 | 386 | To check which attributes a given file has with the current config, run: 387 | 388 | git check-attr -a -- path/to/file 389 | 390 | For a file to which the filter applies you will see the following: 391 | 392 | $ git check-attr -a -- foo.ipynb 393 | foo.ipynb: diff: ipynb 394 | foo.ipynb: filter: nbstripout 395 | 396 | For a file in your excluded folder you will see the following: 397 | 398 | $ git check-attr -a -- docs/foo.ipynb 399 | foo.ipynb: diff: 400 | foo.ipynb: filter: 401 | 402 | ## Manual filter installation 403 | 404 | Set up a git filter and diff driver using nbstripout as follows: 405 | 406 | git config filter.nbstripout.clean '/path/to/nbstripout' 407 | git config filter.nbstripout.smudge cat 408 | git config filter.nbstripout.required true 409 | git config diff.ipynb.textconv '/path/to/nbstripout -t' 410 | 411 | This will add a section to the `.git/config` file of the current repository. 412 | 413 | If you want the filter to be installed globally for your user, add the 414 | `--global` flag to the `git config` invocations above to have the configuration 415 | written to your `~/.gitconfig` and apply to all repositories. 416 | 417 | If you want the filter to be installed system-wide, add the `--system` flag to 418 | the `git config` invocations above to have the configuration written to 419 | `$(prefix)/etc/gitconfig` and apply to all repositories for all users. 420 | 421 | Create a file `.gitattributes` (if you want it versioned with the repository) or 422 | `.git/info/attributes` (to apply it only to the current repository) with the 423 | following content: 424 | 425 | *.ipynb filter=nbstripout 426 | *.ipynb diff=ipynb 427 | 428 | This instructs git to use the filter named `nbstripout` and the diff driver 429 | named `ipynb` set up in the git config above for every `.ipynb` file in the 430 | repository. 431 | 432 | If you want the attributes be set for `.ipynb` files in any of your git 433 | repositories, add those two lines to `~/.config/git/attributes`. Note that this 434 | file and the `~/.config/git` directory may not exist. 435 | 436 | If you want the attributes be set for `.ipynb` files in any git repository on 437 | your system, add those two lines to `$(prefix)/etc/gitattributes`. Note that 438 | this file may not exist. 439 | 440 | ## Using `nbstripout` as a pre-commit hook 441 | 442 | [pre-commit](https://pre-commit.com) is a framework for managing git 443 | [pre-commit hooks](https://git-scm.com/docs/githooks). 444 | 445 | Once you have [pre-commit](https://pre-commit.com) installed, add the following 446 | to the `.pre-commit-config.yaml` in your repository: 447 | 448 | repos: 449 | - repo: https://github.com/kynan/nbstripout 450 | rev: 0.8.1 451 | hooks: 452 | - id: nbstripout 453 | 454 | Then run `pre-commit install` to activate the hook. 455 | 456 | When passing parameters to the hook, be aware that arguments with spaces such as 457 | `--extra-keys` need to be quoted as a whole: 458 | 459 | repos: 460 | - repo: https://github.com/kynan/nbstripout 461 | rev: 0.8.1 462 | hooks: 463 | - id: nbstripout 464 | args: ['--extra-keys=metadata.celltoolbar cell.metadata.heading_collapsed'] 465 | 466 | > [!WARNING] 467 | > 468 | > In this mode, `nbstripout` is used as a git hook to strip any `.ipynb` files 469 | > before committing. This also modifies your working copy! 470 | > 471 | > In its regular mode, `nbstripout` acts as a filter and only modifies what git 472 | > gets to see for committing or diffing. The working copy stays intact. 473 | 474 | ## Troubleshooting 475 | 476 | ### Known issues 477 | 478 | Certain Git workflows are not well supported by `nbstripout`: 479 | 480 | - Local changes to notebook files that are made invisible to Git due to the 481 | `nbstripout` filter do still cause conflicts when attempting to sync 482 | upstream changes (`git pull`, `git merge` etc.). This is because Git has no 483 | way of resolving a conflict caused by a non-stripped local file being merged 484 | with a stripped upstream file. Addressing this issue is out of scope for 485 | `nbstripout`. Read more and find workarounds in 486 | [#108](https://github.com/kynan/nbstripout/issues/108). 487 | 488 | ### Show files processed by nbstripout filter 489 | 490 | Git has [no builtin support](https://stackoverflow.com/a/52065333/396967) for 491 | listing files a clean or smudge filter operates on. As a workaround, change the 492 | setup of your filter in `.git/config`, `~/.gitconfig` or 493 | `$(prefix)/etc/gitconfig` as follows to see the filenames either filter operates 494 | on: 495 | 496 | [filter "nbstripout"] 497 | clean = "f() { echo >&2 \"clean: nbstripout $1\"; nbstripout; }; f %f" 498 | smudge = "f() { echo >&2 \"smudge: cat $1\"; cat; }; f %f" 499 | required = true 500 | -------------------------------------------------------------------------------- /nbstripout/__init__.py: -------------------------------------------------------------------------------- 1 | from ._nbstripout import install, uninstall, status, main, __doc__ as docstring 2 | from ._utils import pop_recursive, strip_output, MetadataError 3 | 4 | __all__ = ['install', 'uninstall', 'status', 'main', 'pop_recursive', 'strip_output', 'MetadataError'] 5 | __doc__ = docstring 6 | -------------------------------------------------------------------------------- /nbstripout/__main__.py: -------------------------------------------------------------------------------- 1 | from nbstripout import main 2 | 3 | main() 4 | -------------------------------------------------------------------------------- /nbstripout/_nbstripout.py: -------------------------------------------------------------------------------- 1 | """ 2 | Strip output from Jupyter and IPython notebooks 3 | =============================================== 4 | 5 | Opens a notebook, strips its output, and writes the outputless version to the 6 | original file. 7 | 8 | Useful mainly as a git filter or pre-commit hook for users who don't want to 9 | track output in VCS. 10 | 11 | This does mostly the same thing as the `Clear All Output` command in the 12 | notebook UI. 13 | 14 | Usage 15 | ===== 16 | 17 | Strip output from IPython / Jupyter / Zeppelin notebook (modifies the file in-place): :: 18 | 19 | nbstripout 20 | nbstripout 21 | 22 | By default, nbstripout will only modify files ending in '.ipynb' or '.zpln', to 23 | process other files us the '-f' flag to force the application. 24 | 25 | nbstripout -f 26 | 27 | For using Zeppelin mode while processing files with other extensions use: 28 | nbstripout -m zeppelin -f 29 | 30 | Use as part of a shell pipeline: :: 31 | 32 | cat FILE.ipynb | nbstripout > OUT.ipynb 33 | cat FILE.zpln | nbstripout -m zeppelin > OUT.zpln 34 | 35 | Set up the git filter and attributes as described in the manual installation 36 | instructions below: :: 37 | 38 | nbstripout --install 39 | 40 | Set up the git filter using ``.gitattributes`` :: 41 | 42 | nbstripout --install --attributes .gitattributes 43 | 44 | Set up the git filter in your global ``~/.gitconfig`` :: 45 | 46 | nbstripout --install --global 47 | 48 | Set up the git filter in your system-wide ``$(prefix)/etc/gitconfig`` (most installations will require you to ``sudo``) :: 49 | 50 | [sudo] nbstripout --install --system 51 | 52 | Remove the git filter and attributes: :: 53 | 54 | nbstripout --uninstall 55 | 56 | Remove the git filter from your global ``~/.gitconfig`` and attributes :: 57 | 58 | nbstripout --uninstall --global 59 | 60 | Remove the git filter from your system-wide ``$(prefix)/etc/gitconfig`` and attributes :: 61 | 62 | nbstripout --uninstall --system 63 | 64 | Remove the git filter and attributes from ``.gitattributes``: :: 65 | 66 | nbstripout --uninstall --attributes .gitattributes 67 | 68 | Check if ``nbstripout`` is installed in the current repository 69 | (exits with code 0 if installed, 1 otherwise): :: 70 | 71 | nbstripout --is-installed 72 | 73 | Print status of ``nbstripout`` installation in the current repository and 74 | configuration summary of filter and attributes if installed 75 | (exits with code 0 if installed, 1 otherwise): :: 76 | 77 | nbstripout --status 78 | 79 | Do a dry run and only list which files would have been stripped: :: 80 | 81 | nbstripout --dry-run FILE.ipynb [FILE2.ipynb ...] 82 | 83 | Print the version: :: 84 | 85 | nbstripout --version 86 | 87 | Show this help page: :: 88 | 89 | nbstripout --help 90 | 91 | Manual filter installation 92 | ========================== 93 | 94 | Set up a git filter using nbstripout as follows: :: 95 | 96 | git config filter.nbstripout.clean '/path/to/nbstripout' 97 | git config filter.nbstripout.smudge cat 98 | 99 | Create a file ``.gitattributes`` or ``.git/info/attributes`` with: :: 100 | 101 | *.ipynb filter=nbstripout 102 | 103 | Apply the filter for git diff of ``*.ipynb`` files: :: 104 | 105 | git config diff.ipynb.textconv '/path/to/nbstripout -t' 106 | 107 | In file ``.gitattributes`` or ``.git/info/attributes`` add: :: 108 | 109 | *.ipynb diff=ipynb 110 | """ 111 | 112 | from argparse import ArgumentParser, RawDescriptionHelpFormatter, Namespace 113 | import collections 114 | import copy 115 | import io 116 | import json 117 | from os import devnull, environ, makedirs, path 118 | from pathlib import PureWindowsPath 119 | import re 120 | from subprocess import call, check_call, check_output, CalledProcessError, STDOUT 121 | from typing import List, Optional 122 | import sys 123 | import warnings 124 | 125 | import nbformat 126 | 127 | from nbstripout._utils import strip_output, strip_zeppelin_output 128 | 129 | __all__ = ['install', 'uninstall', 'status', 'main'] 130 | __version__ = '0.8.1' 131 | 132 | 133 | INSTALL_LOCATION_LOCAL = 'local' 134 | INSTALL_LOCATION_GLOBAL = 'global' 135 | INSTALL_LOCATION_SYSTEM = 'system' 136 | 137 | 138 | def _get_system_gitconfig_folder() -> str: 139 | try: 140 | git_config_output = check_output( 141 | ['git', 'config', '--system', '--list', '--show-origin'], universal_newlines=True, stderr=STDOUT 142 | ).strip() 143 | 144 | # If the output is empty, it means the file exists but is empty, so we cannot get the path. 145 | # To still get it, we're setting a temporary config parameter. 146 | if git_config_output == '': 147 | check_call(['git', 'config', '--system', 'filter.nbstripoutput.test', 'test']) 148 | git_config_output = check_output( 149 | ['git', 'config', '--system', '--list', '--show-origin'], universal_newlines=True 150 | ).strip() 151 | check_call(['git', 'config', '--system', '--unset', 'filter.nbstripoutput.test']) 152 | 153 | output_lines = git_config_output.split('\n') 154 | 155 | system_gitconfig_file_path = re.sub(r'^file:', '', output_lines[0].split('\t')[0]) 156 | except CalledProcessError as e: 157 | git_config_output = e.output 158 | 159 | system_gitconfig_file_path = re.match(r"fatal:.*file '([^']+)'.*", git_config_output).group(1) 160 | 161 | return path.abspath(path.dirname(system_gitconfig_file_path)) 162 | 163 | 164 | def _get_attrfile( 165 | git_config: str, install_location: str = INSTALL_LOCATION_LOCAL, attrfile: Optional[str] = None 166 | ) -> str: 167 | if not attrfile: 168 | if install_location == INSTALL_LOCATION_SYSTEM: 169 | try: 170 | attrfile = check_output(git_config + ['core.attributesFile'], universal_newlines=True).strip() 171 | except CalledProcessError: 172 | config_dir = _get_system_gitconfig_folder() 173 | attrfile = path.join(config_dir, 'gitattributes') 174 | elif install_location == INSTALL_LOCATION_GLOBAL: 175 | try: 176 | attrfile = check_output(git_config + ['core.attributesFile'], universal_newlines=True).strip() 177 | except CalledProcessError: 178 | config_dir = environ.get('XDG_CONFIG_DIR', path.expanduser('~/.config')) 179 | attrfile = path.join(config_dir, 'git', 'attributes') 180 | else: 181 | git_dir = check_output(['git', 'rev-parse', '--git-dir'], universal_newlines=True).strip() 182 | attrfile = path.join(git_dir, 'info', 'attributes') 183 | 184 | attrfile = path.expanduser(attrfile) 185 | if path.dirname(attrfile): 186 | makedirs(path.dirname(attrfile), exist_ok=True) 187 | 188 | return attrfile 189 | 190 | 191 | def _parse_size(num_str: str) -> int: 192 | num_str = num_str.upper() 193 | if num_str[-1].isdigit(): 194 | return int(num_str) 195 | elif num_str[-1] == 'K': 196 | return int(num_str[:-1]) * (10**3) 197 | elif num_str[-1] == 'M': 198 | return int(num_str[:-1]) * (10**6) 199 | elif num_str[-1] == 'G': 200 | return int(num_str[:-1]) * (10**9) 201 | raise ValueError(f'Unknown size identifier {num_str[-1]}') 202 | 203 | 204 | def install( 205 | git_config: str, 206 | install_location: str = INSTALL_LOCATION_LOCAL, 207 | python: Optional[str] = None, 208 | attrfile: Optional[str] = None, 209 | ) -> int: 210 | """Install the git filter and set the git attributes.""" 211 | try: 212 | filepath = f'"{PureWindowsPath(python or sys.executable).as_posix()}" -m nbstripout' 213 | check_call(git_config + ['filter.nbstripout.clean', filepath]) 214 | check_call(git_config + ['filter.nbstripout.smudge', 'cat']) 215 | check_call(git_config + ['filter.nbstripout.required', 'true']) 216 | check_call(git_config + ['diff.ipynb.textconv', filepath + ' -t']) 217 | attrfile = _get_attrfile(git_config, install_location, attrfile) 218 | except FileNotFoundError: 219 | print('Installation failed: git is not on path!', file=sys.stderr) 220 | return 1 221 | except CalledProcessError: 222 | print('Installation failed: not a git repository!', file=sys.stderr) 223 | return 1 224 | 225 | # Check if there is already a filter for ipynb files 226 | filt_exists = False 227 | zeppelin_filt_exists = False 228 | diff_exists = False 229 | 230 | if path.exists(attrfile): 231 | with open(attrfile, 'r') as f: 232 | attrs = f.read() 233 | 234 | filt_exists = '*.ipynb filter' in attrs 235 | zeppelin_filt_exists = '*.zpln filter' in attrs 236 | diff_exists = '*.ipynb diff' in attrs 237 | 238 | if filt_exists and diff_exists: 239 | return 0 240 | 241 | try: 242 | with open(attrfile, 'a', newline='') as f: 243 | # If the file already exists, ensure it ends with a new line 244 | if f.tell(): 245 | f.write('\n') 246 | if not filt_exists: 247 | print('*.ipynb filter=nbstripout', file=f) 248 | if not zeppelin_filt_exists: 249 | print('*.zpln filter=nbstripout', file=f) 250 | if not diff_exists: 251 | print('*.ipynb diff=ipynb', file=f) 252 | return 0 253 | except PermissionError: 254 | print(f'Installation failed: could not write to {attrfile}', file=sys.stderr) 255 | 256 | if install_location == INSTALL_LOCATION_GLOBAL: 257 | print('Did you forget to sudo?', file=sys.stderr) 258 | 259 | return 1 260 | 261 | 262 | def uninstall(git_config: str, install_location: str = INSTALL_LOCATION_LOCAL, attrfile: Optional[str] = None) -> int: 263 | """Uninstall the git filter and unset the git attributes.""" 264 | try: 265 | call(git_config + ['--unset', 'filter.nbstripout.clean'], stdout=open(devnull, 'w'), stderr=STDOUT) 266 | call(git_config + ['--unset', 'filter.nbstripout.smudge'], stdout=open(devnull, 'w'), stderr=STDOUT) 267 | call(git_config + ['--unset', 'filter.nbstripout.required'], stdout=open(devnull, 'w'), stderr=STDOUT) 268 | call(git_config + ['--remove-section', 'diff.ipynb'], stdout=open(devnull, 'w'), stderr=STDOUT) 269 | attrfile = _get_attrfile(git_config, install_location, attrfile) 270 | except FileNotFoundError: 271 | print('Uninstall failed: git is not on path!', file=sys.stderr) 272 | return 1 273 | except CalledProcessError: 274 | print('Uninstall failed: not a git repository!', file=sys.stderr) 275 | return 1 276 | 277 | # Check if there is a filter for ipynb files 278 | if path.exists(attrfile): 279 | with open(attrfile, 'r+') as f: 280 | patterns = ('*.ipynb filter', '*.zpln filter', '*.ipynb diff') 281 | lines = [line for line in f if not any(line.startswith(p) for p in patterns)] 282 | f.seek(0) 283 | f.write(''.join(lines)) 284 | f.truncate() 285 | return 0 286 | 287 | 288 | def status(git_config: str, install_location: str = INSTALL_LOCATION_LOCAL, verbose: bool = False) -> int: 289 | """Return 0 if nbstripout is installed in the current repo, 1 otherwise""" 290 | try: 291 | if install_location == INSTALL_LOCATION_SYSTEM: 292 | location = 'system-wide' 293 | elif install_location == INSTALL_LOCATION_GLOBAL: 294 | location = 'globally' 295 | else: 296 | git_dir = path.dirname( 297 | path.abspath(check_output(['git', 'rev-parse', '--git-dir'], universal_newlines=True).strip()) 298 | ) 299 | location = f"in repository '{git_dir}'" 300 | 301 | clean = check_output(git_config + ['filter.nbstripout.clean'], universal_newlines=True).strip() 302 | smudge = check_output(git_config + ['filter.nbstripout.smudge'], universal_newlines=True).strip() 303 | diff = check_output(git_config + ['diff.ipynb.textconv'], universal_newlines=True).strip() 304 | 305 | if install_location in {INSTALL_LOCATION_SYSTEM, INSTALL_LOCATION_GLOBAL}: 306 | attrfile = _get_attrfile(git_config, install_location) 307 | attributes = '' 308 | diff_attributes = '' 309 | 310 | if path.exists(attrfile): 311 | with open(attrfile, 'r') as f: 312 | attrs = f.readlines() 313 | attributes = ''.join(line for line in attrs if 'filter' in line).strip() 314 | diff_attributes = ''.join(line for line in attrs if 'diff' in line).strip() 315 | else: 316 | attributes = check_output(['git', 'check-attr', 'filter', '--', '*.ipynb'], universal_newlines=True).strip() 317 | diff_attributes = check_output( 318 | ['git', 'check-attr', 'diff', '--', '*.ipynb'], universal_newlines=True 319 | ).strip() 320 | 321 | try: 322 | extra_keys = check_output(git_config + ['filter.nbstripout.extrakeys'], universal_newlines=True).strip() 323 | except CalledProcessError: 324 | extra_keys = '' 325 | 326 | if attributes.endswith('unspecified'): 327 | if verbose: 328 | print('nbstripout is not installed', location) 329 | 330 | return 1 331 | 332 | if verbose: 333 | print('nbstripout is installed', location) 334 | print('\nFilter:') 335 | print(' clean =', clean) 336 | print(' smudge =', smudge) 337 | print(' diff=', diff) 338 | print(' extrakeys=', extra_keys) 339 | print('\nAttributes:\n ', attributes) 340 | print('\nDiff Attributes:\n ', diff_attributes) 341 | 342 | return 0 343 | except FileNotFoundError: 344 | print('Cannot determine status: git is not on path!', file=sys.stderr) 345 | 346 | return 1 347 | except CalledProcessError: 348 | if verbose and 'location' in locals(): 349 | print('nbstripout is not installed', location) 350 | 351 | return 1 352 | 353 | 354 | def process_jupyter_notebook( 355 | input_stream: io.IOBase, 356 | output_stream: io.IOBase, 357 | args: Namespace, 358 | extra_keys: List[str], 359 | filename: str = 'input from stdin', 360 | ) -> bool: 361 | with warnings.catch_warnings(): 362 | warnings.simplefilter('ignore', category=UserWarning) 363 | nb = nbformat.read(input_stream, as_version=nbformat.NO_CONVERT) 364 | 365 | nb_orig = copy.deepcopy(nb) 366 | nb_stripped = strip_output( 367 | nb=nb, 368 | keep_output=args.keep_output, 369 | keep_count=args.keep_count, 370 | keep_id=args.keep_id, 371 | extra_keys=extra_keys, 372 | drop_empty_cells=args.drop_empty_cells, 373 | drop_tagged_cells=args.drop_tagged_cells.split(), 374 | strip_init_cells=args.strip_init_cells, 375 | max_size=_parse_size(args.max_size), 376 | ) 377 | 378 | any_change = nb_orig != nb_stripped 379 | 380 | if args.dry_run: 381 | if any_change: 382 | output_stream.write(f'Dry run: would have stripped {filename}\n') 383 | return any_change 384 | 385 | if output_stream.seekable(): 386 | output_stream.seek(0) 387 | output_stream.truncate() 388 | with warnings.catch_warnings(): 389 | warnings.simplefilter('ignore', category=UserWarning) 390 | nbformat.write(nb_stripped, output_stream) 391 | output_stream.flush() 392 | return any_change 393 | 394 | 395 | def process_zeppelin_notebook( 396 | input_stream: io.IOBase, 397 | output_stream: io.IOBase, 398 | args: Namespace, 399 | extra_keys: List[str], 400 | filename: str = 'input from stdin', 401 | ): 402 | nb = json.load(input_stream, object_pairs_hook=collections.OrderedDict) 403 | nb_orig = copy.deepcopy(nb) 404 | nb_stripped = strip_zeppelin_output(nb) 405 | 406 | any_change = nb_orig != nb_stripped 407 | 408 | if args.dry_run: 409 | if any_change: 410 | output_stream.write(f'Dry run: would have stripped {filename}\n') 411 | return any_change 412 | 413 | if output_stream.seekable(): 414 | output_stream.seek(0) 415 | output_stream.truncate() 416 | json.dump(nb_stripped, output_stream, indent=2) 417 | output_stream.write('\n') 418 | output_stream.flush() 419 | return any_change 420 | 421 | 422 | def main(): 423 | parser = ArgumentParser(epilog=__doc__, formatter_class=RawDescriptionHelpFormatter) 424 | task = parser.add_mutually_exclusive_group() 425 | task.add_argument('--dry-run', action='store_true', help='Print which notebooks would have been stripped') 426 | task.add_argument( 427 | '--install', 428 | action='store_true', 429 | help='Install nbstripout in the current repository (set up the git filter and attributes)', 430 | ) 431 | task.add_argument( 432 | '--uninstall', 433 | action='store_true', 434 | help='Uninstall nbstripout from the current repository (remove the git filter and attributes)', 435 | ) 436 | task.add_argument( 437 | '--is-installed', action='store_true', help='Check if nbstripout is installed in current repository' 438 | ) 439 | task.add_argument( 440 | '--status', 441 | action='store_true', 442 | help='Print status of nbstripout installation in current repository and configuration summary if installed', 443 | ) 444 | task.add_argument('--version', action='store_true', help='Print version') 445 | parser.add_argument( 446 | '--verify', action='store_true', help='Return a non-zero exit code if any files were changed, Implies --dry-run' 447 | ) 448 | parser.add_argument('--keep-count', action='store_true', help='Do not strip the execution count/prompt number') 449 | parser.add_argument('--keep-output', action='store_true', help='Do not strip output', default=None) 450 | parser.add_argument( 451 | '--keep-id', 452 | action='store_true', 453 | help='Keep the randomly generated cell ids, which will be different after each execution.', 454 | ) 455 | parser.add_argument( 456 | '--extra-keys', 457 | default='', 458 | help='Space separated list of extra keys to strip from metadata, e.g. metadata.foo cell.metadata.bar', 459 | ) 460 | parser.add_argument( 461 | '--keep-metadata-keys', 462 | default='', 463 | help='Space separated list of metadata keys to keep, e.g. metadata.foo cell.metadata.bar', 464 | ) 465 | parser.add_argument( 466 | '--drop-empty-cells', 467 | action='store_true', 468 | help='Remove cells where `source` is empty or contains only whitepace', 469 | ) 470 | parser.add_argument( 471 | '--drop-tagged-cells', default='', help='Space separated list of cell-tags that remove an entire cell' 472 | ) 473 | parser.add_argument( 474 | '--strip-init-cells', action='store_true', help='Remove cells with `init_cell: true` metadata (default: False)' 475 | ) 476 | parser.add_argument( 477 | '--attributes', 478 | metavar='FILEPATH', 479 | help='Attributes file to add the filter to (in ' 480 | 'combination with --install/--uninstall), ' 481 | 'defaults to .git/info/attributes', 482 | ) 483 | location = parser.add_mutually_exclusive_group() 484 | location.add_argument( 485 | '--global', dest='_global', action='store_true', help='Use global git config (default is local config)' 486 | ) 487 | location.add_argument( 488 | '--system', dest='_system', action='store_true', help='Use system git config (default is local config)' 489 | ) 490 | location.add_argument( 491 | '--python', 492 | dest='_python', 493 | metavar='PATH', 494 | help="Path to python executable to use when --install'ing (default is deduced from `sys.executable`)", 495 | ) 496 | parser.add_argument( 497 | '--force', '-f', action='store_true', help='Strip output also from files with non ipynb extension' 498 | ) 499 | parser.add_argument('--max-size', metavar='SIZE', help='Keep outputs smaller than SIZE', default='0') 500 | parser.add_argument( 501 | '--mode', 502 | '-m', 503 | default='jupyter', 504 | choices=['jupyter', 'zeppelin'], 505 | help='Specify mode between [jupyter (default) | zeppelin] (to be used in combination with -f)', 506 | ) 507 | 508 | parser.add_argument('--textconv', '-t', action='store_true', help='Prints stripped files to STDOUT') 509 | 510 | parser.add_argument('files', nargs='*', help='Files to strip output from') 511 | args = parser.parse_args() 512 | git_config = ['git', 'config'] 513 | 514 | if args.verify and not args.dry_run: 515 | args.dry_run = True 516 | 517 | if args._system: 518 | git_config.append('--system') 519 | install_location = INSTALL_LOCATION_SYSTEM 520 | elif args._global: 521 | git_config.append('--global') 522 | install_location = INSTALL_LOCATION_GLOBAL 523 | else: 524 | git_config.append('--local') 525 | install_location = INSTALL_LOCATION_LOCAL 526 | 527 | if args.install: 528 | raise SystemExit(install(git_config, install_location, python=args._python, attrfile=args.attributes)) 529 | if args.uninstall: 530 | raise SystemExit(uninstall(git_config, install_location, attrfile=args.attributes)) 531 | if args.is_installed: 532 | raise SystemExit(status(git_config, install_location, verbose=False)) 533 | if args.status: 534 | raise SystemExit(status(git_config, install_location, verbose=True)) 535 | if args.version: 536 | print(__version__) 537 | raise SystemExit(0) 538 | 539 | extra_keys = [ 540 | 'metadata.signature', 541 | 'metadata.widgets', 542 | 'cell.metadata.collapsed', 543 | 'cell.metadata.ExecuteTime', 544 | 'cell.metadata.execution', 545 | 'cell.metadata.heading_collapsed', 546 | 'cell.metadata.hidden', 547 | 'cell.metadata.scrolled', 548 | ] 549 | 550 | try: 551 | extra_keys.extend( 552 | check_output( 553 | (git_config if args._system or args._global else ['git', 'config']) + ['filter.nbstripout.extrakeys'], 554 | universal_newlines=True, 555 | ) 556 | .strip() 557 | .split() 558 | ) 559 | except (CalledProcessError, FileNotFoundError): 560 | pass 561 | 562 | extra_keys.extend(args.extra_keys.split()) 563 | 564 | try: 565 | keep_metadata_keys = ( 566 | check_output( 567 | (git_config if args._system or args._global else ['git', 'config']) 568 | + ['filter.nbstripout.keepmetadatakeys'], 569 | universal_newlines=True, 570 | ) 571 | .strip() 572 | .split() 573 | ) 574 | except (CalledProcessError, FileNotFoundError): 575 | keep_metadata_keys = [] 576 | keep_metadata_keys.extend(args.keep_metadata_keys.split()) 577 | extra_keys = [i for i in extra_keys if i not in keep_metadata_keys] 578 | 579 | # Wrap input/output stream in UTF-8 encoded text wrapper 580 | # https://stackoverflow.com/a/16549381 581 | input_stream = io.TextIOWrapper(sys.stdin.buffer, encoding='utf-8') if sys.stdin else None 582 | output_stream = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', newline='') 583 | 584 | process_notebook = {'jupyter': process_jupyter_notebook, 'zeppelin': process_zeppelin_notebook}[args.mode] 585 | any_change = False 586 | for filename in args.files: 587 | if not (args.force or filename.endswith('.ipynb') or filename.endswith('.zpln')): 588 | continue 589 | 590 | try: 591 | with io.open(filename, 'r+', encoding='utf8', newline='') as f: 592 | out = output_stream if args.textconv or args.dry_run else f 593 | if process_notebook( 594 | input_stream=f, output_stream=out, args=args, extra_keys=extra_keys, filename=filename 595 | ): 596 | any_change = True 597 | 598 | except nbformat.reader.NotJSONError: 599 | print(f"No valid notebook detected in '{filename}'", file=sys.stderr) 600 | raise SystemExit(1) 601 | except FileNotFoundError: 602 | print(f"Could not strip '{filename}': file not found", file=sys.stderr) 603 | raise SystemExit(1) 604 | except Exception: 605 | # Ignore exceptions for non-notebook files. 606 | print(f"Could not strip '{filename}'", file=sys.stderr) 607 | raise 608 | 609 | if not args.files and input_stream: 610 | try: 611 | if process_notebook(input_stream, output_stream, args, extra_keys): 612 | any_change = True 613 | except nbformat.reader.NotJSONError: 614 | print('No valid notebook detected on stdin', file=sys.stderr) 615 | raise SystemExit(1) 616 | 617 | if args.verify and any_change: 618 | raise SystemExit(1) 619 | -------------------------------------------------------------------------------- /nbstripout/_utils.py: -------------------------------------------------------------------------------- 1 | from collections import defaultdict 2 | import sys 3 | from typing import Any, Callable, Iterator, List, Optional 4 | 5 | from nbformat import NotebookNode 6 | 7 | __all__ = ['pop_recursive', 'strip_output', 'strip_zeppelin_output', 'MetadataError'] 8 | 9 | 10 | class MetadataError(Exception): 11 | pass 12 | 13 | 14 | def pop_recursive(d: dict, key: str, default: Optional[NotebookNode] = None) -> NotebookNode: 15 | """dict.pop(key) where `key` is a `.`-delimited list of nested keys. 16 | 17 | >>> d = {'a': {'b': 1, 'c': 2}} 18 | >>> pop_recursive(d, 'a.c') 19 | 2 20 | >>> d 21 | {'a': {'b': 1}} 22 | """ 23 | if not isinstance(d, dict): 24 | return default 25 | if key in d: 26 | return d.pop(key, default) 27 | if '.' not in key: 28 | return default 29 | key_head, key_tail = key.split('.', maxsplit=1) 30 | if key_head in d: 31 | return pop_recursive(d[key_head], key=key_tail, default=default) 32 | return default 33 | 34 | 35 | def _cells(nb: NotebookNode, conditionals: Callable[[NotebookNode], bool]) -> Iterator[NotebookNode]: 36 | """Remove cells not satisfying any conditional in conditionals and yield all other cells.""" 37 | if hasattr(nb, 'nbformat') and nb.nbformat < 4: 38 | for ws in nb.worksheets: 39 | for conditional in conditionals: 40 | ws.cells = list(filter(conditional, ws.cells)) 41 | for cell in ws.cells: 42 | yield cell 43 | else: 44 | for conditional in conditionals: 45 | nb.cells = list(filter(conditional, nb.cells)) 46 | for cell in nb.cells: 47 | yield cell 48 | 49 | 50 | def get_size(item: Any) -> int: 51 | """Recursively sums length of all strings in `item`""" 52 | if isinstance(item, str): 53 | return len(item) 54 | elif isinstance(item, list): 55 | return sum(get_size(elem) for elem in item) 56 | elif isinstance(item, dict): 57 | return get_size(list(item.values())) 58 | else: 59 | return len(str(item)) 60 | 61 | 62 | def determine_keep_output(cell: NotebookNode, default: bool, strip_init_cells: bool = False): 63 | """Given a cell, determine whether output should be kept 64 | 65 | Based on whether the metadata has "init_cell": true, 66 | "keep_output": true, or the tags contain "keep_output" """ 67 | if 'metadata' not in cell: 68 | return default 69 | if 'init_cell' in cell.metadata: 70 | return bool(cell.metadata.init_cell) and not strip_init_cells 71 | 72 | has_keep_output_metadata = 'keep_output' in cell.metadata 73 | keep_output_metadata = bool(cell.metadata.get('keep_output', False)) 74 | 75 | has_keep_output_tag = 'keep_output' in cell.metadata.get('tags', []) 76 | 77 | # keep_output between metadata and tags should not contradict each other 78 | if has_keep_output_metadata and has_keep_output_tag and not keep_output_metadata: 79 | raise MetadataError('cell metadata contradicts tags: `keep_output` is false, but `keep_output` in tags') 80 | 81 | if has_keep_output_metadata or has_keep_output_tag: 82 | return keep_output_metadata or has_keep_output_tag 83 | return default 84 | 85 | 86 | def _zeppelin_cells(nb: dict) -> Iterator[dict]: 87 | for pg in nb['paragraphs']: 88 | yield pg 89 | 90 | 91 | def strip_zeppelin_output(nb: dict) -> dict: 92 | for cell in _zeppelin_cells(nb): 93 | if 'results' in cell: 94 | cell['results'] = {} 95 | return nb 96 | 97 | 98 | def strip_output( 99 | nb: NotebookNode, 100 | keep_output: bool, 101 | keep_count: bool, 102 | keep_id: bool, 103 | extra_keys: List[str] = [], 104 | drop_empty_cells: bool = False, 105 | drop_tagged_cells: List[str] = [], 106 | strip_init_cells: bool = False, 107 | max_size: int = 0, 108 | ) -> NotebookNode: 109 | """ 110 | Strip the outputs, execution count/prompt number and miscellaneous 111 | metadata from a notebook object, unless specified to keep either the outputs 112 | or counts. 113 | 114 | `extra_keys` could be 'metadata.foo cell.metadata.bar metadata.baz' 115 | """ 116 | if keep_output is None and 'keep_output' in nb.metadata: 117 | keep_output = bool(nb.metadata['keep_output']) 118 | 119 | keys = defaultdict(list) 120 | for key in extra_keys: 121 | if '.' not in key or key.split('.')[0] not in ['cell', 'metadata']: 122 | sys.stderr.write(f'Ignoring invalid extra key `{key}`\n') 123 | else: 124 | namespace, subkey = key.split('.', maxsplit=1) 125 | keys[namespace].append(subkey) 126 | 127 | for field in keys['metadata']: 128 | pop_recursive(nb.metadata, key=field) 129 | 130 | conditionals = [] 131 | # Keep cells if they have any `source` line that contains non-whitespace 132 | if drop_empty_cells: 133 | conditionals.append(lambda c: any(line.strip() for line in c.get('source', []))) 134 | for tag_to_drop in drop_tagged_cells: 135 | conditionals.append(lambda c: tag_to_drop not in c.get('metadata', {}).get('tags', [])) 136 | 137 | for i, cell in enumerate(_cells(nb, conditionals)): 138 | keep_output_this_cell = determine_keep_output(cell=cell, default=keep_output, strip_init_cells=strip_init_cells) 139 | 140 | # Remove the outputs, unless directed otherwise 141 | if 'outputs' in cell: 142 | # Default behavior (max_size == 0) strips all outputs. 143 | if not keep_output_this_cell: 144 | cell['outputs'] = [output for output in cell['outputs'] if get_size(output) <= max_size] 145 | 146 | # Strip the counts from the outputs that were kept if not keep_count. 147 | if not keep_count: 148 | for output in cell['outputs']: 149 | if 'execution_count' in output: 150 | output['execution_count'] = None 151 | 152 | # If keep_output_this_cell and keep_count, do nothing. 153 | 154 | # Remove the prompt_number/execution_count, unless directed otherwise 155 | if 'prompt_number' in cell and not keep_count: 156 | cell['prompt_number'] = None 157 | if 'execution_count' in cell and not keep_count: 158 | cell['execution_count'] = None 159 | # Replace the cell id with an incremental value that will be consistent across runs 160 | if 'id' in cell and not keep_id: 161 | cell['id'] = str(i) 162 | for field in keys['cell']: 163 | pop_recursive(cell, key=field) 164 | return nb 165 | -------------------------------------------------------------------------------- /pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # strip output of IPython Notebooks 4 | # add this as `.git/hooks/pre-commit` 5 | # to run every time you commit a notebook 6 | # 7 | # requires `nbstripout` to be available on your PATH 8 | # 9 | 10 | if git rev-parse --verify HEAD >/dev/null 2>&1; then 11 | against=HEAD 12 | else 13 | # Initial commit: diff against an empty tree object 14 | against=4b825dc642cb6eb9a060e54bf8d69288fbee4904 15 | fi 16 | 17 | # Find notebooks to be committed 18 | ( 19 | IFS=' 20 | ' 21 | NBS=$(git diff-index -z --cached $against --name-only | grep '.ipynb$' | uniq) 22 | 23 | for NB in $NBS ; do 24 | echo "Removing outputs from $NB" 25 | nbstripout "$NB" 26 | git add "$NB" 27 | done 28 | ) 29 | 30 | exec git diff-index --check --cached $against -- 31 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.ruff] 2 | line-length = 120 3 | indent-width = 4 4 | extend-exclude = ["*.ipynb"] 5 | 6 | [tool.ruff.format] 7 | quote-style = "single" -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | addopts = --ruff 3 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 0.8.1 3 | commit = True 4 | tag = True 5 | tag_name = {new_version} 6 | tag_message = v{new_version} 7 | 8 | [bumpversion:file:CONTRIBUTING.md] 9 | 10 | [bumpversion:file:README.md] 11 | 12 | [bumpversion:file:setup.py] 13 | 14 | [bumpversion:file:nbstripout/_nbstripout.py] 15 | 16 | [bdist_wheel] 17 | universal = 1 18 | 19 | [metadata] 20 | license_files = LICENSE.txt 21 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | with open('README.md') as f: 4 | long_description = f.read() 5 | 6 | install_requires = ['nbformat'] 7 | 8 | setup( 9 | name='nbstripout', 10 | version='0.8.1', 11 | author='Florian Rathgeber', 12 | author_email='florian.rathgeber@gmail.com', 13 | url='https://github.com/kynan/nbstripout', 14 | license='License :: OSI Approved :: MIT License', 15 | description='Strips outputs from Jupyter and IPython notebooks', 16 | long_description=long_description, 17 | long_description_content_type='text/markdown', 18 | packages=find_packages(), 19 | provides=['nbstripout'], 20 | entry_points={ 21 | 'console_scripts': ['nbstripout = nbstripout._nbstripout:main'], 22 | }, 23 | install_requires=install_requires, 24 | python_requires='>=3.8', 25 | classifiers=[ 26 | 'Development Status :: 4 - Beta', 27 | 'Environment :: Other Environment', 28 | 'Framework :: IPython', 29 | 'Intended Audience :: Developers', 30 | 'Programming Language :: Python', 31 | 'Programming Language :: Python :: 3', 32 | 'Programming Language :: Python :: 3.8', 33 | 'Programming Language :: Python :: 3.9', 34 | 'Programming Language :: Python :: 3.10', 35 | 'Programming Language :: Python :: 3.11', 36 | 'Programming Language :: Python :: 3.12', 37 | 'Topic :: Software Development :: Version Control', 38 | ], 39 | ) 40 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | pytest_plugins = 'pytester' 2 | -------------------------------------------------------------------------------- /tests/e2e_notebooks/test_drop_empty_cells.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "metadata": {}, 7 | "outputs": [ 8 | { 9 | "name": "stdout", 10 | "output_type": "stream", 11 | "text": [ 12 | "This cell will always be kept\n" 13 | ] 14 | } 15 | ], 16 | "source": [ 17 | "print('This cell will always be kept')" 18 | ] 19 | }, 20 | { 21 | "cell_type": "markdown", 22 | "metadata": {}, 23 | "source": [ 24 | "# This cell will also be kept" 25 | ] 26 | }, 27 | { 28 | "cell_type": "code", 29 | "execution_count": null, 30 | "metadata": {}, 31 | "outputs": [], 32 | "source": [] 33 | }, 34 | { 35 | "cell_type": "code", 36 | "execution_count": null, 37 | "metadata": {}, 38 | "outputs": [], 39 | "source": [ 40 | "\n", 41 | " \n", 42 | " " 43 | ] 44 | }, 45 | { 46 | "cell_type": "markdown", 47 | "metadata": {}, 48 | "source": [] 49 | } 50 | ], 51 | "metadata": { 52 | "kernelspec": { 53 | "display_name": "Python 3", 54 | "language": "python", 55 | "name": "python3" 56 | }, 57 | "language_info": { 58 | "codemirror_mode": { 59 | "name": "ipython", 60 | "version": 3 61 | }, 62 | "file_extension": ".py", 63 | "mimetype": "text/x-python", 64 | "name": "python", 65 | "nbconvert_exporter": "python", 66 | "pygments_lexer": "ipython3", 67 | "version": "3.9.2" 68 | } 69 | }, 70 | "nbformat": 4, 71 | "nbformat_minor": 4 72 | } 73 | -------------------------------------------------------------------------------- /tests/e2e_notebooks/test_drop_empty_cells.ipynb.expected: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "print('This cell will always be kept')" 10 | ] 11 | }, 12 | { 13 | "cell_type": "markdown", 14 | "metadata": {}, 15 | "source": [ 16 | "# This cell will also be kept" 17 | ] 18 | } 19 | ], 20 | "metadata": { 21 | "kernelspec": { 22 | "display_name": "Python 3", 23 | "language": "python", 24 | "name": "python3" 25 | }, 26 | "language_info": { 27 | "codemirror_mode": { 28 | "name": "ipython", 29 | "version": 3 30 | }, 31 | "file_extension": ".py", 32 | "mimetype": "text/x-python", 33 | "name": "python", 34 | "nbconvert_exporter": "python", 35 | "pygments_lexer": "ipython3", 36 | "version": "3.9.2" 37 | } 38 | }, 39 | "nbformat": 4, 40 | "nbformat_minor": 4 41 | } 42 | -------------------------------------------------------------------------------- /tests/e2e_notebooks/test_drop_empty_cells_dontdrop.ipynb.expected: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "print('This cell will always be kept')" 10 | ] 11 | }, 12 | { 13 | "cell_type": "markdown", 14 | "metadata": {}, 15 | "source": [ 16 | "# This cell will also be kept" 17 | ] 18 | }, 19 | { 20 | "cell_type": "code", 21 | "execution_count": null, 22 | "metadata": {}, 23 | "outputs": [], 24 | "source": [] 25 | }, 26 | { 27 | "cell_type": "code", 28 | "execution_count": null, 29 | "metadata": {}, 30 | "outputs": [], 31 | "source": [ 32 | "\n", 33 | " \n", 34 | " " 35 | ] 36 | }, 37 | { 38 | "cell_type": "markdown", 39 | "metadata": {}, 40 | "source": [] 41 | } 42 | ], 43 | "metadata": { 44 | "kernelspec": { 45 | "display_name": "Python 3", 46 | "language": "python", 47 | "name": "python3" 48 | }, 49 | "language_info": { 50 | "codemirror_mode": { 51 | "name": "ipython", 52 | "version": 3 53 | }, 54 | "file_extension": ".py", 55 | "mimetype": "text/x-python", 56 | "name": "python", 57 | "nbconvert_exporter": "python", 58 | "pygments_lexer": "ipython3", 59 | "version": "3.9.2" 60 | } 61 | }, 62 | "nbformat": 4, 63 | "nbformat_minor": 4 64 | } 65 | -------------------------------------------------------------------------------- /tests/e2e_notebooks/test_drop_tagged_cells.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "metadata": {}, 7 | "outputs": [ 8 | { 9 | "name": "stdout", 10 | "output_type": "stream", 11 | "text": [ 12 | "This cell will always be kept\n" 13 | ] 14 | } 15 | ], 16 | "source": [ 17 | "print('This cell will always be kept')" 18 | ] 19 | }, 20 | { 21 | "cell_type": "markdown", 22 | "metadata": {}, 23 | "source": [ 24 | "# This cell will also be kept" 25 | ] 26 | }, 27 | { 28 | "cell_type": "code", 29 | "execution_count": 3, 30 | "metadata": { 31 | "tags": [ 32 | "test2" 33 | ] 34 | }, 35 | "outputs": [], 36 | "source": [ 37 | "# This Code cell will stay" 38 | ] 39 | }, 40 | { 41 | "cell_type": "code", 42 | "execution_count": 2, 43 | "metadata": { 44 | "tags": [ 45 | "test" 46 | ] 47 | }, 48 | "outputs": [], 49 | "source": [ 50 | "# This is a Code Cell will be removed" 51 | ] 52 | }, 53 | { 54 | "cell_type": "code", 55 | "execution_count": null, 56 | "metadata": {}, 57 | "outputs": [], 58 | "source": [ 59 | "\n", 60 | " \n", 61 | " " 62 | ] 63 | }, 64 | { 65 | "cell_type": "markdown", 66 | "metadata": { 67 | "tags": [ 68 | "test" 69 | ] 70 | }, 71 | "source": [ 72 | "This is markdown and will be removed" 73 | ] 74 | }, 75 | { 76 | "cell_type": "markdown", 77 | "metadata": { 78 | "tags": [ 79 | "test2" 80 | ] 81 | }, 82 | "source": [ 83 | "This is Markdown and will be kept" 84 | ] 85 | }, 86 | { 87 | "cell_type": "code", 88 | "execution_count": null, 89 | "metadata": {}, 90 | "outputs": [], 91 | "source": [] 92 | } 93 | ], 94 | "metadata": { 95 | "kernelspec": { 96 | "display_name": "Python 3 (ipykernel)", 97 | "language": "python", 98 | "name": "python3" 99 | }, 100 | "language_info": { 101 | "codemirror_mode": { 102 | "name": "ipython", 103 | "version": 3 104 | }, 105 | "file_extension": ".py", 106 | "mimetype": "text/x-python", 107 | "name": "python", 108 | "nbconvert_exporter": "python", 109 | "pygments_lexer": "ipython3", 110 | "version": "3.9.6" 111 | } 112 | }, 113 | "nbformat": 4, 114 | "nbformat_minor": 4 115 | } 116 | -------------------------------------------------------------------------------- /tests/e2e_notebooks/test_drop_tagged_cells.ipynb.expected: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "print('This cell will always be kept')" 10 | ] 11 | }, 12 | { 13 | "cell_type": "markdown", 14 | "metadata": {}, 15 | "source": [ 16 | "# This cell will also be kept" 17 | ] 18 | }, 19 | { 20 | "cell_type": "code", 21 | "execution_count": null, 22 | "metadata": { 23 | "tags": [ 24 | "test2" 25 | ] 26 | }, 27 | "outputs": [], 28 | "source": [ 29 | "# This Code cell will stay" 30 | ] 31 | }, 32 | { 33 | "cell_type": "code", 34 | "execution_count": null, 35 | "metadata": {}, 36 | "outputs": [], 37 | "source": [ 38 | "\n", 39 | " \n", 40 | " " 41 | ] 42 | }, 43 | { 44 | "cell_type": "markdown", 45 | "metadata": { 46 | "tags": [ 47 | "test2" 48 | ] 49 | }, 50 | "source": [ 51 | "This is Markdown and will be kept" 52 | ] 53 | }, 54 | { 55 | "cell_type": "code", 56 | "execution_count": null, 57 | "metadata": {}, 58 | "outputs": [], 59 | "source": [] 60 | } 61 | ], 62 | "metadata": { 63 | "kernelspec": { 64 | "display_name": "Python 3 (ipykernel)", 65 | "language": "python", 66 | "name": "python3" 67 | }, 68 | "language_info": { 69 | "codemirror_mode": { 70 | "name": "ipython", 71 | "version": 3 72 | }, 73 | "file_extension": ".py", 74 | "mimetype": "text/x-python", 75 | "name": "python", 76 | "nbconvert_exporter": "python", 77 | "pygments_lexer": "ipython3", 78 | "version": "3.9.6" 79 | } 80 | }, 81 | "nbformat": 4, 82 | "nbformat_minor": 4 83 | } 84 | -------------------------------------------------------------------------------- /tests/e2e_notebooks/test_drop_tagged_cells_dontdrop.ipynb.expected: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "print('This cell will always be kept')" 10 | ] 11 | }, 12 | { 13 | "cell_type": "markdown", 14 | "metadata": {}, 15 | "source": [ 16 | "# This cell will also be kept" 17 | ] 18 | }, 19 | { 20 | "cell_type": "code", 21 | "execution_count": null, 22 | "metadata": { 23 | "tags": [ 24 | "test2" 25 | ] 26 | }, 27 | "outputs": [], 28 | "source": [ 29 | "# This Code cell will stay" 30 | ] 31 | }, 32 | { 33 | "cell_type": "code", 34 | "execution_count": null, 35 | "metadata": { 36 | "tags": [ 37 | "test" 38 | ] 39 | }, 40 | "outputs": [], 41 | "source": [ 42 | "# This is a Code Cell will be removed" 43 | ] 44 | }, 45 | { 46 | "cell_type": "code", 47 | "execution_count": null, 48 | "metadata": {}, 49 | "outputs": [], 50 | "source": [ 51 | "\n", 52 | " \n", 53 | " " 54 | ] 55 | }, 56 | { 57 | "cell_type": "markdown", 58 | "metadata": { 59 | "tags": [ 60 | "test" 61 | ] 62 | }, 63 | "source": [ 64 | "This is markdown and will be removed" 65 | ] 66 | }, 67 | { 68 | "cell_type": "markdown", 69 | "metadata": { 70 | "tags": [ 71 | "test2" 72 | ] 73 | }, 74 | "source": [ 75 | "This is Markdown and will be kept" 76 | ] 77 | }, 78 | { 79 | "cell_type": "code", 80 | "execution_count": null, 81 | "metadata": {}, 82 | "outputs": [], 83 | "source": [] 84 | } 85 | ], 86 | "metadata": { 87 | "kernelspec": { 88 | "display_name": "Python 3 (ipykernel)", 89 | "language": "python", 90 | "name": "python3" 91 | }, 92 | "language_info": { 93 | "codemirror_mode": { 94 | "name": "ipython", 95 | "version": 3 96 | }, 97 | "file_extension": ".py", 98 | "mimetype": "text/x-python", 99 | "name": "python", 100 | "nbconvert_exporter": "python", 101 | "pygments_lexer": "ipython3", 102 | "version": "3.9.6" 103 | } 104 | }, 105 | "nbformat": 4, 106 | "nbformat_minor": 4 107 | } 108 | -------------------------------------------------------------------------------- /tests/e2e_notebooks/test_empty_metadata.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "5c42035d", 6 | "metadata": {}, 7 | "source": [ 8 | "This notebook is missing `metadata.kernelspec`." 9 | ] 10 | } 11 | ], 12 | "metadata": {}, 13 | "nbformat": 4, 14 | "nbformat_minor": 5 15 | } 16 | -------------------------------------------------------------------------------- /tests/e2e_notebooks/test_empty_metadata.ipynb.expected: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "0", 6 | "metadata": {}, 7 | "source": [ 8 | "This notebook is missing `metadata.kernelspec`." 9 | ] 10 | } 11 | ], 12 | "metadata": {}, 13 | "nbformat": 4, 14 | "nbformat_minor": 5 15 | } 16 | -------------------------------------------------------------------------------- /tests/e2e_notebooks/test_execution_timing.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "metadata": { 7 | "execution": { 8 | "iopub.execute_input": "2020-05-28T20:57:07.733742Z", 9 | "iopub.status.busy": "2020-05-28T20:57:07.733470Z", 10 | "iopub.status.idle": "2020-05-28T20:57:07.788874Z", 11 | "shell.execute_reply": "2020-05-28T20:57:07.788239Z", 12 | "shell.execute_reply.started": "2020-05-28T20:57:07.733718Z" 13 | } 14 | }, 15 | "outputs": [ 16 | { 17 | "name": "stdout", 18 | "output_type": "stream", 19 | "text": [ 20 | "Hello World\n" 21 | ] 22 | } 23 | ], 24 | "source": [ 25 | "print('Hello World')" 26 | ] 27 | }, 28 | { 29 | "cell_type": "code", 30 | "execution_count": null, 31 | "metadata": {}, 32 | "outputs": [], 33 | "source": [] 34 | } 35 | ], 36 | "metadata": { 37 | "kernelspec": { 38 | "display_name": "Python 3", 39 | "language": "python", 40 | "name": "python3" 41 | }, 42 | "language_info": { 43 | "codemirror_mode": { 44 | "name": "ipython", 45 | "version": 3 46 | }, 47 | "file_extension": ".py", 48 | "mimetype": "text/x-python", 49 | "name": "python", 50 | "nbconvert_exporter": "python", 51 | "pygments_lexer": "ipython3", 52 | "version": "3.7.4" 53 | } 54 | }, 55 | "nbformat": 4, 56 | "nbformat_minor": 4 57 | } 58 | -------------------------------------------------------------------------------- /tests/e2e_notebooks/test_execution_timing.ipynb.expected: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "print('Hello World')" 10 | ] 11 | }, 12 | { 13 | "cell_type": "code", 14 | "execution_count": null, 15 | "metadata": {}, 16 | "outputs": [], 17 | "source": [] 18 | } 19 | ], 20 | "metadata": { 21 | "kernelspec": { 22 | "display_name": "Python 3", 23 | "language": "python", 24 | "name": "python3" 25 | }, 26 | "language_info": { 27 | "codemirror_mode": { 28 | "name": "ipython", 29 | "version": 3 30 | }, 31 | "file_extension": ".py", 32 | "mimetype": "text/x-python", 33 | "name": "python", 34 | "nbconvert_exporter": "python", 35 | "pygments_lexer": "ipython3", 36 | "version": "3.7.4" 37 | } 38 | }, 39 | "nbformat": 4, 40 | "nbformat_minor": 4 41 | } 42 | -------------------------------------------------------------------------------- /tests/e2e_notebooks/test_invalid_json.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "5c42035d", 6 | "metadata": {}, 7 | "source": [ 8 | "This notebook is not valid JSON." 9 | ] 10 | } 11 | ], 12 | "metadata": { 13 | "kernelspec": { 14 | "display_name": "Python 3 (ipykernel)", 15 | "language": "python", 16 | "name": "python3" 17 | }, 18 | "language_info": { 19 | "codemirror_mode": { 20 | "name": "ipython", 21 | "version": 3 22 | }, 23 | "file_extension": ".py", 24 | "mimetype": "text/x-python", 25 | "name": "python", 26 | "nbconvert_exporter": "python", 27 | "pygments_lexer": "ipython3", 28 | "version": "3.10.6" 29 | } 30 | }, 31 | "nbformat": 4, 32 | "nbformat_minor": 5, 33 | } 34 | -------------------------------------------------------------------------------- /tests/e2e_notebooks/test_keep_metadata_keys.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": { 6 | "scrolled": true 7 | }, 8 | "source": [ 9 | "This notebook tests that using \"--keep-metadata-keys\" works as expected." 10 | ] 11 | }, 12 | { 13 | "cell_type": "code", 14 | "execution_count": null, 15 | "metadata": { 16 | "collapsed": true 17 | }, 18 | "outputs": [], 19 | "source": [ 20 | "1+1" 21 | ] 22 | } 23 | ], 24 | "metadata": {}, 25 | "nbformat": 4, 26 | "nbformat_minor": 0 27 | } 28 | -------------------------------------------------------------------------------- /tests/e2e_notebooks/test_keep_metadata_keys.ipynb.expected: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": { 6 | "scrolled": true 7 | }, 8 | "source": [ 9 | "This notebook tests that using \"--keep-metadata-keys\" works as expected." 10 | ] 11 | }, 12 | { 13 | "cell_type": "code", 14 | "execution_count": null, 15 | "metadata": { 16 | "collapsed": true 17 | }, 18 | "outputs": [], 19 | "source": [ 20 | "1+1" 21 | ] 22 | } 23 | ], 24 | "metadata": {}, 25 | "nbformat": 4, 26 | "nbformat_minor": 0 27 | } 28 | -------------------------------------------------------------------------------- /tests/e2e_notebooks/test_max_size.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "336ba586", 6 | "metadata": {}, 7 | "source": [ 8 | "This notebook tests that outputs can be cleared based on size" 9 | ] 10 | }, 11 | { 12 | "cell_type": "code", 13 | "execution_count": 1, 14 | "id": "a014078d", 15 | "metadata": { 16 | "ExecuteTime": { 17 | "end_time": "2021-05-02T02:39:41.848031Z", 18 | "start_time": "2021-05-02T02:39:41.842433Z" 19 | } 20 | }, 21 | "outputs": [ 22 | { 23 | "name": "stdout", 24 | "output_type": "stream", 25 | "text": [ 26 | "aaaaaaaaaa\n" 27 | ] 28 | } 29 | ], 30 | "source": [ 31 | "print(\"a\"*10)" 32 | ] 33 | }, 34 | { 35 | "cell_type": "code", 36 | "execution_count": 2, 37 | "id": "89ff455b", 38 | "metadata": { 39 | "ExecuteTime": { 40 | "end_time": "2021-05-02T02:39:55.772449Z", 41 | "start_time": "2021-05-02T02:39:55.766400Z" 42 | } 43 | }, 44 | "outputs": [ 45 | { 46 | "name": "stdout", 47 | "output_type": "stream", 48 | "text": [ 49 | "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n" 50 | ] 51 | } 52 | ], 53 | "source": [ 54 | "print(\"a\"*100)" 55 | ] 56 | } 57 | ], 58 | "metadata": { 59 | "kernelspec": { 60 | "display_name": "Python 3", 61 | "language": "python", 62 | "name": "python3" 63 | }, 64 | "language_info": { 65 | "codemirror_mode": { 66 | "name": "ipython", 67 | "version": 3 68 | }, 69 | "file_extension": ".py", 70 | "mimetype": "text/x-python", 71 | "name": "python", 72 | "nbconvert_exporter": "python", 73 | "pygments_lexer": "ipython3", 74 | "version": "3.9.4" 75 | }, 76 | "varInspector": { 77 | "cols": { 78 | "lenName": 16, 79 | "lenType": 16, 80 | "lenVar": 40 81 | }, 82 | "kernels_config": { 83 | "python": { 84 | "delete_cmd_postfix": "", 85 | "delete_cmd_prefix": "del ", 86 | "library": "var_list.py", 87 | "varRefreshCmd": "print(var_dic_list())" 88 | }, 89 | "r": { 90 | "delete_cmd_postfix": ") ", 91 | "delete_cmd_prefix": "rm(", 92 | "library": "var_list.r", 93 | "varRefreshCmd": "cat(var_dic_list()) " 94 | } 95 | }, 96 | "types_to_exclude": [ 97 | "module", 98 | "function", 99 | "builtin_function_or_method", 100 | "instance", 101 | "_Feature" 102 | ], 103 | "window_display": false 104 | } 105 | }, 106 | "nbformat": 4, 107 | "nbformat_minor": 5 108 | } 109 | -------------------------------------------------------------------------------- /tests/e2e_notebooks/test_max_size.ipynb.expected: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "336ba586", 6 | "metadata": {}, 7 | "source": [ 8 | "This notebook tests that outputs can be cleared based on size" 9 | ] 10 | }, 11 | { 12 | "cell_type": "code", 13 | "execution_count": null, 14 | "id": "a014078d", 15 | "metadata": {}, 16 | "outputs": [ 17 | { 18 | "name": "stdout", 19 | "output_type": "stream", 20 | "text": [ 21 | "aaaaaaaaaa\n" 22 | ] 23 | } 24 | ], 25 | "source": [ 26 | "print(\"a\"*10)" 27 | ] 28 | }, 29 | { 30 | "cell_type": "code", 31 | "execution_count": null, 32 | "id": "89ff455b", 33 | "metadata": {}, 34 | "outputs": [], 35 | "source": [ 36 | "print(\"a\"*100)" 37 | ] 38 | } 39 | ], 40 | "metadata": { 41 | "kernelspec": { 42 | "display_name": "Python 3", 43 | "language": "python", 44 | "name": "python3" 45 | }, 46 | "language_info": { 47 | "codemirror_mode": { 48 | "name": "ipython", 49 | "version": 3 50 | }, 51 | "file_extension": ".py", 52 | "mimetype": "text/x-python", 53 | "name": "python", 54 | "nbconvert_exporter": "python", 55 | "pygments_lexer": "ipython3", 56 | "version": "3.9.4" 57 | }, 58 | "varInspector": { 59 | "cols": { 60 | "lenName": 16, 61 | "lenType": 16, 62 | "lenVar": 40 63 | }, 64 | "kernels_config": { 65 | "python": { 66 | "delete_cmd_postfix": "", 67 | "delete_cmd_prefix": "del ", 68 | "library": "var_list.py", 69 | "varRefreshCmd": "print(var_dic_list())" 70 | }, 71 | "r": { 72 | "delete_cmd_postfix": ") ", 73 | "delete_cmd_prefix": "rm(", 74 | "library": "var_list.r", 75 | "varRefreshCmd": "cat(var_dic_list()) " 76 | } 77 | }, 78 | "types_to_exclude": [ 79 | "module", 80 | "function", 81 | "builtin_function_or_method", 82 | "instance", 83 | "_Feature" 84 | ], 85 | "window_display": false 86 | } 87 | }, 88 | "nbformat": 4, 89 | "nbformat_minor": 5 90 | } 91 | -------------------------------------------------------------------------------- /tests/e2e_notebooks/test_max_size.ipynb.expected_sequential_id: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "0", 6 | "metadata": {}, 7 | "source": [ 8 | "This notebook tests that outputs can be cleared based on size" 9 | ] 10 | }, 11 | { 12 | "cell_type": "code", 13 | "execution_count": null, 14 | "id": "1", 15 | "metadata": {}, 16 | "outputs": [ 17 | { 18 | "name": "stdout", 19 | "output_type": "stream", 20 | "text": [ 21 | "aaaaaaaaaa\n" 22 | ] 23 | } 24 | ], 25 | "source": [ 26 | "print(\"a\"*10)" 27 | ] 28 | }, 29 | { 30 | "cell_type": "code", 31 | "execution_count": null, 32 | "id": "2", 33 | "metadata": {}, 34 | "outputs": [], 35 | "source": [ 36 | "print(\"a\"*100)" 37 | ] 38 | } 39 | ], 40 | "metadata": { 41 | "kernelspec": { 42 | "display_name": "Python 3", 43 | "language": "python", 44 | "name": "python3" 45 | }, 46 | "language_info": { 47 | "codemirror_mode": { 48 | "name": "ipython", 49 | "version": 3 50 | }, 51 | "file_extension": ".py", 52 | "mimetype": "text/x-python", 53 | "name": "python", 54 | "nbconvert_exporter": "python", 55 | "pygments_lexer": "ipython3", 56 | "version": "3.9.4" 57 | }, 58 | "varInspector": { 59 | "cols": { 60 | "lenName": 16, 61 | "lenType": 16, 62 | "lenVar": 40 63 | }, 64 | "kernels_config": { 65 | "python": { 66 | "delete_cmd_postfix": "", 67 | "delete_cmd_prefix": "del ", 68 | "library": "var_list.py", 69 | "varRefreshCmd": "print(var_dic_list())" 70 | }, 71 | "r": { 72 | "delete_cmd_postfix": ") ", 73 | "delete_cmd_prefix": "rm(", 74 | "library": "var_list.r", 75 | "varRefreshCmd": "cat(var_dic_list()) " 76 | } 77 | }, 78 | "types_to_exclude": [ 79 | "module", 80 | "function", 81 | "builtin_function_or_method", 82 | "instance", 83 | "_Feature" 84 | ], 85 | "window_display": false 86 | } 87 | }, 88 | "nbformat": 4, 89 | "nbformat_minor": 5 90 | } 91 | -------------------------------------------------------------------------------- /tests/e2e_notebooks/test_metadata.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "This notebook tests that cells with either `\"keep_output\": true` or `\"init_cell\": true` are not stripped." 8 | ] 9 | }, 10 | { 11 | "cell_type": "code", 12 | "execution_count": 1, 13 | "metadata": { 14 | "collapsed": false, 15 | "init_cell": true 16 | }, 17 | "outputs": [ 18 | { 19 | "data": { 20 | "text/plain": [ 21 | "2" 22 | ] 23 | }, 24 | "execution_count": 1, 25 | "metadata": {}, 26 | "output_type": "execute_result" 27 | } 28 | ], 29 | "source": [ 30 | "1+1 # This cell has `\"init_cell:\" true`" 31 | ] 32 | }, 33 | { 34 | "cell_type": "code", 35 | "execution_count": 2, 36 | "metadata": { 37 | "collapsed": false, 38 | "keep_output": true 39 | }, 40 | "outputs": [ 41 | { 42 | "data": { 43 | "text/plain": [ 44 | "4" 45 | ] 46 | }, 47 | "execution_count": 2, 48 | "metadata": {}, 49 | "output_type": "execute_result" 50 | } 51 | ], 52 | "source": [ 53 | "2+2 # This cell has `\"keep_output:\" true`" 54 | ] 55 | }, 56 | { 57 | "cell_type": "code", 58 | "execution_count": 3, 59 | "metadata": { 60 | "collapsed": false 61 | }, 62 | "outputs": [ 63 | { 64 | "data": { 65 | "text/plain": [ 66 | "6" 67 | ] 68 | }, 69 | "execution_count": 3, 70 | "metadata": {}, 71 | "output_type": "execute_result" 72 | } 73 | ], 74 | "source": [ 75 | "3+3" 76 | ] 77 | } 78 | ], 79 | "metadata": { 80 | "celltoolbar": "Edit Metadata", 81 | "kernelspec": { 82 | "display_name": "Python 2", 83 | "language": "python", 84 | "name": "python2" 85 | }, 86 | "language_info": { 87 | "codemirror_mode": { 88 | "name": "ipython", 89 | "version": 2 90 | }, 91 | "file_extension": ".py", 92 | "mimetype": "text/x-python", 93 | "name": "python", 94 | "nbconvert_exporter": "python", 95 | "pygments_lexer": "ipython2", 96 | "version": "2.7.11" 97 | } 98 | }, 99 | "nbformat": 4, 100 | "nbformat_minor": 0 101 | } 102 | -------------------------------------------------------------------------------- /tests/e2e_notebooks/test_metadata.ipynb.expected: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "This notebook tests that cells with either `\"keep_output\": true` or `\"init_cell\": true` are not stripped." 8 | ] 9 | }, 10 | { 11 | "cell_type": "code", 12 | "execution_count": null, 13 | "metadata": { 14 | "init_cell": true 15 | }, 16 | "outputs": [ 17 | { 18 | "data": { 19 | "text/plain": [ 20 | "2" 21 | ] 22 | }, 23 | "execution_count": null, 24 | "metadata": {}, 25 | "output_type": "execute_result" 26 | } 27 | ], 28 | "source": [ 29 | "1+1 # This cell has `\"init_cell:\" true`" 30 | ] 31 | }, 32 | { 33 | "cell_type": "code", 34 | "execution_count": null, 35 | "metadata": { 36 | "keep_output": true 37 | }, 38 | "outputs": [ 39 | { 40 | "data": { 41 | "text/plain": [ 42 | "4" 43 | ] 44 | }, 45 | "execution_count": null, 46 | "metadata": {}, 47 | "output_type": "execute_result" 48 | } 49 | ], 50 | "source": [ 51 | "2+2 # This cell has `\"keep_output:\" true`" 52 | ] 53 | }, 54 | { 55 | "cell_type": "code", 56 | "execution_count": null, 57 | "metadata": {}, 58 | "outputs": [], 59 | "source": [ 60 | "3+3" 61 | ] 62 | } 63 | ], 64 | "metadata": { 65 | "celltoolbar": "Edit Metadata", 66 | "kernelspec": { 67 | "display_name": "Python 2", 68 | "language": "python", 69 | "name": "python2" 70 | }, 71 | "language_info": { 72 | "codemirror_mode": { 73 | "name": "ipython", 74 | "version": 2 75 | }, 76 | "file_extension": ".py", 77 | "mimetype": "text/x-python", 78 | "name": "python", 79 | "nbconvert_exporter": "python", 80 | "pygments_lexer": "ipython2", 81 | "version": "2.7.11" 82 | } 83 | }, 84 | "nbformat": 4, 85 | "nbformat_minor": 0 86 | } 87 | -------------------------------------------------------------------------------- /tests/e2e_notebooks/test_metadata_exception.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "id": "gross-insertion", 7 | "metadata": { 8 | "keep_output": false, 9 | "tags": [ 10 | "keep_output" 11 | ] 12 | }, 13 | "outputs": [ 14 | { 15 | "name": "stdout", 16 | "output_type": "stream", 17 | "text": [ 18 | "this cell has a contradiction between tags\n" 19 | ] 20 | } 21 | ], 22 | "source": [ 23 | "print('this cell has a contradiction between tags')" 24 | ] 25 | } 26 | ], 27 | "metadata": { 28 | "celltoolbar": "Edit Metadata", 29 | "kernelspec": { 30 | "display_name": "Python 3", 31 | "language": "python", 32 | "name": "python3" 33 | }, 34 | "language_info": { 35 | "codemirror_mode": { 36 | "name": "ipython", 37 | "version": 3 38 | }, 39 | "file_extension": ".py", 40 | "mimetype": "text/x-python", 41 | "name": "python", 42 | "nbconvert_exporter": "python", 43 | "pygments_lexer": "ipython3", 44 | "version": "3.9.2" 45 | } 46 | }, 47 | "nbformat": 4, 48 | "nbformat_minor": 5 49 | } 50 | -------------------------------------------------------------------------------- /tests/e2e_notebooks/test_metadata_extra_keys.ipynb.expected: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "This notebook tests that cells with either `\"keep_output\": true` or `\"init_cell\": true` are not stripped." 8 | ] 9 | }, 10 | { 11 | "cell_type": "code", 12 | "execution_count": null, 13 | "metadata": { 14 | "init_cell": true 15 | }, 16 | "outputs": [ 17 | { 18 | "data": { 19 | "text/plain": [ 20 | "2" 21 | ] 22 | }, 23 | "execution_count": null, 24 | "metadata": {}, 25 | "output_type": "execute_result" 26 | } 27 | ], 28 | "source": [ 29 | "1+1 # This cell has `\"init_cell:\" true`" 30 | ] 31 | }, 32 | { 33 | "cell_type": "code", 34 | "execution_count": null, 35 | "metadata": { 36 | "keep_output": true 37 | }, 38 | "outputs": [ 39 | { 40 | "data": { 41 | "text/plain": [ 42 | "4" 43 | ] 44 | }, 45 | "execution_count": null, 46 | "metadata": {}, 47 | "output_type": "execute_result" 48 | } 49 | ], 50 | "source": [ 51 | "2+2 # This cell has `\"keep_output:\" true`" 52 | ] 53 | }, 54 | { 55 | "cell_type": "code", 56 | "execution_count": null, 57 | "metadata": {}, 58 | "outputs": [], 59 | "source": [ 60 | "3+3" 61 | ] 62 | } 63 | ], 64 | "metadata": { 65 | "celltoolbar": "Edit Metadata" 66 | }, 67 | "nbformat": 4, 68 | "nbformat_minor": 0 69 | } 70 | -------------------------------------------------------------------------------- /tests/e2e_notebooks/test_metadata_keep_count.ipynb.expected: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "This notebook tests that cells with either `\"keep_output\": true` or `\"init_cell\": true` are not stripped." 8 | ] 9 | }, 10 | { 11 | "cell_type": "code", 12 | "execution_count": 1, 13 | "metadata": { 14 | "init_cell": true 15 | }, 16 | "outputs": [ 17 | { 18 | "data": { 19 | "text/plain": [ 20 | "2" 21 | ] 22 | }, 23 | "execution_count": 1, 24 | "metadata": {}, 25 | "output_type": "execute_result" 26 | } 27 | ], 28 | "source": [ 29 | "1+1 # This cell has `\"init_cell:\" true`" 30 | ] 31 | }, 32 | { 33 | "cell_type": "code", 34 | "execution_count": 2, 35 | "metadata": { 36 | "keep_output": true 37 | }, 38 | "outputs": [ 39 | { 40 | "data": { 41 | "text/plain": [ 42 | "4" 43 | ] 44 | }, 45 | "execution_count": 2, 46 | "metadata": {}, 47 | "output_type": "execute_result" 48 | } 49 | ], 50 | "source": [ 51 | "2+2 # This cell has `\"keep_output:\" true`" 52 | ] 53 | }, 54 | { 55 | "cell_type": "code", 56 | "execution_count": 3, 57 | "metadata": {}, 58 | "outputs": [], 59 | "source": [ 60 | "3+3" 61 | ] 62 | } 63 | ], 64 | "metadata": { 65 | "celltoolbar": "Edit Metadata", 66 | "kernelspec": { 67 | "display_name": "Python 2", 68 | "language": "python", 69 | "name": "python2" 70 | }, 71 | "language_info": { 72 | "codemirror_mode": { 73 | "name": "ipython", 74 | "version": 2 75 | }, 76 | "file_extension": ".py", 77 | "mimetype": "text/x-python", 78 | "name": "python", 79 | "nbconvert_exporter": "python", 80 | "pygments_lexer": "ipython2", 81 | "version": "2.7.11" 82 | } 83 | }, 84 | "nbformat": 4, 85 | "nbformat_minor": 0 86 | } 87 | -------------------------------------------------------------------------------- /tests/e2e_notebooks/test_metadata_keep_output.ipynb.expected: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "This notebook tests that cells with either `\"keep_output\": true` or `\"init_cell\": true` are not stripped." 8 | ] 9 | }, 10 | { 11 | "cell_type": "code", 12 | "execution_count": null, 13 | "metadata": { 14 | "init_cell": true 15 | }, 16 | "outputs": [ 17 | { 18 | "data": { 19 | "text/plain": [ 20 | "2" 21 | ] 22 | }, 23 | "execution_count": null, 24 | "metadata": {}, 25 | "output_type": "execute_result" 26 | } 27 | ], 28 | "source": [ 29 | "1+1 # This cell has `\"init_cell:\" true`" 30 | ] 31 | }, 32 | { 33 | "cell_type": "code", 34 | "execution_count": null, 35 | "metadata": { 36 | "keep_output": true 37 | }, 38 | "outputs": [ 39 | { 40 | "data": { 41 | "text/plain": [ 42 | "4" 43 | ] 44 | }, 45 | "execution_count": null, 46 | "metadata": {}, 47 | "output_type": "execute_result" 48 | } 49 | ], 50 | "source": [ 51 | "2+2 # This cell has `\"keep_output:\" true`" 52 | ] 53 | }, 54 | { 55 | "cell_type": "code", 56 | "execution_count": null, 57 | "metadata": {}, 58 | "outputs": [ 59 | { 60 | "data": { 61 | "text/plain": [ 62 | "6" 63 | ] 64 | }, 65 | "execution_count": null, 66 | "metadata": {}, 67 | "output_type": "execute_result" 68 | } 69 | ], 70 | "source": [ 71 | "3+3" 72 | ] 73 | } 74 | ], 75 | "metadata": { 76 | "celltoolbar": "Edit Metadata", 77 | "kernelspec": { 78 | "display_name": "Python 2", 79 | "language": "python", 80 | "name": "python2" 81 | }, 82 | "language_info": { 83 | "codemirror_mode": { 84 | "name": "ipython", 85 | "version": 2 86 | }, 87 | "file_extension": ".py", 88 | "mimetype": "text/x-python", 89 | "name": "python", 90 | "nbconvert_exporter": "python", 91 | "pygments_lexer": "ipython2", 92 | "version": "2.7.11" 93 | } 94 | }, 95 | "nbformat": 4, 96 | "nbformat_minor": 0 97 | } 98 | -------------------------------------------------------------------------------- /tests/e2e_notebooks/test_metadata_keep_output_keep_count.ipynb.expected: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "This notebook tests that cells with either `\"keep_output\": true` or `\"init_cell\": true` are not stripped." 8 | ] 9 | }, 10 | { 11 | "cell_type": "code", 12 | "execution_count": 1, 13 | "metadata": { 14 | "init_cell": true 15 | }, 16 | "outputs": [ 17 | { 18 | "data": { 19 | "text/plain": [ 20 | "2" 21 | ] 22 | }, 23 | "execution_count": 1, 24 | "metadata": {}, 25 | "output_type": "execute_result" 26 | } 27 | ], 28 | "source": [ 29 | "1+1 # This cell has `\"init_cell:\" true`" 30 | ] 31 | }, 32 | { 33 | "cell_type": "code", 34 | "execution_count": 2, 35 | "metadata": { 36 | "keep_output": true 37 | }, 38 | "outputs": [ 39 | { 40 | "data": { 41 | "text/plain": [ 42 | "4" 43 | ] 44 | }, 45 | "execution_count": 2, 46 | "metadata": {}, 47 | "output_type": "execute_result" 48 | } 49 | ], 50 | "source": [ 51 | "2+2 # This cell has `\"keep_output:\" true`" 52 | ] 53 | }, 54 | { 55 | "cell_type": "code", 56 | "execution_count": 3, 57 | "metadata": {}, 58 | "outputs": [ 59 | { 60 | "data": { 61 | "text/plain": [ 62 | "6" 63 | ] 64 | }, 65 | "execution_count": 3, 66 | "metadata": {}, 67 | "output_type": "execute_result" 68 | } 69 | ], 70 | "source": [ 71 | "3+3" 72 | ] 73 | } 74 | ], 75 | "metadata": { 76 | "celltoolbar": "Edit Metadata", 77 | "kernelspec": { 78 | "display_name": "Python 2", 79 | "language": "python", 80 | "name": "python2" 81 | }, 82 | "language_info": { 83 | "codemirror_mode": { 84 | "name": "ipython", 85 | "version": 2 86 | }, 87 | "file_extension": ".py", 88 | "mimetype": "text/x-python", 89 | "name": "python", 90 | "nbconvert_exporter": "python", 91 | "pygments_lexer": "ipython2", 92 | "version": "2.7.11" 93 | } 94 | }, 95 | "nbformat": 4, 96 | "nbformat_minor": 0 97 | } 98 | -------------------------------------------------------------------------------- /tests/e2e_notebooks/test_metadata_notebook.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "This notebook tests that when using `\"keep_out\": true` on notebook level, only cells with `\"keep_output\": false` are stripped." 8 | ] 9 | }, 10 | { 11 | "cell_type": "code", 12 | "execution_count": null, 13 | "metadata": {}, 14 | "outputs": [ 15 | { 16 | "data": { 17 | "text/plain": [ 18 | "2" 19 | ] 20 | }, 21 | "execution_count": null, 22 | "metadata": {}, 23 | "output_type": "execute_result" 24 | } 25 | ], 26 | "source": [ 27 | "1+1 # This cell uses the notebook level `\"keep_output:\" true`" 28 | ] 29 | }, 30 | { 31 | "cell_type": "code", 32 | "execution_count": null, 33 | "metadata": { 34 | "keep_output": false 35 | }, 36 | "outputs": [ 37 | { 38 | "data": { 39 | "text/plain": [ 40 | "4" 41 | ] 42 | }, 43 | "execution_count": null, 44 | "metadata": {}, 45 | "output_type": "execute_result" 46 | } 47 | ], 48 | "source": [ 49 | "2+2 # This cell has `\"keep_output:\" false`" 50 | ] 51 | } 52 | ], 53 | "metadata": { 54 | "celltoolbar": "Edit Metadata", 55 | "keep_output": true, 56 | "kernelspec": { 57 | "display_name": "Python 2", 58 | "language": "python", 59 | "name": "python2" 60 | }, 61 | "language_info": { 62 | "codemirror_mode": { 63 | "name": "ipython", 64 | "version": 2 65 | }, 66 | "file_extension": ".py", 67 | "mimetype": "text/x-python", 68 | "name": "python", 69 | "nbconvert_exporter": "python", 70 | "pygments_lexer": "ipython2", 71 | "version": "2.7.11" 72 | } 73 | }, 74 | "nbformat": 4, 75 | "nbformat_minor": 0 76 | } 77 | -------------------------------------------------------------------------------- /tests/e2e_notebooks/test_metadata_notebook.ipynb.expected: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "This notebook tests that when using `\"keep_out\": true` on notebook level, only cells with `\"keep_output\": false` are stripped." 8 | ] 9 | }, 10 | { 11 | "cell_type": "code", 12 | "execution_count": null, 13 | "metadata": {}, 14 | "outputs": [ 15 | { 16 | "data": { 17 | "text/plain": [ 18 | "2" 19 | ] 20 | }, 21 | "execution_count": null, 22 | "metadata": {}, 23 | "output_type": "execute_result" 24 | } 25 | ], 26 | "source": [ 27 | "1+1 # This cell uses the notebook level `\"keep_output:\" true`" 28 | ] 29 | }, 30 | { 31 | "cell_type": "code", 32 | "execution_count": null, 33 | "metadata": { 34 | "keep_output": false 35 | }, 36 | "outputs": [], 37 | "source": [ 38 | "2+2 # This cell has `\"keep_output:\" false`" 39 | ] 40 | } 41 | ], 42 | "metadata": { 43 | "celltoolbar": "Edit Metadata", 44 | "keep_output": true, 45 | "kernelspec": { 46 | "display_name": "Python 2", 47 | "language": "python", 48 | "name": "python2" 49 | }, 50 | "language_info": { 51 | "codemirror_mode": { 52 | "name": "ipython", 53 | "version": 2 54 | }, 55 | "file_extension": ".py", 56 | "mimetype": "text/x-python", 57 | "name": "python", 58 | "nbconvert_exporter": "python", 59 | "pygments_lexer": "ipython2", 60 | "version": "2.7.11" 61 | } 62 | }, 63 | "nbformat": 4, 64 | "nbformat_minor": 0 65 | } 66 | -------------------------------------------------------------------------------- /tests/e2e_notebooks/test_metadata_period.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "source": [ 6 | "This notebook tests that metadata keys with periods can be stripped." 7 | ], 8 | "metadata": { 9 | "application/vnd.databricks.v1+cell": { 10 | "title": "", 11 | "showTitle": false, 12 | "inputWidgets": {}, 13 | "nuid": "4006abf4-c940-4065-aa15-3772229cbe25" 14 | } 15 | } 16 | }, 17 | { 18 | "cell_type": "code", 19 | "source": [ 20 | "1+1" 21 | ], 22 | "metadata": { 23 | "application/vnd.databricks.v1+cell": { 24 | "title": "", 25 | "showTitle": false, 26 | "inputWidgets": {}, 27 | "nuid": "5069d88c-a72a-40b8-acf5-258ee9649736" 28 | } 29 | }, 30 | "outputs": [ 31 | { 32 | "output_type": "display_data", 33 | "metadata": { 34 | "application/vnd.databricks.v1+output": { 35 | "datasetInfos": [], 36 | "data": "
Out[1]: 2
", 37 | "removedWidgets": [], 38 | "addedWidgets": {}, 39 | "type": "html", 40 | "arguments": {} 41 | } 42 | }, 43 | "data": { 44 | "text/html": [ 45 | "\n
Out[1]: 2
" 46 | ] 47 | } 48 | } 49 | ], 50 | "execution_count": 0 51 | } 52 | ], 53 | "metadata": { 54 | "application/vnd.databricks.v1+notebook": { 55 | "notebookName": "test_metadata_period", 56 | "dashboards": [], 57 | "language": "python", 58 | "widgets": {}, 59 | "notebookOrigID": 123456 60 | } 61 | }, 62 | "nbformat": 4, 63 | "nbformat_minor": 0 64 | } 65 | -------------------------------------------------------------------------------- /tests/e2e_notebooks/test_metadata_period.ipynb.expected: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "This notebook tests that metadata keys with periods can be stripped." 8 | ] 9 | }, 10 | { 11 | "cell_type": "code", 12 | "execution_count": null, 13 | "metadata": {}, 14 | "outputs": [], 15 | "source": [ 16 | "1+1" 17 | ] 18 | } 19 | ], 20 | "metadata": {}, 21 | "nbformat": 4, 22 | "nbformat_minor": 0 23 | } 24 | -------------------------------------------------------------------------------- /tests/e2e_notebooks/test_missing_nbformat.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "5c42035d", 6 | "metadata": {}, 7 | "source": [ 8 | "This notebook is missing `nbformat` and `nbformat_minor` keys." 9 | ] 10 | } 11 | ], 12 | "metadata": { 13 | "kernelspec": { 14 | "display_name": "Python 3 (ipykernel)", 15 | "language": "python", 16 | "name": "python3" 17 | }, 18 | "language_info": { 19 | "codemirror_mode": { 20 | "name": "ipython", 21 | "version": 3 22 | }, 23 | "file_extension": ".py", 24 | "mimetype": "text/x-python", 25 | "name": "python", 26 | "nbconvert_exporter": "python", 27 | "pygments_lexer": "ipython3", 28 | "version": "3.10.6" 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /tests/e2e_notebooks/test_missing_nbformat.ipynb.expected: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "0", 6 | "metadata": {}, 7 | "source": [ 8 | "This notebook is missing `nbformat` and `nbformat_minor` keys." 9 | ] 10 | } 11 | ], 12 | "metadata": { 13 | "kernelspec": { 14 | "display_name": "Python 3 (ipykernel)", 15 | "language": "python", 16 | "name": "python3" 17 | }, 18 | "language_info": { 19 | "codemirror_mode": { 20 | "name": "ipython", 21 | "version": 3 22 | }, 23 | "file_extension": ".py", 24 | "mimetype": "text/x-python", 25 | "name": "python", 26 | "nbconvert_exporter": "python", 27 | "pygments_lexer": "ipython3", 28 | "version": "3.10.6" 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /tests/e2e_notebooks/test_nbformat2.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "metadata": { 3 | "name": "01_notebook_introduction" 4 | }, 5 | "nbformat": 2, 6 | "worksheets": [ 7 | { 8 | "cells": [ 9 | { 10 | "cell_type": "code", 11 | "collapsed": false, 12 | "input": [ 13 | "\"This is the new Jupyter notebook\"" 14 | ], 15 | "language": "python", 16 | "outputs": [ 17 | { 18 | "output_type": "pyout", 19 | "prompt_number": 1, 20 | "text": [ 21 | "'This is the new Jupyter notebook'" 22 | ] 23 | } 24 | ], 25 | "prompt_number": 1 26 | }, 27 | { 28 | "cell_type": "code", 29 | "collapsed": false, 30 | "input": [ 31 | "ls" 32 | ], 33 | "language": "python", 34 | "outputs": [ 35 | { 36 | "output_type": "stream", 37 | "stream": "stdout", 38 | "text": [ 39 | "00_notebook_tour.ipynb formatting.ipynb sympy_quantum_computing.ipynb", 40 | "01_notebook_introduction.ipynb python-logo.svg trapezoid_rule.ipynb", 41 | "display_protocol.ipynb sympy.ipynb" 42 | ] 43 | } 44 | ], 45 | "prompt_number": 2 46 | }, 47 | { 48 | "cell_type": "code", 49 | "collapsed": false, 50 | "input": [ 51 | "def f(x):", 52 | " \"\"\"My function", 53 | " x : parameter\"\"\"", 54 | " ", 55 | " return x+1", 56 | "", 57 | "print \"f(3) = \", f(3)" 58 | ], 59 | "language": "python", 60 | "outputs": [ 61 | { 62 | "output_type": "stream", 63 | "stream": "stdout", 64 | "text": [ 65 | "f(3) = 4" 66 | ] 67 | } 68 | ], 69 | "prompt_number": 3 70 | } 71 | ] 72 | } 73 | ] 74 | } 75 | -------------------------------------------------------------------------------- /tests/e2e_notebooks/test_nbformat2.ipynb.expected: -------------------------------------------------------------------------------- 1 | { 2 | "metadata": { 3 | "name": "01_notebook_introduction" 4 | }, 5 | "nbformat": 2, 6 | "worksheets": [ 7 | { 8 | "cells": [ 9 | { 10 | "cell_type": "code", 11 | "collapsed": false, 12 | "input": [ 13 | "\"This is the new Jupyter notebook\"" 14 | ], 15 | "language": "python", 16 | "outputs": [], 17 | "prompt_number": null 18 | }, 19 | { 20 | "cell_type": "code", 21 | "collapsed": false, 22 | "input": [ 23 | "ls" 24 | ], 25 | "language": "python", 26 | "outputs": [], 27 | "prompt_number": null 28 | }, 29 | { 30 | "cell_type": "code", 31 | "collapsed": false, 32 | "input": [ 33 | "def f(x):", 34 | " \"\"\"My function", 35 | " x : parameter\"\"\"", 36 | " ", 37 | " return x+1", 38 | "", 39 | "print \"f(3) = \", f(3)" 40 | ], 41 | "language": "python", 42 | "outputs": [], 43 | "prompt_number": null 44 | } 45 | ] 46 | } 47 | ] 48 | } 49 | -------------------------------------------------------------------------------- /tests/e2e_notebooks/test_nbformat45.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "id": "5c42035d", 7 | "metadata": {}, 8 | "outputs": [ 9 | { 10 | "data": { 11 | "text/plain": [ 12 | "'This is the new Jupyter notebook'" 13 | ] 14 | }, 15 | "execution_count": 1, 16 | "metadata": {}, 17 | "output_type": "execute_result" 18 | } 19 | ], 20 | "source": [ 21 | "\"This is the new Jupyter notebook\"" 22 | ] 23 | }, 24 | { 25 | "cell_type": "code", 26 | "execution_count": 1, 27 | "id": "886205fa", 28 | "metadata": { 29 | "scrolled": false 30 | }, 31 | "outputs": [ 32 | { 33 | "data": { 34 | "text/plain": [ 35 | "'text2'" 36 | ] 37 | }, 38 | "execution_count": 1, 39 | "metadata": {}, 40 | "output_type": "execute_result" 41 | } 42 | ], 43 | "source": [ 44 | "\"text2\"" 45 | ] 46 | }, 47 | { 48 | "cell_type": "code", 49 | "execution_count": 3, 50 | "id": "a183d4e9", 51 | "metadata": {}, 52 | "outputs": [ 53 | { 54 | "name": "stdout", 55 | "output_type": "stream", 56 | "text": [ 57 | "f(3) = 4\n" 58 | ] 59 | } 60 | ], 61 | "source": [ 62 | "def f(x):\n", 63 | " \"\"\"My function\n", 64 | " x : parameter\"\"\"\n", 65 | " \n", 66 | " return x+1\n", 67 | "\n", 68 | "print(\"f(3) = \", f(3))" 69 | ] 70 | } 71 | ], 72 | "metadata": { 73 | "kernelspec": { 74 | "display_name": "Python 3 (ipykernel)", 75 | "language": "python", 76 | "name": "python3" 77 | }, 78 | "language_info": { 79 | "codemirror_mode": { 80 | "name": "ipython", 81 | "version": 3 82 | }, 83 | "file_extension": ".py", 84 | "mimetype": "text/x-python", 85 | "name": "python", 86 | "nbconvert_exporter": "python", 87 | "pygments_lexer": "ipython3", 88 | "version": "3.10.6" 89 | } 90 | }, 91 | "nbformat": 4, 92 | "nbformat_minor": 5 93 | } 94 | -------------------------------------------------------------------------------- /tests/e2e_notebooks/test_nbformat45.ipynb.expected: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "id": "5c42035d", 7 | "metadata": {}, 8 | "outputs": [], 9 | "source": [ 10 | "\"This is the new Jupyter notebook\"" 11 | ] 12 | }, 13 | { 14 | "cell_type": "code", 15 | "execution_count": null, 16 | "id": "886205fa", 17 | "metadata": {}, 18 | "outputs": [], 19 | "source": [ 20 | "\"text2\"" 21 | ] 22 | }, 23 | { 24 | "cell_type": "code", 25 | "execution_count": null, 26 | "id": "a183d4e9", 27 | "metadata": {}, 28 | "outputs": [], 29 | "source": [ 30 | "def f(x):\n", 31 | " \"\"\"My function\n", 32 | " x : parameter\"\"\"\n", 33 | " \n", 34 | " return x+1\n", 35 | "\n", 36 | "print(\"f(3) = \", f(3))" 37 | ] 38 | } 39 | ], 40 | "metadata": { 41 | "kernelspec": { 42 | "display_name": "Python 3 (ipykernel)", 43 | "language": "python", 44 | "name": "python3" 45 | }, 46 | "language_info": { 47 | "codemirror_mode": { 48 | "name": "ipython", 49 | "version": 3 50 | }, 51 | "file_extension": ".py", 52 | "mimetype": "text/x-python", 53 | "name": "python", 54 | "nbconvert_exporter": "python", 55 | "pygments_lexer": "ipython3", 56 | "version": "3.10.6" 57 | } 58 | }, 59 | "nbformat": 4, 60 | "nbformat_minor": 5 61 | } 62 | -------------------------------------------------------------------------------- /tests/e2e_notebooks/test_nbformat45.ipynb.expected_sequential_id: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "id": "0", 7 | "metadata": {}, 8 | "outputs": [], 9 | "source": [ 10 | "\"This is the new Jupyter notebook\"" 11 | ] 12 | }, 13 | { 14 | "cell_type": "code", 15 | "execution_count": null, 16 | "id": "1", 17 | "metadata": {}, 18 | "outputs": [], 19 | "source": [ 20 | "\"text2\"" 21 | ] 22 | }, 23 | { 24 | "cell_type": "code", 25 | "execution_count": null, 26 | "id": "2", 27 | "metadata": {}, 28 | "outputs": [], 29 | "source": [ 30 | "def f(x):\n", 31 | " \"\"\"My function\n", 32 | " x : parameter\"\"\"\n", 33 | " \n", 34 | " return x+1\n", 35 | "\n", 36 | "print(\"f(3) = \", f(3))" 37 | ] 38 | } 39 | ], 40 | "metadata": { 41 | "kernelspec": { 42 | "display_name": "Python 3 (ipykernel)", 43 | "language": "python", 44 | "name": "python3" 45 | }, 46 | "language_info": { 47 | "codemirror_mode": { 48 | "name": "ipython", 49 | "version": 3 50 | }, 51 | "file_extension": ".py", 52 | "mimetype": "text/x-python", 53 | "name": "python", 54 | "nbconvert_exporter": "python", 55 | "pygments_lexer": "ipython3", 56 | "version": "3.10.6" 57 | } 58 | }, 59 | "nbformat": 4, 60 | "nbformat_minor": 5 61 | } 62 | -------------------------------------------------------------------------------- /tests/e2e_notebooks/test_nochange.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "This notebook should not be changed by nbstripout." 8 | ] 9 | }, 10 | { 11 | "cell_type": "code", 12 | "metadata": {}, 13 | "outputs": [], 14 | "source": [ 15 | "1+1 # This cell has `\"init_cell:\" true`" 16 | ] 17 | }, 18 | { 19 | "cell_type": "code", 20 | "metadata": { 21 | "keep_output": true 22 | }, 23 | "outputs": [ 24 | { 25 | "data": { 26 | "text/plain": [ 27 | "4" 28 | ] 29 | }, 30 | "metadata": {}, 31 | "output_type": "execute_result" 32 | } 33 | ], 34 | "source": [ 35 | "2+2 # This cell has `\"keep_output:\" true`" 36 | ] 37 | } 38 | ], 39 | "metadata": { 40 | "celltoolbar": "Edit Metadata", 41 | "kernelspec": { 42 | "display_name": "Python 2", 43 | "language": "python", 44 | "name": "python2" 45 | }, 46 | "language_info": { 47 | "codemirror_mode": { 48 | "name": "ipython", 49 | "version": 2 50 | }, 51 | "file_extension": ".py", 52 | "mimetype": "text/x-python", 53 | "name": "python", 54 | "nbconvert_exporter": "python", 55 | "pygments_lexer": "ipython2", 56 | "version": "2.7.11" 57 | } 58 | }, 59 | "nbformat": 4, 60 | "nbformat_minor": 0 61 | } 62 | -------------------------------------------------------------------------------- /tests/e2e_notebooks/test_strip_init_cells.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "This notebook tests that cells with `\"init_cell\": true` are stripped when the `--strip-init-cells` flag is passed in." 8 | ] 9 | }, 10 | { 11 | "cell_type": "code", 12 | "execution_count": 1, 13 | "metadata": { 14 | "collapsed": false, 15 | "init_cell": true 16 | }, 17 | "outputs": [ 18 | { 19 | "data": { 20 | "text/plain": [ 21 | "2" 22 | ] 23 | }, 24 | "execution_count": 1, 25 | "metadata": {}, 26 | "output_type": "execute_result" 27 | } 28 | ], 29 | "source": [ 30 | "1+1 # This cell has `\"init_cell:\" true`" 31 | ] 32 | } 33 | ], 34 | "metadata": { 35 | "celltoolbar": "Edit Metadata", 36 | "kernelspec": { 37 | "display_name": "Python 2", 38 | "language": "python", 39 | "name": "python2" 40 | }, 41 | "language_info": { 42 | "codemirror_mode": { 43 | "name": "ipython", 44 | "version": 2 45 | }, 46 | "file_extension": ".py", 47 | "mimetype": "text/x-python", 48 | "name": "python", 49 | "nbconvert_exporter": "python", 50 | "pygments_lexer": "ipython2", 51 | "version": "2.7.11" 52 | } 53 | }, 54 | "nbformat": 4, 55 | "nbformat_minor": 0 56 | } 57 | -------------------------------------------------------------------------------- /tests/e2e_notebooks/test_strip_init_cells.ipynb.expected: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "This notebook tests that cells with `\"init_cell\": true` are stripped when the `--strip-init-cells` flag is passed in." 8 | ] 9 | }, 10 | { 11 | "cell_type": "code", 12 | "execution_count": null, 13 | "metadata": { 14 | "init_cell": true 15 | }, 16 | "outputs": [], 17 | "source": [ 18 | "1+1 # This cell has `\"init_cell:\" true`" 19 | ] 20 | } 21 | ], 22 | "metadata": { 23 | "celltoolbar": "Edit Metadata", 24 | "kernelspec": { 25 | "display_name": "Python 2", 26 | "language": "python", 27 | "name": "python2" 28 | }, 29 | "language_info": { 30 | "codemirror_mode": { 31 | "name": "ipython", 32 | "version": 2 33 | }, 34 | "file_extension": ".py", 35 | "mimetype": "text/x-python", 36 | "name": "python", 37 | "nbconvert_exporter": "python", 38 | "pygments_lexer": "ipython2", 39 | "version": "2.7.11" 40 | } 41 | }, 42 | "nbformat": 4, 43 | "nbformat_minor": 0 44 | } 45 | -------------------------------------------------------------------------------- /tests/e2e_notebooks/test_unicode.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 5, 6 | "metadata": { 7 | "collapsed": false 8 | }, 9 | "outputs": [ 10 | { 11 | "name": "stdout", 12 | "output_type": "stream", 13 | "text": [ 14 | "äöü\n" 15 | ] 16 | } 17 | ], 18 | "source": [ 19 | "print u\"äöü\"" 20 | ] 21 | } 22 | ], 23 | "metadata": { 24 | "kernelspec": { 25 | "display_name": "Python 2", 26 | "language": "python", 27 | "name": "python2" 28 | }, 29 | "language_info": { 30 | "codemirror_mode": { 31 | "name": "ipython", 32 | "version": 2 33 | }, 34 | "file_extension": ".py", 35 | "mimetype": "text/x-python", 36 | "name": "python", 37 | "nbconvert_exporter": "python", 38 | "pygments_lexer": "ipython2", 39 | "version": "2.7.11" 40 | } 41 | }, 42 | "nbformat": 4, 43 | "nbformat_minor": 0 44 | } 45 | -------------------------------------------------------------------------------- /tests/e2e_notebooks/test_unicode.ipynb.expected: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "print u\"äöü\"" 10 | ] 11 | } 12 | ], 13 | "metadata": { 14 | "kernelspec": { 15 | "display_name": "Python 2", 16 | "language": "python", 17 | "name": "python2" 18 | }, 19 | "language_info": { 20 | "codemirror_mode": { 21 | "name": "ipython", 22 | "version": 2 23 | }, 24 | "file_extension": ".py", 25 | "mimetype": "text/x-python", 26 | "name": "python", 27 | "nbconvert_exporter": "python", 28 | "pygments_lexer": "ipython2", 29 | "version": "2.7.11" 30 | } 31 | }, 32 | "nbformat": 4, 33 | "nbformat_minor": 0 34 | } 35 | -------------------------------------------------------------------------------- /tests/e2e_notebooks/test_widgets.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 5, 6 | "metadata": { 7 | "collapsed": false 8 | }, 9 | "outputs": [], 10 | "source": [ 11 | "from ipywidgets import interact" 12 | ] 13 | }, 14 | { 15 | "cell_type": "code", 16 | "execution_count": 6, 17 | "metadata": { 18 | "collapsed": false 19 | }, 20 | "outputs": [ 21 | { 22 | "name": "stdout", 23 | "output_type": "stream", 24 | "text": [ 25 | "1\n" 26 | ] 27 | } 28 | ], 29 | "source": [ 30 | "@interact(foo=1)\n", 31 | "def f(foo):\n", 32 | " print(foo)" 33 | ] 34 | } 35 | ], 36 | "metadata": { 37 | "kernelspec": { 38 | "display_name": "Python 3", 39 | "language": "python", 40 | "name": "python3" 41 | }, 42 | "language_info": { 43 | "codemirror_mode": { 44 | "name": "ipython", 45 | "version": 3 46 | }, 47 | "file_extension": ".py", 48 | "mimetype": "text/x-python", 49 | "name": "python", 50 | "nbconvert_exporter": "python", 51 | "pygments_lexer": "ipython3", 52 | "version": "3.5.2" 53 | }, 54 | "widgets": { 55 | "state": { 56 | "0da880c230634707bab4e9cd1fa24285": { 57 | "views": [ 58 | { 59 | "cell_index": 1 60 | } 61 | ] 62 | } 63 | }, 64 | "version": "1.2.0" 65 | } 66 | }, 67 | "nbformat": 4, 68 | "nbformat_minor": 1 69 | } 70 | -------------------------------------------------------------------------------- /tests/e2e_notebooks/test_widgets.ipynb.expected: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "from ipywidgets import interact" 10 | ] 11 | }, 12 | { 13 | "cell_type": "code", 14 | "execution_count": null, 15 | "metadata": {}, 16 | "outputs": [], 17 | "source": [ 18 | "@interact(foo=1)\n", 19 | "def f(foo):\n", 20 | " print(foo)" 21 | ] 22 | } 23 | ], 24 | "metadata": { 25 | "kernelspec": { 26 | "display_name": "Python 3", 27 | "language": "python", 28 | "name": "python3" 29 | }, 30 | "language_info": { 31 | "codemirror_mode": { 32 | "name": "ipython", 33 | "version": 3 34 | }, 35 | "file_extension": ".py", 36 | "mimetype": "text/x-python", 37 | "name": "python", 38 | "nbconvert_exporter": "python", 39 | "pygments_lexer": "ipython3", 40 | "version": "3.5.2" 41 | } 42 | }, 43 | "nbformat": 4, 44 | "nbformat_minor": 1 45 | } 46 | -------------------------------------------------------------------------------- /tests/e2e_notebooks/test_zeppelin.zpln: -------------------------------------------------------------------------------- 1 | { 2 | "paragraphs": [ 3 | { 4 | "text": "%pyspark\nprint(\u0027hello\u0027)\n", 5 | "user": "anonymous", 6 | "dateUpdated": "2020-08-16 14:01:17.340", 7 | "config": { 8 | "colWidth": 12.0, 9 | "fontSize": 9.0, 10 | "enabled": true, 11 | "results": {}, 12 | "editorSetting": { 13 | "language": "python", 14 | "editOnDblClick": false, 15 | "completionKey": "TAB", 16 | "completionSupport": true 17 | }, 18 | "editorMode": "ace/mode/python" 19 | }, 20 | "settings": { 21 | "params": {}, 22 | "forms": {} 23 | }, 24 | "results": { 25 | "code": "SUCCESS", 26 | "msg": [ 27 | { 28 | "type": "TEXT", 29 | "data": "hello\n" 30 | } 31 | ] 32 | }, 33 | "apps": [], 34 | "progressUpdateIntervalMs": 500, 35 | "jobName": "paragraph_1594203870449_-297207159", 36 | "id": "paragraph_1594203870449_-297207159", 37 | "dateCreated": "2020-07-08 15:54:30.449", 38 | "dateStarted": "2020-08-16 14:01:17.351", 39 | "dateFinished": "2020-08-16 14:01:17.391", 40 | "status": "FINISHED" 41 | }, 42 | { 43 | "text": "%pyspark\n", 44 | "user": "anonymous", 45 | "dateUpdated": "2020-07-30 13:52:45.488", 46 | "config": {}, 47 | "settings": { 48 | "params": {}, 49 | "forms": {} 50 | }, 51 | "apps": [], 52 | "progressUpdateIntervalMs": 500, 53 | "jobName": "paragraph_1596097365488_-203822127", 54 | "id": "paragraph_1596097365488_-203822127", 55 | "dateCreated": "2020-07-30 13:52:45.488", 56 | "status": "READY" 57 | } 58 | ], 59 | "name": "test", 60 | "id": "2FCP5JWR7", 61 | "defaultInterpreterGroup": "spark", 62 | "version": "0.9.0-preview1", 63 | "noteParams": {}, 64 | "noteForms": {}, 65 | "angularObjects": {}, 66 | "config": { 67 | "isZeppelinNotebookCronEnable": false 68 | }, 69 | "info": {} 70 | } -------------------------------------------------------------------------------- /tests/e2e_notebooks/test_zeppelin.zpln.expected: -------------------------------------------------------------------------------- 1 | { 2 | "paragraphs": [ 3 | { 4 | "text": "%pyspark\nprint('hello')\n", 5 | "user": "anonymous", 6 | "dateUpdated": "2020-08-16 14:01:17.340", 7 | "config": { 8 | "colWidth": 12.0, 9 | "fontSize": 9.0, 10 | "enabled": true, 11 | "results": {}, 12 | "editorSetting": { 13 | "language": "python", 14 | "editOnDblClick": false, 15 | "completionKey": "TAB", 16 | "completionSupport": true 17 | }, 18 | "editorMode": "ace/mode/python" 19 | }, 20 | "settings": { 21 | "params": {}, 22 | "forms": {} 23 | }, 24 | "results": {}, 25 | "apps": [], 26 | "progressUpdateIntervalMs": 500, 27 | "jobName": "paragraph_1594203870449_-297207159", 28 | "id": "paragraph_1594203870449_-297207159", 29 | "dateCreated": "2020-07-08 15:54:30.449", 30 | "dateStarted": "2020-08-16 14:01:17.351", 31 | "dateFinished": "2020-08-16 14:01:17.391", 32 | "status": "FINISHED" 33 | }, 34 | { 35 | "text": "%pyspark\n", 36 | "user": "anonymous", 37 | "dateUpdated": "2020-07-30 13:52:45.488", 38 | "config": {}, 39 | "settings": { 40 | "params": {}, 41 | "forms": {} 42 | }, 43 | "apps": [], 44 | "progressUpdateIntervalMs": 500, 45 | "jobName": "paragraph_1596097365488_-203822127", 46 | "id": "paragraph_1596097365488_-203822127", 47 | "dateCreated": "2020-07-30 13:52:45.488", 48 | "status": "READY" 49 | } 50 | ], 51 | "name": "test", 52 | "id": "2FCP5JWR7", 53 | "defaultInterpreterGroup": "spark", 54 | "version": "0.9.0-preview1", 55 | "noteParams": {}, 56 | "noteForms": {}, 57 | "angularObjects": {}, 58 | "config": { 59 | "isZeppelinNotebookCronEnable": false 60 | }, 61 | "info": {} 62 | } 63 | -------------------------------------------------------------------------------- /tests/requirements.txt: -------------------------------------------------------------------------------- 1 | pytest 2 | pytest-ruff 3 | -------------------------------------------------------------------------------- /tests/test_diff.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 5, 6 | "metadata": { 7 | "collapsed": false, 8 | "scrolled": true 9 | }, 10 | "outputs": [], 11 | "source": [ 12 | "print(\"aou\")" 13 | ] 14 | } 15 | ], 16 | "metadata": { 17 | "kernelspec": { 18 | "display_name": "Python 2", 19 | "language": "python", 20 | "name": "python2" 21 | }, 22 | "language_info": { 23 | "codemirror_mode": { 24 | "name": "ipython", 25 | "version": 2 26 | }, 27 | "file_extension": ".py", 28 | "mimetype": "text/x-python", 29 | "name": "python", 30 | "nbconvert_exporter": "python", 31 | "pygments_lexer": "ipython2", 32 | "version": "2.7.11" 33 | } 34 | }, 35 | "nbformat": 4, 36 | "nbformat_minor": 0 37 | } 38 | -------------------------------------------------------------------------------- /tests/test_diff_different.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 5, 6 | "metadata": { 7 | "collapsed": false 8 | }, 9 | "outputs": [ 10 | { 11 | "name": "stdout", 12 | "output_type": "stream", 13 | "text": [ 14 | "aou now it is different\n" 15 | ] 16 | } 17 | ], 18 | "source": [ 19 | "print(\"aou now it is different\")" 20 | ] 21 | } 22 | ], 23 | "metadata": { 24 | "kernelspec": { 25 | "display_name": "Python 2", 26 | "language": "python", 27 | "name": "python2" 28 | }, 29 | "language_info": { 30 | "codemirror_mode": { 31 | "name": "ipython", 32 | "version": 2 33 | }, 34 | "file_extension": ".py", 35 | "mimetype": "text/x-python", 36 | "name": "python", 37 | "nbconvert_exporter": "python", 38 | "pygments_lexer": "ipython2", 39 | "version": "2.7.11" 40 | } 41 | }, 42 | "nbformat": 4, 43 | "nbformat_minor": 0 44 | } 45 | -------------------------------------------------------------------------------- /tests/test_diff_different_extrakeys.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "print(\"aou now it is different\")" 10 | ] 11 | } 12 | ], 13 | "metadata": { 14 | "kernelspec": { 15 | "display_name": "Python 2", 16 | "language": "python" 17 | }, 18 | "language_info": { 19 | "codemirror_mode": { 20 | "name": "ipython", 21 | "version": 2 22 | }, 23 | "file_extension": ".py", 24 | "mimetype": "text/x-python", 25 | "name": "python", 26 | "nbconvert_exporter": "python", 27 | "pygments_lexer": "ipython2", 28 | "version": "2.7.11" 29 | } 30 | }, 31 | "nbformat": 4, 32 | "nbformat_minor": 0 33 | } 34 | -------------------------------------------------------------------------------- /tests/test_diff_from_bash_subshells.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | import sys 3 | 4 | import pytest 5 | 6 | # fix this before pytester.chdir() happens 7 | NOTEBOOKS_FOLDER = Path('tests').absolute() 8 | 9 | 10 | def test_diff_with_process_substitution_nodiff(pytester: pytest.Pytester): 11 | if sys.platform.startswith('win'): 12 | pytest.skip('test requires proper bash shell') 13 | 14 | r = pytester.run( 15 | 'bash', 16 | '-c', 17 | f'diff <( nbstripout -t {NOTEBOOKS_FOLDER / "test_diff.ipynb"} ) <( nbstripout -t {NOTEBOOKS_FOLDER / "test_diff_output.ipynb"} )', 18 | ) 19 | assert not r.outlines 20 | assert r.ret == 0 21 | 22 | 23 | def test_diff_with_process_substitution_diff(pytester: pytest.Pytester): 24 | if sys.platform.startswith('win'): 25 | pytest.skip('test requires proper bash shell') 26 | 27 | r = pytester.run( 28 | 'bash', 29 | '-c', 30 | f'diff <( nbstripout -t {NOTEBOOKS_FOLDER / "test_diff.ipynb"} ) <( nbstripout -t {NOTEBOOKS_FOLDER / "test_diff_different.ipynb"} )', 31 | ) 32 | r.stdout.re_match_lines( 33 | r"""(.*) 34 | < "print(\"aou\")" 35 | --- 36 | (.*\"print\(\\\"aou now it is different\\\"\)\") 37 | """.splitlines() 38 | ) 39 | assert r.ret == 1 40 | -------------------------------------------------------------------------------- /tests/test_diff_output.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 5, 6 | "metadata": { 7 | "collapsed": false 8 | }, 9 | "outputs": [ 10 | { 11 | "name": "stdout", 12 | "output_type": "stream", 13 | "text": [ 14 | "aou\n" 15 | ] 16 | } 17 | ], 18 | "source": [ 19 | "print(\"aou\")" 20 | ] 21 | } 22 | ], 23 | "metadata": { 24 | "kernelspec": { 25 | "display_name": "Python 2", 26 | "language": "python", 27 | "name": "python2" 28 | }, 29 | "language_info": { 30 | "codemirror_mode": { 31 | "name": "ipython", 32 | "version": 2 33 | }, 34 | "file_extension": ".py", 35 | "mimetype": "text/x-python", 36 | "name": "python", 37 | "nbconvert_exporter": "python", 38 | "pygments_lexer": "ipython2", 39 | "version": "2.7.11" 40 | } 41 | }, 42 | "nbformat": 4, 43 | "nbformat_minor": 0 44 | } 45 | -------------------------------------------------------------------------------- /tests/test_end_to_end.py: -------------------------------------------------------------------------------- 1 | import os 2 | from pathlib import Path 3 | import re 4 | from subprocess import run, PIPE 5 | 6 | # Note: typing.Pattern is deprecated, for removal in 3.13 in favour of re.Pattern introduced in 3.8 7 | from typing import List, Union, Pattern 8 | 9 | import pytest 10 | 11 | NOTEBOOKS_FOLDER = Path('tests/e2e_notebooks') 12 | 13 | TEST_CASES = [ 14 | ('test_drop_empty_cells.ipynb', 'test_drop_empty_cells_dontdrop.ipynb.expected', []), 15 | ('test_drop_empty_cells.ipynb', 'test_drop_empty_cells.ipynb.expected', ['--drop-empty-cells']), 16 | ('test_drop_tagged_cells.ipynb', 'test_drop_tagged_cells_dontdrop.ipynb.expected', []), 17 | ('test_drop_tagged_cells.ipynb', 'test_drop_tagged_cells.ipynb.expected', ['--drop-tagged-cells=test']), 18 | ('test_execution_timing.ipynb', 'test_execution_timing.ipynb.expected', []), 19 | ('test_max_size.ipynb', 'test_max_size.ipynb.expected', ['--max-size', '50', '--keep-id']), 20 | ('test_max_size.ipynb', 'test_max_size.ipynb.expected_sequential_id', ['--max-size', '50']), 21 | ('test_empty_metadata.ipynb', 'test_empty_metadata.ipynb.expected', []), 22 | ('test_metadata.ipynb', 'test_metadata.ipynb.expected', []), 23 | ( 24 | 'test_metadata.ipynb', 25 | 'test_metadata_extra_keys.ipynb.expected', 26 | ['--extra-keys', 'metadata.kernelspec metadata.language_info'], 27 | ), 28 | ('test_metadata.ipynb', 'test_metadata_keep_count.ipynb.expected', ['--keep-count']), 29 | ('test_metadata.ipynb', 'test_metadata_keep_output.ipynb.expected', ['--keep-output']), 30 | ('test_metadata.ipynb', 'test_metadata_keep_output_keep_count.ipynb.expected', ['--keep-output', '--keep-count']), 31 | ('test_metadata_notebook.ipynb', 'test_metadata_notebook.ipynb.expected', []), 32 | ( 33 | 'test_keep_metadata_keys.ipynb', 34 | 'test_keep_metadata_keys.ipynb.expected', 35 | ['--keep-metadata-keys', 'cell.metadata.scrolled cell.metadata.collapsed metadata.a'], 36 | ), 37 | ( 38 | 'test_metadata_period.ipynb', 39 | 'test_metadata_period.ipynb.expected', 40 | [ 41 | '--extra-keys', 42 | 'cell.metadata.application/vnd.databricks.v1+cell metadata.application/vnd.databricks.v1+notebook', 43 | ], 44 | ), 45 | ('test_strip_init_cells.ipynb', 'test_strip_init_cells.ipynb.expected', ['--strip-init-cells']), 46 | ('test_nbformat2.ipynb', 'test_nbformat2.ipynb.expected', []), 47 | ('test_nbformat45.ipynb', 'test_nbformat45.ipynb.expected', ['--keep-id']), 48 | ('test_nbformat45.ipynb', 'test_nbformat45.ipynb.expected_sequential_id', []), 49 | ('test_missing_nbformat.ipynb', 'test_missing_nbformat.ipynb.expected', []), 50 | ('test_unicode.ipynb', 'test_unicode.ipynb.expected', []), 51 | ('test_widgets.ipynb', 'test_widgets.ipynb.expected', []), 52 | ('test_zeppelin.zpln', 'test_zeppelin.zpln.expected', ['--mode', 'zeppelin']), 53 | ] 54 | 55 | DRY_RUN_CASES = [ 56 | ('test_metadata.ipynb', [], True), 57 | ('test_zeppelin.zpln', ['--mode', 'zeppelin'], True), 58 | ('test_nochange.ipynb', [], False), 59 | ] 60 | 61 | ERR_OUTPUT_CASES = [ 62 | ( 63 | 'test_metadata.ipynb', 64 | ['Ignoring invalid extra key `invalid`', 'Ignoring invalid extra key `foo.invalid`'], 65 | ['--extra-keys', 'invalid foo.invalid'], 66 | ), 67 | ( 68 | 'test_metadata_exception.ipynb', 69 | [ 70 | re.compile( 71 | '.*MetadataError: cell metadata contradicts tags: `keep_output` is false, but `keep_output` in tags' 72 | ) 73 | ], 74 | [], 75 | ), 76 | ('test_invalid_json.ipynb', ['No valid notebook detected on stdin'], []), 77 | ] 78 | 79 | 80 | def nbstripout_exe(): 81 | return os.environ.get('NBSTRIPOUT_EXE', 'nbstripout') 82 | 83 | 84 | @pytest.mark.parametrize('input_file, expected_file, args', TEST_CASES) 85 | @pytest.mark.parametrize('verify', (True, False)) 86 | def test_end_to_end_stdin(input_file: str, expected_file: str, args: List[str], verify: bool): 87 | with open(NOTEBOOKS_FOLDER / expected_file, mode='r') as f: 88 | expected = f.read() 89 | 90 | with open(NOTEBOOKS_FOLDER / input_file, mode='r') as f: 91 | input_ = f.read() 92 | 93 | with open(NOTEBOOKS_FOLDER / input_file, mode='r') as f: 94 | args = [nbstripout_exe()] + args 95 | if verify: 96 | args.append('--verify') 97 | pc = run(args, stdin=f, stdout=PIPE, universal_newlines=True) 98 | output = pc.stdout 99 | 100 | if verify: 101 | # When using stin, the dry run flag is disregarded. 102 | assert pc.returncode == (1 if input_ != expected else 0) 103 | else: 104 | assert output == expected 105 | assert pc.returncode == 0 106 | 107 | 108 | @pytest.mark.parametrize('input_file, expected_file, args', TEST_CASES) 109 | @pytest.mark.parametrize('verify', (True, False)) 110 | def test_end_to_end_file(input_file: str, expected_file: str, args: List[str], tmp_path, verify: bool): 111 | with open(NOTEBOOKS_FOLDER / expected_file, mode='r') as f: 112 | expected = f.read() 113 | 114 | p = tmp_path / input_file 115 | with open(NOTEBOOKS_FOLDER / input_file, mode='r') as f: 116 | p.write_text(f.read()) 117 | 118 | with open(NOTEBOOKS_FOLDER / input_file, mode='r') as f: 119 | input_ = f.read() 120 | 121 | args = [nbstripout_exe(), p] + args 122 | if verify: 123 | args.append('--verify') 124 | pc = run(args, stdout=PIPE, universal_newlines=True) 125 | 126 | output = pc.stdout.strip() 127 | if verify: 128 | if expected != input_: 129 | assert 'Dry run: would have stripped' in output 130 | assert pc.returncode == 1 131 | 132 | # Since verify implies --dry-run, we make sure the file is not modified 133 | with open(NOTEBOOKS_FOLDER / input_file, mode='r') as f: 134 | output_ = f.read() 135 | 136 | assert output_ == input_ 137 | else: 138 | assert pc.returncode == 0 139 | assert not pc.stdout and p.read_text() == expected 140 | 141 | 142 | @pytest.mark.parametrize('input_file, extra_args, any_change', DRY_RUN_CASES) 143 | @pytest.mark.parametrize('verify', (True, False)) 144 | def test_dry_run_stdin(input_file: str, extra_args: List[str], any_change: bool, verify: bool): 145 | expected = 'Dry run: would have stripped input from stdin\n' 146 | 147 | with open(NOTEBOOKS_FOLDER / input_file, mode='r') as f: 148 | args = [nbstripout_exe(), '--dry-run'] + extra_args 149 | if verify: 150 | args.append('--verify') 151 | pc = run(args, stdin=f, stdout=PIPE, universal_newlines=True) 152 | output = pc.stdout 153 | 154 | assert output == (expected if any_change else '') 155 | assert pc.returncode == (1 if verify and any_change else 0) 156 | 157 | 158 | @pytest.mark.parametrize('input_file, extra_args, any_change', DRY_RUN_CASES) 159 | @pytest.mark.parametrize('verify', (True, False)) 160 | def test_dry_run_args(input_file: str, extra_args: List[str], any_change: bool, verify: bool): 161 | expected_regex = re.compile(f'Dry run: would have stripped .*[/\\\\]{input_file}\n') 162 | args = [ 163 | nbstripout_exe(), 164 | str(NOTEBOOKS_FOLDER / input_file), 165 | '--dry-run', 166 | ] + extra_args 167 | if verify: 168 | args.append('--verify') 169 | pc = run(args, stdout=PIPE, universal_newlines=True) 170 | output = pc.stdout 171 | 172 | assert expected_regex.match(output) if any_change else output == '' 173 | assert pc.returncode == (1 if verify and any_change else 0) 174 | 175 | 176 | @pytest.mark.parametrize('input_file, expected_errs, extra_args', ERR_OUTPUT_CASES) 177 | def test_make_errors(input_file: str, expected_errs: List[Union[str, Pattern]], extra_args: List[str]): 178 | with open(NOTEBOOKS_FOLDER / input_file, mode='r') as f: 179 | pc = run([nbstripout_exe(), '--dry-run'] + extra_args, stdin=f, stderr=PIPE, universal_newlines=True) 180 | err_output = pc.stderr 181 | 182 | for e in expected_errs: 183 | if isinstance(e, Pattern): 184 | assert e.search(err_output) 185 | else: 186 | assert e in err_output 187 | -------------------------------------------------------------------------------- /tests/test_git_integration.py: -------------------------------------------------------------------------------- 1 | from configparser import ConfigParser 2 | from pathlib import Path 3 | import re 4 | import sys 5 | 6 | import pytest 7 | 8 | # fix this before pytester.chdir() happens 9 | NOTEBOOKS_FOLDER = Path('tests').absolute() 10 | 11 | 12 | def test_install(pytester: pytest.Pytester): 13 | pytester.run('git', 'init') 14 | assert pytester.run('nbstripout', '--is-installed').ret == 1 15 | pytester.run('nbstripout', '--install') 16 | assert pytester.run('nbstripout', '--is-installed').ret == 0 17 | 18 | with open('.git/info/attributes', 'r') as f: 19 | attr_lines = f.readlines() 20 | assert '*.ipynb filter=nbstripout\n' in attr_lines 21 | assert '*.zpln filter=nbstripout\n' in attr_lines 22 | assert '*.ipynb diff=ipynb\n' in attr_lines 23 | 24 | config = ConfigParser() 25 | config.read('.git/config') 26 | assert re.match(r'.*python.* -m nbstripout', config['filter "nbstripout"']['clean']) 27 | assert config['filter "nbstripout"']['required'] == 'true' 28 | assert config['filter "nbstripout"']['smudge'] == 'cat' 29 | assert re.match(r'.*python.* -m nbstripout -t', config['diff "ipynb"']['textconv']) 30 | 31 | 32 | def test_install_different_python(pytester: pytest.Pytester): 33 | pytester.run('git', 'init') 34 | assert pytester.run('nbstripout', '--is-installed').ret == 1 35 | pytester.run('nbstripout', '--install', '--python', 'DIFFERENTPYTHON') 36 | assert pytester.run('nbstripout', '--is-installed').ret == 0 37 | 38 | config = ConfigParser() 39 | config.read('.git/config') 40 | assert re.match(r'.*DIFFERENTPYTHON.* -m nbstripout', config['filter "nbstripout"']['clean']) 41 | assert sys.executable not in config['filter "nbstripout"']['clean'] 42 | assert config['filter "nbstripout"']['required'] == 'true' 43 | assert config['filter "nbstripout"']['smudge'] == 'cat' 44 | assert re.match(r'.*DIFFERENTPYTHON.* -m nbstripout -t', config['diff "ipynb"']['textconv']) 45 | assert sys.executable not in config['diff "ipynb"']['textconv'] 46 | 47 | 48 | def test_uninstall(pytester: pytest.Pytester): 49 | pytester.run('git', 'init') 50 | # add extra filter at the start, so we can check we don't remove it 51 | pytester.path.joinpath('.git/info/attributes').write_text('*.txt text') 52 | 53 | # do the install and verify 54 | pytester.run('nbstripout', '--install') 55 | assert pytester.run('nbstripout', '--is-installed').ret == 0 56 | 57 | # uninstall and verify (the actual test) 58 | pytester.run('nbstripout', '--uninstall') 59 | assert pytester.run('nbstripout', '--is-installed').ret == 1 60 | 61 | with open('.git/info/attributes', 'r') as f: 62 | attr_lines = f.readlines() 63 | assert '*.txt text\n' in attr_lines # still there and not removed 64 | assert len(attr_lines) == 1 65 | 66 | config = ConfigParser() 67 | config.read('.git/config') 68 | assert 'filter "nbstripout"' not in config 69 | assert 'diff "ipynb"' not in config 70 | 71 | 72 | def test_uninstall_leave_extrakeys(pytester: pytest.Pytester): 73 | pytester.run('git', 'init') 74 | 75 | # add extrakeys so we can check that we don't remove them 76 | pytester.run('git', 'config', 'filter.nbstripout.extrakeys', 'spam eggs') 77 | 78 | # check not installed 79 | assert pytester.run('nbstripout', '--is-installed').ret == 1 80 | 81 | # do the install and verify 82 | pytester.run('nbstripout', '--install') 83 | assert pytester.run('nbstripout', '--is-installed').ret == 0 84 | 85 | # uninstall and verify 86 | pytester.run('nbstripout', '--uninstall') 87 | assert pytester.run('nbstripout', '--is-installed').ret == 1 88 | 89 | # check extrakeys still exist 90 | r = pytester.run('git', 'config', 'filter.nbstripout.extrakeys') 91 | assert r.stdout.str() == 'spam eggs' 92 | 93 | 94 | def test_status(pytester: pytest.Pytester): 95 | pytester.run('git', 'init') 96 | 97 | # status when not installed 98 | r = pytester.run('nbstripout', '--status') 99 | r.stdout.fnmatch_lines(['nbstripout is not installed in repository *']) 100 | assert r.ret == 1 101 | 102 | # do the install and verify 103 | pytester.run('nbstripout', '--install') 104 | r = pytester.run('nbstripout', '--status') 105 | assert r.ret == 0 106 | r.stdout.re_match_lines( 107 | r"""nbstripout is installed in repository .* 108 | \s* 109 | Filter: 110 | clean = .* -m nbstripout 111 | smudge = cat 112 | diff= .* -m nbstripout -t 113 | extrakeys=\s* 114 | \s* 115 | Attributes: 116 | \*.ipynb: filter: nbstripout 117 | \s* 118 | Diff Attributes: 119 | \*.ipynb: diff: ipynb 120 | """.splitlines() 121 | ) 122 | 123 | # uninstall and verify 124 | pytester.run('nbstripout', '--uninstall') 125 | r = pytester.run('nbstripout', '--status') 126 | r.stdout.fnmatch_lines(['nbstripout is not installed in repository *']) 127 | assert r.ret == 1 128 | 129 | 130 | def test_git_diff_nodiff(pytester: pytest.Pytester): 131 | pytester.run('git', 'init') 132 | pytester.run('git', 'config', '--local', 'filter.nbstripout.extrakeys', ' ') 133 | pytester.run('nbstripout', '--install') 134 | 135 | r = pytester.run( 136 | 'git', 137 | 'diff', 138 | '--no-index', 139 | '--no-ext-diff', 140 | '--unified=0', 141 | '--exit-code', 142 | '-a', 143 | '--no-prefix', 144 | NOTEBOOKS_FOLDER / 'test_diff.ipynb', 145 | NOTEBOOKS_FOLDER / 'test_diff_output.ipynb', 146 | ) 147 | assert r.ret == 0 148 | assert not r.outlines 149 | 150 | 151 | def test_git_diff_diff(pytester: pytest.Pytester): 152 | pytester.run('git', 'init') 153 | pytester.run('git', 'config', '--local', 'filter.nbstripout.extrakeys', ' ') 154 | pytester.run('nbstripout', '--install') 155 | 156 | r = pytester.run( 157 | 'git', 158 | 'diff', 159 | '--no-index', 160 | NOTEBOOKS_FOLDER / 'test_diff.ipynb', 161 | NOTEBOOKS_FOLDER / 'test_diff_different_extrakeys.ipynb', 162 | ) 163 | assert r.ret == 1 164 | r.stdout.fnmatch_lines( 165 | r"""index* 166 | --- *test_diff.ipynb* 167 | +++ *test_diff_different_extrakeys.ipynb* 168 | @@ -6,15 +6,14 @@ 169 | "metadata": {}, 170 | "outputs": [], 171 | "source": [ 172 | - "print(\"aou\")" 173 | + "print(\"aou now it is different\")" 174 | ] 175 | } 176 | ], 177 | "metadata": { 178 | "kernelspec": { 179 | "display_name": "Python 2", 180 | - "language": "python", 181 | - "name": "python2" 182 | + "language": "python" 183 | }, 184 | "language_info": { 185 | "codemirror_mode": { 186 | """.splitlines() 187 | ) 188 | assert len(r.outlines) == 22 # 21 lines + new line at end 189 | 190 | 191 | def test_git_diff_extrakeys(pytester: pytest.Pytester): 192 | pytester.run('git', 'init') 193 | pytester.run( 194 | 'git', 'config', '--local', 'filter.nbstripout.extrakeys', 'cell.metadata.collapsed metadata.kernelspec.name' 195 | ) 196 | pytester.run('nbstripout', '--install') 197 | 198 | r = pytester.run( 199 | 'git', 200 | 'diff', 201 | '--no-index', 202 | NOTEBOOKS_FOLDER / 'test_diff.ipynb', 203 | NOTEBOOKS_FOLDER / 'test_diff_different_extrakeys.ipynb', 204 | ) 205 | assert r.ret == 1 206 | r.stdout.fnmatch_lines( 207 | r"""index* 208 | --- *test_diff.ipynb* 209 | +++ *test_diff_different_extrakeys.ipynb* 210 | @@ -6,7 +6,7 @@ 211 | "metadata": {}, 212 | "outputs": [], 213 | "source": [ 214 | - "print(\"aou\")" 215 | + "print(\"aou now it is different\")" 216 | ] 217 | } 218 | ], 219 | """.splitlines() 220 | ) 221 | assert len(r.outlines) == 13 # 12 lines + new line at end 222 | 223 | 224 | def test_git_diff_keepmetadatakeys(pytester: pytest.Pytester): 225 | pytester.run('git', 'init') 226 | pytester.run( 227 | 'git', 'config', '--local', 'filter.nbstripout.keepmetadatakeys', 'cell.metadata.scrolled metadata.foo.bar' 228 | ) 229 | pytester.run('nbstripout', '--install') 230 | 231 | r = pytester.run( 232 | 'git', 233 | 'diff', 234 | '--no-index', 235 | NOTEBOOKS_FOLDER / 'test_diff.ipynb', 236 | NOTEBOOKS_FOLDER / 'test_diff_different_extrakeys.ipynb', 237 | ) 238 | assert r.ret == 1 239 | r.stdout.fnmatch_lines( 240 | r"""index* 241 | --- *test_diff.ipynb* 242 | +++ *test_diff_different_extrakeys.ipynb* 243 | @@ -3,20 +3,17 @@ 244 | { 245 | "cell_type": "code", 246 | "execution_count": null, 247 | - "metadata": { 248 | - "scrolled": true 249 | - }, 250 | + "metadata": {}, 251 | "outputs": [], 252 | "source": [ 253 | - "print(\"aou\")" 254 | + "print(\"aou now it is different\")" 255 | ] 256 | } 257 | ], 258 | """.splitlines() 259 | ) 260 | assert len(r.outlines) == 28 # 12 lines + new line at end 261 | -------------------------------------------------------------------------------- /tests/test_keep_output_tags.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "This notebook tests that output is stripped unless the cell has a `keep_output` tag in its metadata" 8 | ] 9 | }, 10 | { 11 | "cell_type": "code", 12 | "execution_count": 1, 13 | "metadata": { 14 | "ExecuteTime": { 15 | "end_time": "2020-03-15T03:18:30.324432Z", 16 | "start_time": "2020-03-15T03:18:30.313427Z" 17 | } 18 | }, 19 | "outputs": [ 20 | { 21 | "name": "stdout", 22 | "output_type": "stream", 23 | "text": [ 24 | "This cell should have no output, because it does not have keep_output in its metadata (top level) or its tags\n" 25 | ] 26 | } 27 | ], 28 | "source": [ 29 | "# no_output\n", 30 | "print('''This cell should have no output, because it does not have keep_output in its metadata (top level) or its tags''')" 31 | ] 32 | }, 33 | { 34 | "cell_type": "code", 35 | "execution_count": 2, 36 | "metadata": { 37 | "ExecuteTime": { 38 | "end_time": "2020-03-15T03:18:30.381986Z", 39 | "start_time": "2020-03-15T03:18:30.329424Z" 40 | }, 41 | "tags": [ 42 | "keep_output" 43 | ] 44 | }, 45 | "outputs": [ 46 | { 47 | "name": "stdout", 48 | "output_type": "stream", 49 | "text": [ 50 | "This cell has keep_output in its tags metadata\n" 51 | ] 52 | } 53 | ], 54 | "source": [ 55 | "# output\n", 56 | "print('''This cell has keep_output in its tags metadata''')" 57 | ] 58 | }, 59 | { 60 | "cell_type": "code", 61 | "execution_count": 3, 62 | "metadata": { 63 | "ExecuteTime": { 64 | "end_time": "2020-03-15T03:18:30.393917Z", 65 | "start_time": "2020-03-15T03:18:30.386910Z" 66 | }, 67 | "keep_output": true 68 | }, 69 | "outputs": [ 70 | { 71 | "name": "stdout", 72 | "output_type": "stream", 73 | "text": [ 74 | "This cell has \"keep_output\": true in it's metadata but not in its tags\n" 75 | ] 76 | } 77 | ], 78 | "source": [ 79 | "# output\n", 80 | "print('''This cell has \"keep_output\": true in it's metadata but not in its tags''')" 81 | ] 82 | } 83 | ], 84 | "metadata": { 85 | "celltoolbar": "Edit Metadata", 86 | "kernelspec": { 87 | "display_name": "local", 88 | "language": "python", 89 | "name": "local" 90 | }, 91 | "language_info": { 92 | "codemirror_mode": { 93 | "name": "ipython", 94 | "version": 3 95 | }, 96 | "file_extension": ".py", 97 | "mimetype": "text/x-python", 98 | "name": "python", 99 | "nbconvert_exporter": "python", 100 | "pygments_lexer": "ipython3", 101 | "version": "3.7.4" 102 | } 103 | }, 104 | "nbformat": 4, 105 | "nbformat_minor": 2 106 | } 107 | -------------------------------------------------------------------------------- /tests/test_keep_output_tags.py: -------------------------------------------------------------------------------- 1 | from copy import deepcopy 2 | import os 3 | import re 4 | 5 | import nbformat 6 | import pytest 7 | 8 | from nbstripout import strip_output, MetadataError 9 | 10 | directory = os.path.dirname(__file__) 11 | 12 | 13 | @pytest.fixture 14 | def orig_nb(): 15 | fname = 'test_keep_output_tags.ipynb' 16 | return nbformat.read(os.path.join(directory, fname), nbformat.NO_CONVERT) 17 | 18 | 19 | @pytest.fixture 20 | def nb_with_exception(): 21 | fname = 'test_keep_output_tags_exception.ipynb' 22 | return nbformat.read(os.path.join(directory, fname), nbformat.NO_CONVERT) 23 | 24 | 25 | def test_cells(orig_nb): 26 | nb_stripped = deepcopy(orig_nb) 27 | nb_stripped = strip_output(nb_stripped, keep_output=None, keep_count=None, keep_id=None) 28 | for i, cell in enumerate(nb_stripped.cells): 29 | if cell.cell_type == 'code' and cell.source: 30 | match = re.match(r'\s*#\s*(output|no_output)', cell.source) 31 | if match: 32 | # original cell should have had output. 33 | # If not, there's a problem with the test fixture 34 | assert orig_nb.cells[i].outputs 35 | 36 | if match.group(1) == 'output': 37 | assert len(cell.outputs) > 0 38 | else: 39 | assert len(cell.outputs) == 0 40 | 41 | 42 | def test_exception(nb_with_exception): 43 | with pytest.raises(MetadataError): 44 | strip_output(nb_with_exception, keep_output=None, keep_count=None, keep_id=None) 45 | -------------------------------------------------------------------------------- /tests/test_keep_output_tags_exception.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "This notebook tests that output is stripped unless the cell has a `keep_output` tag in its metadata" 8 | ] 9 | }, 10 | { 11 | "cell_type": "code", 12 | "execution_count": 4, 13 | "metadata": { 14 | "ExecuteTime": { 15 | "end_time": "2020-03-15T03:18:30.406033Z", 16 | "start_time": "2020-03-15T03:18:30.398910Z" 17 | }, 18 | "keep_output": false, 19 | "tags": [ 20 | "keep_output" 21 | ] 22 | }, 23 | "outputs": [ 24 | { 25 | "name": "stdout", 26 | "output_type": "stream", 27 | "text": [ 28 | "This cell should raise an exception, because the metadata and tags conflict in terms of keep_output\n" 29 | ] 30 | } 31 | ], 32 | "source": [ 33 | "# exception\n", 34 | "print('''This cell should raise an exception, because the metadata and tags conflict in terms of keep_output''')" 35 | ] 36 | } 37 | ], 38 | "metadata": { 39 | "celltoolbar": "Edit Metadata", 40 | "kernelspec": { 41 | "display_name": "local", 42 | "language": "python", 43 | "name": "local" 44 | }, 45 | "language_info": { 46 | "codemirror_mode": { 47 | "name": "ipython", 48 | "version": 3 49 | }, 50 | "file_extension": ".py", 51 | "mimetype": "text/x-python", 52 | "name": "python", 53 | "nbconvert_exporter": "python", 54 | "pygments_lexer": "ipython3", 55 | "version": "3.7.4" 56 | } 57 | }, 58 | "nbformat": 4, 59 | "nbformat_minor": 2 60 | } 61 | -------------------------------------------------------------------------------- /tests/test_utils.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from nbstripout._utils import pop_recursive 4 | 5 | 6 | def make_dict(): 7 | return {'a': {'b': 1, 'c': 2, 'd.e': 3, 'f': {'g': 4}}} 8 | 9 | 10 | def make_data(default=None): 11 | return [ 12 | ('a.c', 2, {'a': {'b': 1, 'd.e': 3, 'f': {'g': 4}}}), 13 | ('a.d.e', 3, {'a': {'b': 1, 'c': 2, 'f': {'g': 4}}}), 14 | ('a.f', {'g': 4}, {'a': {'b': 1, 'c': 2, 'd.e': 3}}), 15 | ('a.f.g', 4, {'a': {'b': 1, 'c': 2, 'd.e': 3, 'f': {}}}), 16 | ('a', {'b': 1, 'c': 2, 'd.e': 3, 'f': {'g': 4}}, {}), 17 | ('notfound', default, make_dict()), 18 | ('a.notfound', default, make_dict()), 19 | ('a.b.notfound', default, make_dict()), 20 | ] 21 | 22 | 23 | @pytest.fixture 24 | def d(): 25 | return make_dict() 26 | 27 | 28 | @pytest.mark.parametrize(('key', 'res', 'remainder'), make_data()) 29 | def test_pop_recursive(d, key, res, remainder): 30 | assert pop_recursive(d, key) == res 31 | assert d == remainder 32 | 33 | 34 | @pytest.mark.parametrize(('key', 'res', 'remainder'), make_data(default=0)) 35 | def test_pop_recursive_default(d, key, res, remainder): 36 | assert pop_recursive(d, key, default=0) == res 37 | assert d == remainder 38 | --------------------------------------------------------------------------------