From 4a291d54f911e0528853f85d9504f2b0d872917a Mon Sep 17 00:00:00 2001 From: Sebastian Weigand Date: Sat, 7 Feb 2026 19:25:42 +0100 Subject: [PATCH 1/8] =?UTF-8?q?=E2=9C=A8=20Add=20--force-lf-eol=20flag=20t?= =?UTF-8?q?o=20normalize=20lineendings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- nbstripout/_nbstripout.py | 5 ++++- tests/test_end_to_end.py | 17 +++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/nbstripout/_nbstripout.py b/nbstripout/_nbstripout.py index 41dfe4d..a8bfab7 100644 --- a/nbstripout/_nbstripout.py +++ b/nbstripout/_nbstripout.py @@ -531,6 +531,8 @@ def main(): parser.add_argument('--textconv', '-t', action='store_true', help='Prints stripped files to STDOUT') + parser.add_argument('--force-lf-eol', action='store_true', help='Force LF line endings when writing files') + parser.add_argument('files', nargs='*', help='Files to strip output from') args = parser.parse_args() git_config = ['git', 'config'] @@ -612,7 +614,8 @@ def main(): continue try: - with io.open(filename, 'r+', encoding='utf8') as f: + file_newline = '' if args.force_lf_eol else None + with io.open(filename, 'r+', encoding='utf8', newline=file_newline) as f: out = output_stream if args.textconv or args.dry_run else f if process_notebook( input_stream=f, output_stream=out, args=args, extra_keys=extra_keys, filename=filename diff --git a/tests/test_end_to_end.py b/tests/test_end_to_end.py index f0bfb83..70987bb 100644 --- a/tests/test_end_to_end.py +++ b/tests/test_end_to_end.py @@ -1,4 +1,5 @@ import os +import sys from pathlib import Path import re from subprocess import run, PIPE @@ -202,3 +203,19 @@ def test_nochange_notebook_unchanged(): zpln_mtime_after = zpln_file.stat().st_mtime_ns assert zpln_mtime_after == zpln_mtime_before + + +def test_force_lf_eol(tmp_path: Path): + input_content = (NOTEBOOKS_FOLDER / 'test_drop_empty_cells.ipynb').read_bytes().replace(b'\n', b'\r\n') + + p = tmp_path / 'input.ipynb' + p.write_bytes(input_content) + + run([nbstripout_exe(), p]) + if sys.platform == 'win32': + assert b'\r\n' in p.read_bytes() + else: + assert b'\r\n' not in p.read_bytes() + + run([nbstripout_exe(), '--force-lf-eol', p]) + assert b'\r\n' not in p.read_bytes() From df58a04f01e964c1654661c4850ddd50b44a9cd7 Mon Sep 17 00:00:00 2001 From: Sebastian Weigand Date: Sun, 8 Feb 2026 11:44:44 +0100 Subject: [PATCH 2/8] Apply suggestion from @kynan to rename test Co-authored-by: Florian Rathgeber --- tests/test_end_to_end.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_end_to_end.py b/tests/test_end_to_end.py index 70987bb..d3f45ec 100644 --- a/tests/test_end_to_end.py +++ b/tests/test_end_to_end.py @@ -205,7 +205,7 @@ def test_nochange_notebook_unchanged(): assert zpln_mtime_after == zpln_mtime_before -def test_force_lf_eol(tmp_path: Path): +def test_newline_behavior(tmp_path: Path): input_content = (NOTEBOOKS_FOLDER / 'test_drop_empty_cells.ipynb').read_bytes().replace(b'\n', b'\r\n') p = tmp_path / 'input.ipynb' From 0e686017899436007384c85df490e75bdbbcd5a7 Mon Sep 17 00:00:00 2001 From: Sebastian Weigand Date: Sun, 8 Feb 2026 16:02:09 +0100 Subject: [PATCH 3/8] =?UTF-8?q?=F0=9F=91=8C=20Add=20requested=20review=20c?= =?UTF-8?q?hanges?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ๐Ÿงช Use different files for test with and without flag ๐Ÿ‘Œ Use same newline convention for stdout with textconv ๐Ÿงช Add test for stdout with textconv --- nbstripout/_nbstripout.py | 9 +++++---- tests/test_end_to_end.py | 26 +++++++++++++++++++------- 2 files changed, 24 insertions(+), 11 deletions(-) diff --git a/nbstripout/_nbstripout.py b/nbstripout/_nbstripout.py index a8bfab7..78e330c 100644 --- a/nbstripout/_nbstripout.py +++ b/nbstripout/_nbstripout.py @@ -531,7 +531,7 @@ def main(): parser.add_argument('--textconv', '-t', action='store_true', help='Prints stripped files to STDOUT') - parser.add_argument('--force-lf-eol', action='store_true', help='Force LF line endings when writing files') + parser.add_argument('--preserve-newlines', action='store_true', help='Preserve OS line endings when writing files') parser.add_argument('files', nargs='*', help='Files to strip output from') args = parser.parse_args() @@ -602,10 +602,12 @@ def main(): keep_metadata_keys.extend(args.keep_metadata_keys.split()) extra_keys = [i for i in extra_keys if i not in keep_metadata_keys] + newline = None if args.preserve_newlines else '' + # Wrap input/output stream in UTF-8 encoded text wrapper # https://stackoverflow.com/a/16549381 input_stream = io.TextIOWrapper(sys.stdin.buffer, encoding='utf-8') if sys.stdin else None - output_stream = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', newline='') + output_stream = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', newline=newline) process_notebook = {'jupyter': process_jupyter_notebook, 'zeppelin': process_zeppelin_notebook}[args.mode] any_change = False @@ -614,8 +616,7 @@ def main(): continue try: - file_newline = '' if args.force_lf_eol else None - with io.open(filename, 'r+', encoding='utf8', newline=file_newline) as f: + with io.open(filename, 'r+', encoding='utf8', newline=newline) as f: out = output_stream if args.textconv or args.dry_run else f if process_notebook( input_stream=f, output_stream=out, args=args, extra_keys=extra_keys, filename=filename diff --git a/tests/test_end_to_end.py b/tests/test_end_to_end.py index d3f45ec..fada0f4 100644 --- a/tests/test_end_to_end.py +++ b/tests/test_end_to_end.py @@ -208,14 +208,26 @@ def test_nochange_notebook_unchanged(): def test_newline_behavior(tmp_path: Path): input_content = (NOTEBOOKS_FOLDER / 'test_drop_empty_cells.ipynb').read_bytes().replace(b'\n', b'\r\n') - p = tmp_path / 'input.ipynb' - p.write_bytes(input_content) + to_os_eol = tmp_path / 'should-have-os-eol.ipynb' + to_os_eol.write_bytes(input_content) - run([nbstripout_exe(), p]) + run([nbstripout_exe(), '--preserve-newlines', to_os_eol]) if sys.platform == 'win32': - assert b'\r\n' in p.read_bytes() + assert b'\r\n' in to_os_eol.read_bytes() else: - assert b'\r\n' not in p.read_bytes() + assert b'\r\n' not in to_os_eol.read_bytes() - run([nbstripout_exe(), '--force-lf-eol', p]) - assert b'\r\n' not in p.read_bytes() + pc = run([nbstripout_exe(), '--preserve-newlines', '--textconv', to_os_eol], stdout=PIPE) + if sys.platform == 'win32': + assert b'\r\n' in pc.stdout + else: + assert b'\r\n' not in pc.stdout + + to_lf_eol = tmp_path / 'should-have-lf-eol.ipynb' + to_lf_eol.write_bytes(input_content) + + run([nbstripout_exe(), to_lf_eol]) + assert b'\r\n' not in to_lf_eol.read_bytes() + + pc = run([nbstripout_exe(), '--textconv', to_lf_eol], stdout=PIPE) + assert b'\r\n' not in pc.stdout From 4f28ef9236b1f6716ac1d50fff7e4512ee458e23 Mon Sep 17 00:00:00 2001 From: Sebastian Weigand Date: Sun, 8 Feb 2026 16:57:00 +0100 Subject: [PATCH 4/8] =?UTF-8?q?=F0=9F=A9=B9=20Fix=20inverted=20logic?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- nbstripout/_nbstripout.py | 2 +- tests/test_end_to_end.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/nbstripout/_nbstripout.py b/nbstripout/_nbstripout.py index 78e330c..e384318 100644 --- a/nbstripout/_nbstripout.py +++ b/nbstripout/_nbstripout.py @@ -602,7 +602,7 @@ def main(): keep_metadata_keys.extend(args.keep_metadata_keys.split()) extra_keys = [i for i in extra_keys if i not in keep_metadata_keys] - newline = None if args.preserve_newlines else '' + newline = '' if args.preserve_newlines else None # Wrap input/output stream in UTF-8 encoded text wrapper # https://stackoverflow.com/a/16549381 diff --git a/tests/test_end_to_end.py b/tests/test_end_to_end.py index fada0f4..53ffdba 100644 --- a/tests/test_end_to_end.py +++ b/tests/test_end_to_end.py @@ -211,13 +211,13 @@ def test_newline_behavior(tmp_path: Path): to_os_eol = tmp_path / 'should-have-os-eol.ipynb' to_os_eol.write_bytes(input_content) - run([nbstripout_exe(), '--preserve-newlines', to_os_eol]) + run([nbstripout_exe(), to_os_eol]) if sys.platform == 'win32': assert b'\r\n' in to_os_eol.read_bytes() else: assert b'\r\n' not in to_os_eol.read_bytes() - pc = run([nbstripout_exe(), '--preserve-newlines', '--textconv', to_os_eol], stdout=PIPE) + pc = run([nbstripout_exe(), '--textconv', to_os_eol], stdout=PIPE) if sys.platform == 'win32': assert b'\r\n' in pc.stdout else: @@ -226,8 +226,8 @@ def test_newline_behavior(tmp_path: Path): to_lf_eol = tmp_path / 'should-have-lf-eol.ipynb' to_lf_eol.write_bytes(input_content) - run([nbstripout_exe(), to_lf_eol]) + run([nbstripout_exe(), '--preserve-newlines', to_lf_eol]) assert b'\r\n' not in to_lf_eol.read_bytes() - pc = run([nbstripout_exe(), '--textconv', to_lf_eol], stdout=PIPE) + pc = run([nbstripout_exe(), '--preserve-newlines', '--textconv', to_lf_eol], stdout=PIPE) assert b'\r\n' not in pc.stdout From 75cf0d6262128c15c93735afee5f2a74282ea1e2 Mon Sep 17 00:00:00 2001 From: Sebastian Weigand Date: Sun, 8 Feb 2026 18:40:09 +0100 Subject: [PATCH 5/8] Apply suggestion from @kynan Co-authored-by: Florian Rathgeber --- nbstripout/_nbstripout.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nbstripout/_nbstripout.py b/nbstripout/_nbstripout.py index e384318..8f2e439 100644 --- a/nbstripout/_nbstripout.py +++ b/nbstripout/_nbstripout.py @@ -531,7 +531,7 @@ def main(): parser.add_argument('--textconv', '-t', action='store_true', help='Prints stripped files to STDOUT') - parser.add_argument('--preserve-newlines', action='store_true', help='Preserve OS line endings when writing files') + parser.add_argument('--unix-newlines', action='store_true', help='Force UNIX line endings in output (if unset, normalize to os.linesep)') parser.add_argument('files', nargs='*', help='Files to strip output from') args = parser.parse_args() From 8bf36ac1a03ebbc3470cbac8fa6bc9792a97e7fd Mon Sep 17 00:00:00 2001 From: Sebastian Weigand Date: Sun, 8 Feb 2026 18:44:15 +0100 Subject: [PATCH 6/8] =?UTF-8?q?=F0=9F=A9=B9=20Rename=20flag=20in=20other?= =?UTF-8?q?=20occurances?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- nbstripout/_nbstripout.py | 8 ++++++-- tests/test_end_to_end.py | 4 ++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/nbstripout/_nbstripout.py b/nbstripout/_nbstripout.py index 8f2e439..a0f4eb9 100644 --- a/nbstripout/_nbstripout.py +++ b/nbstripout/_nbstripout.py @@ -531,7 +531,11 @@ def main(): parser.add_argument('--textconv', '-t', action='store_true', help='Prints stripped files to STDOUT') - parser.add_argument('--unix-newlines', action='store_true', help='Force UNIX line endings in output (if unset, normalize to os.linesep)') + parser.add_argument( + '--unix-newlines', + action='store_true', + help='Force UNIX line endings in output (if unset, normalize to os.linesep)', + ) parser.add_argument('files', nargs='*', help='Files to strip output from') args = parser.parse_args() @@ -602,7 +606,7 @@ def main(): keep_metadata_keys.extend(args.keep_metadata_keys.split()) extra_keys = [i for i in extra_keys if i not in keep_metadata_keys] - newline = '' if args.preserve_newlines else None + newline = '' if args.unix_newlines else None # Wrap input/output stream in UTF-8 encoded text wrapper # https://stackoverflow.com/a/16549381 diff --git a/tests/test_end_to_end.py b/tests/test_end_to_end.py index 53ffdba..321e64c 100644 --- a/tests/test_end_to_end.py +++ b/tests/test_end_to_end.py @@ -226,8 +226,8 @@ def test_newline_behavior(tmp_path: Path): to_lf_eol = tmp_path / 'should-have-lf-eol.ipynb' to_lf_eol.write_bytes(input_content) - run([nbstripout_exe(), '--preserve-newlines', to_lf_eol]) + run([nbstripout_exe(), '--unix-newlines', to_lf_eol]) assert b'\r\n' not in to_lf_eol.read_bytes() - pc = run([nbstripout_exe(), '--preserve-newlines', '--textconv', to_lf_eol], stdout=PIPE) + pc = run([nbstripout_exe(), '--unix-newlines', '--textconv', to_lf_eol], stdout=PIPE) assert b'\r\n' not in pc.stdout From 4fa2256a1b75116fc3543f1a6165cdcb16b1e8e1 Mon Sep 17 00:00:00 2001 From: Sebastian Weigand Date: Sun, 8 Feb 2026 18:58:36 +0100 Subject: [PATCH 7/8] =?UTF-8?q?=F0=9F=93=9A=20Add=20"Forcing=20UNIX=20newl?= =?UTF-8?q?ines"=20section=20to=20readme?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.md b/README.md index 1f4438d..b9a1ff5 100644 --- a/README.md +++ b/README.md @@ -310,6 +310,13 @@ To drop all cells tagged with "solution" run: The option accepts a list of tags separated by whitespace. +### Forcing UNIX newlines + +Force UNIX (LF) newlines in the output (useful on Windows to keep consistent +line endings in filtered output or textconv diffs): + + nbstripout --unix-newlines FILE.ipynb + ### Keeping some output Do not strip the execution count/prompt number: From 3d9b624ddb4c128c8e56d0c357135611856092ad Mon Sep 17 00:00:00 2001 From: Sebastian Weigand Date: Tue, 10 Feb 2026 10:00:43 +0100 Subject: [PATCH 8/8] Apply suggestion from @kynan Co-authored-by: Florian Rathgeber --- nbstripout/_nbstripout.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/nbstripout/_nbstripout.py b/nbstripout/_nbstripout.py index a0f4eb9..a241c3d 100644 --- a/nbstripout/_nbstripout.py +++ b/nbstripout/_nbstripout.py @@ -606,6 +606,8 @@ def main(): keep_metadata_keys.extend(args.keep_metadata_keys.split()) extra_keys = [i for i in extra_keys if i not in keep_metadata_keys] + # Note that we can't actually preserve newlines from the input file: nbformat implicitly converts all newlines to \n + # and setting newline='' disables normalization of newlines on output, so the output will always use \n as newlines. newline = '' if args.unix_newlines else None # Wrap input/output stream in UTF-8 encoded text wrapper