From 96da09b5029a0fe5a4c95a95c74fbab2ba6caf41 Mon Sep 17 00:00:00 2001 From: BichengWang Date: Fri, 16 Oct 2026 10:28:58 -0700 Subject: [PATCH 1/2] fix(nbfmt): preserve Unicode separators in notebook JSON --- tools/nbfmt/formatter.py | 4 +- tools/test_nbfmt.py | 82 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+), 2 deletions(-) create mode 100644 tools/test_nbfmt.py diff --git a/tools/nbfmt/formatter.py b/tools/nbfmt/formatter.py index f3e576348..f305a968b 100644 --- a/tools/nbfmt/formatter.py +++ b/tools/nbfmt/formatter.py @@ -178,8 +178,8 @@ def format_bytes(self, data: Dict[str, Any], file_path: pathlib.Path) -> bytes: ensure_ascii=False ) - # Clean trailing whitespace on each line - lines = [line.rstrip() for line in formatted_json.splitlines()] + # Split only on JSON formatting newlines, preserving Unicode separators in strings. + lines = [line.rstrip() for line in formatted_json.split("\n")] clean_json_str = "\n".join(lines) + "\n" # Match standard HTML entity escape conventions diff --git a/tools/test_nbfmt.py b/tools/test_nbfmt.py new file mode 100644 index 000000000..fd1afb2c2 --- /dev/null +++ b/tools/test_nbfmt.py @@ -0,0 +1,82 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Regression tests for the notebook formatter.""" + +import json +import pathlib +import tempfile +import unittest + +from tools.nbfmt.formatter import NotebookFormatter + + +class TestNotebookFormatter(unittest.TestCase): + """Checks that formatting preserves notebook content and valid JSON.""" + + def test_unicode_separators_round_trip(self): + for separator in ("\u0085", "\u2028", "\u2029"): + for source_is_string in (False, True): + with self.subTest(separator=repr(separator), source_is_string=source_is_string): + text = f"before{separator}after" + data = { + "cells": [{ + "cell_type": "markdown", + "metadata": {}, + "source": text if source_is_string else [text], + }], + "metadata": {}, + "nbformat": 4, + "nbformat_minor": 0, + } + formatter = NotebookFormatter() + path = pathlib.Path("example.ipynb") + + formatted = formatter.format_bytes(data, path) + parsed = json.loads(formatted) + + self.assertEqual("".join(parsed["cells"][0]["source"]), text) + self.assertEqual(formatter.format_bytes(parsed, path), formatted) + + def test_unicode_separators_in_outputs_and_metadata(self): + with tempfile.TemporaryDirectory() as temp_dir: + path = pathlib.Path(temp_dir) / "example.ipynb" + text = "before\u0085middle\u2028more\u2029after" + outputs = [{"output_type": "stream", "name": "stdout", "text": [text]}] + data = { + "cells": [{ + "cell_type": "code", + "metadata": {}, + "source": ["print('example')"], + "execution_count": 1, + "outputs": outputs, + }], + "metadata": {"language_info": {"name": "python", "description": text}}, + "nbformat": 4, + "nbformat_minor": 0, + } + path.write_text(json.dumps(data), encoding="utf-8") + formatter = NotebookFormatter() + + result = formatter.format_file(path) + self.assertTrue(result.is_formatted) + parsed = json.loads(path.read_bytes()) + + self.assertEqual(parsed["cells"][0]["outputs"], outputs) + self.assertEqual(parsed["metadata"]["language_info"]["description"], text) + self.assertTrue(formatter.format_file(path, test_only=True).is_formatted) + + +if __name__ == "__main__": + unittest.main() From 4294d50c71cb89426caa708e6cb4366c95a4afaf Mon Sep 17 00:00:00 2001 From: BichengWang Date: Tue, 20 Oct 2026 20:52:33 -0700 Subject: [PATCH 2/2] test(nbfmt): run formatter regressions in notebook CI --- .github/workflows/notebooks.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/notebooks.yaml b/.github/workflows/notebooks.yaml index 177db1a35..d0e2746b9 100644 --- a/.github/workflows/notebooks.yaml +++ b/.github/workflows/notebooks.yaml @@ -7,6 +7,8 @@ on: pull_request: paths: - "**.ipynb" + - "tools/**.py" + - ".github/workflows/notebooks.yaml" # Allow manual runs workflow_dispatch: @@ -26,6 +28,8 @@ jobs: - uses: actions/setup-python@7f4fc3e22c37d6ff65e88745f38bd3157c663f7c # v4 - name: Fetch main branch run: git fetch -u origin main:main + - name: Run formatter unit tests + run: python3 -m unittest tools.test_nbfmt - name: Check notebook formatting run: | if [ "${{ github.event_name }}" == "pull_request" ]; then