From 4d6f3ddd2a0e7acebabfcd89a66af6da56cf5d94 Mon Sep 17 00:00:00 2001 From: hguturu Date: Tue, 18 Mar 2025 10:07:18 -0700 Subject: [PATCH 01/14] feature: Add support for qmd, need to fix handling of leading #| and tests --- nbqa/__main__.py | 6 ++++-- nbqa/path_utils.py | 12 +++++++++--- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/nbqa/__main__.py b/nbqa/__main__.py index 087eb1d3..06b3006c 100644 --- a/nbqa/__main__.py +++ b/nbqa/__main__.py @@ -126,7 +126,7 @@ def _get_notebooks(root_dir: str) -> Iterator[Path]: jupytext_installed = True if os.path.isfile(root_dir): _, ext = os.path.splitext(root_dir) - if (jupytext_installed and ext in (".ipynb", ".md")) or ( + if (jupytext_installed and ext in (".ipynb", ".md", ".qmd")) or ( not jupytext_installed and ext == ".ipynb" ): return iter((Path(root_dir),)) @@ -138,7 +138,9 @@ def _get_notebooks(root_dir: str) -> Iterator[Path]: if jupytext_installed: iterable = itertools.chain( - Path(root_dir).rglob("*.ipynb"), Path(root_dir).rglob("*.md") + Path(root_dir).rglob("*.ipynb"), + Path(root_dir).rglob("*.md"), + Path(root_dir).rglob("*.qmd"), ) else: # pragma: nocover iterable = itertools.chain(Path(root_dir).rglob("*.ipynb")) diff --git a/nbqa/path_utils.py b/nbqa/path_utils.py index 3a5b37d8..de58f1bf 100644 --- a/nbqa/path_utils.py +++ b/nbqa/path_utils.py @@ -88,7 +88,7 @@ def read_notebook(notebook: str) -> Tuple[Optional[Dict[str, Any]], Optional[boo if ext == ".ipynb": trailing_newline = content.endswith("\n") return json.loads(content), trailing_newline - assert ext == ".md" + assert ext in (".md", ".qmd") try: import jupytext # pylint: disable=import-outside-toplevel from markdown_it import MarkdownIt # pylint: disable=import-outside-toplevel @@ -133,8 +133,14 @@ def read_notebook(notebook: str) -> Tuple[Optional[Dict[str, Any]], Optional[boo parsed = MarkdownIt("commonmark").disable("inline", True).parse(content) lexer = None for token in parsed: - if token.type == "fence" and token.info.startswith("{code-cell}"): - lexer = remove_prefix(token.info, "{code-cell}").strip() + if token.type == "fence" and ( + token.info.startswith("{code-cell}") or token.info.startswith("{python}") + ): + lexer = ( + remove_prefix(token.info, "{code-cell}").strip() + if not token.info.startswith("{python}") + else "python" + ) md_content["metadata"]["language_info"] = {"pygments_lexer": lexer} break From 4e59da246abbb8cea6810891d0b6f7bbba94fbd9 Mon Sep 17 00:00:00 2001 From: hguturu Date: Tue, 18 Mar 2025 14:38:05 -0700 Subject: [PATCH 02/14] feature: add _restore_quarto_cell_options to support preserving cell options --- nbqa/replace_source.py | 36 ++++++++++++++++++++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/nbqa/replace_source.py b/nbqa/replace_source.py index 94588f63..cf13c30e 100644 --- a/nbqa/replace_source.py +++ b/nbqa/replace_source.py @@ -28,6 +28,37 @@ SEPARATOR = {True: MARKDOWN_SEPARATOR, False: CODE_SEPARATOR} +def _restore_quarto_cell_options(source: str) -> str: + """ + Restore the cell option comments #| at the start of the cell. + + This comment like is typically changed to '# |' by third party tools. + + This is for the cell options in quarto format as described at + https://quarto.org/docs/reference/cells/cells-jupyter.html. + + Parameters + ---------- + source + Portion of Python file between cell separators. + + Returns + ------- + str + New source with leading '# |' restored to '#!'. + """ + new_lines = [] + lines = source.splitlines() + i = 0 + for i, line in enumerate(lines): + if not line.startswith("# |"): + break # only leading '#!' are cell options + new_lines.append("#|" + line[3:]) + if i < len(lines): + new_lines.extend(lines[i:]) + return "\n".join(new_lines) + + def _restore_semicolon( source: str, cell_number: int, @@ -123,6 +154,7 @@ def _get_new_source( source = _restore_semicolon( pycell, code_cell_number, notebook_info.trailing_semicolons ) + source = _restore_quarto_cell_options(source) return _reinstate_magics( source, notebook_info.temporary_lines.get(code_cell_number, []), @@ -216,7 +248,7 @@ def _write_notebook( f"{json.dumps(notebook_json, indent=1, ensure_ascii=False)}" ) else: - assert ext == ".md" + assert ext in (".md", ".qmd") import jupytext # pylint: disable=import-outside-toplevel from jupytext.config import ( # pylint: disable=import-outside-toplevel load_jupytext_config, @@ -326,7 +358,7 @@ def _print_diff(code_cell_number: int, cell_diff: Iterator[str]) -> bool: if line_changes: header = f"Cell {code_cell_number}" - headers = [f"{BOLD}{header}{RESET}\n", f"{'-'*len(header)}\n"] + headers = [f"{BOLD}{header}{RESET}\n", f"{'-' * len(header)}\n"] sys.stdout.writelines(headers + line_changes + ["\n"]) return True return False From 7de848381204f28bf3e37c370a5d1d97948925a4 Mon Sep 17 00:00:00 2001 From: hguturu Date: Tue, 18 Mar 2025 15:02:42 -0700 Subject: [PATCH 03/14] fixed typo in docstring --- nbqa/replace_source.py | 2 +- tests/test_jupytext.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/nbqa/replace_source.py b/nbqa/replace_source.py index cf13c30e..38a825a7 100644 --- a/nbqa/replace_source.py +++ b/nbqa/replace_source.py @@ -45,7 +45,7 @@ def _restore_quarto_cell_options(source: str) -> str: Returns ------- str - New source with leading '# |' restored to '#!'. + New source with leading '# |' restored to '#|'. """ new_lines = [] lines = source.splitlines() diff --git a/tests/test_jupytext.py b/tests/test_jupytext.py index 7702ea85..8b9afdda 100644 --- a/tests/test_jupytext.py +++ b/tests/test_jupytext.py @@ -296,11 +296,11 @@ def test_jupytext_on_folder(capsys: "CaptureFixture") -> None: ) out, _ = capsys.readouterr() expected = ( - f'{os.path.join(path, "invalid_syntax.ipynb")}:cell_1:0 at module level:\n' + f"{os.path.join(path, 'invalid_syntax.ipynb')}:cell_1:0 at module level:\n" " D100: Missing docstring in public module\n" - f'{os.path.join(path, "assignment_to_literal.ipynb")}:cell_1:0 at module level:\n' + f"{os.path.join(path, 'assignment_to_literal.ipynb')}:cell_1:0 at module level:\n" " D100: Missing docstring in public module\n" - f'{os.path.join(path, "automagic.ipynb")}:cell_1:0 at module level:\n' + f"{os.path.join(path, 'automagic.ipynb')}:cell_1:0 at module level:\n" " D100: Missing docstring in public module\n" ) assert "\n".join(sorted(out.splitlines())) == "\n".join( From 4728e771aa81c029c41e248cd9d34588a678b729 Mon Sep 17 00:00:00 2001 From: hguturu Date: Wed, 19 Mar 2025 11:43:03 -0700 Subject: [PATCH 04/14] tests: Add test_qmd for new jupytext qmd feature --- tests/data/notebook_for_testing.qmd | 33 +++++++++++++++ tests/test_jupytext.py | 63 +++++++++++++++++++++++++++++ 2 files changed, 96 insertions(+) create mode 100644 tests/data/notebook_for_testing.qmd diff --git a/tests/data/notebook_for_testing.qmd b/tests/data/notebook_for_testing.qmd new file mode 100644 index 00000000..f8cfc842 --- /dev/null +++ b/tests/data/notebook_for_testing.qmd @@ -0,0 +1,33 @@ +--- +title: "Quarto Basics" +format: + html: + code-fold: true +jupyter: python3 +--- + +```{python} +#| label: fig-polar +#| fig-cap: A line plot on a polar axis +# This is a comment +# | Looks like a cell option, but treat like +# comment since not at top + +import numpy as np +import matplotlib.pyplot as plt + +r = np.arange(0,2,0.01) +theta = 2*np.pi*r +fig, ax = plt.subplots(subplot_kw={'projection': 'polar'}) +ax.plot(theta, r) +ax.set_rticks([0.5,1,1.5,2]) +ax.grid(True) +plt.show() +``` + +# Other Markdown +This content should not change in any way. +#| cell option like line +#comment like line +code that won't get formatted +ax.set_rticks([0.5,1,1.5,2]) diff --git a/tests/test_jupytext.py b/tests/test_jupytext.py index 8b9afdda..75202be9 100644 --- a/tests/test_jupytext.py +++ b/tests/test_jupytext.py @@ -198,6 +198,69 @@ def test_md(tmp_test_data: Path) -> None: assert result == expected +def test_qmd(tmp_test_data: Path) -> None: + """ + Notebook in qmd format. + + Parameters + ---------- + tmp_test_data + Temporary copy of test data. + """ + notebook = tmp_test_data / "notebook_for_testing.qmd" + + main(["black", str(notebook)]) + + with open(notebook, encoding="utf-8") as fd: + result = fd.read() + expected = ( + """---\n""" + """title: Quarto Basics\n""" + """format:\n""" + """ html:\n""" + """ code-fold: true\n""" + """jupyter:\n""" + """ jupytext:\n""" + """ text_representation:\n""" + """ extension: .qmd\n""" + """ format_name: quarto\n""" + """ format_version: '1.0'\n""" + """ jupytext_version: 1.16.7\n""" + """ kernelspec:\n""" + """ display_name: Python 3\n""" + """ language: python\n""" + """ name: python3\n""" + """---\n""" + """\n""" + """```{python}\n""" + """#| label: fig-polar\n""" + """#| fig-cap: A line plot on a polar axis\n""" + """# This is a comment\n""" + """# | Looks like a cell option, but treat like\n""" + """# comment since not at top\n""" + """\n""" + """import numpy as np\n""" + """import matplotlib.pyplot as plt\n""" + """\n""" + """r = np.arange(0, 2, 0.01)\n""" + """theta = 2 * np.pi * r\n""" + """fig, ax = plt.subplots(subplot_kw={"projection": "polar"})\n""" + """ax.plot(theta, r)\n""" + """ax.set_rticks([0.5, 1, 1.5, 2])\n""" + """ax.grid(True)\n""" + """plt.show()\n""" + """```\n""" + """\n""" + """# Other Markdown\n""" + """This content should not change in any way. \n""" + """#| cell option like line\n""" + """#comment like line \n""" + """code that won't get formatted\n""" + """ax.set_rticks([0.5,1,1.5,2]) \n""" + ) + assert result == expected + + def test_non_jupytext_md() -> None: """Check non-Python markdown will be ignored.""" ret = main(["black", "README.md"]) From 55fd296a4ada49aa3377395a13812266540fe3cc Mon Sep 17 00:00:00 2001 From: hguturu Date: Wed, 19 Mar 2025 11:44:21 -0700 Subject: [PATCH 05/14] tests: fixing test_qmd to work with white space stripping of precommit --- tests/test_jupytext.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_jupytext.py b/tests/test_jupytext.py index 75202be9..cf9a9217 100644 --- a/tests/test_jupytext.py +++ b/tests/test_jupytext.py @@ -252,11 +252,11 @@ def test_qmd(tmp_test_data: Path) -> None: """```\n""" """\n""" """# Other Markdown\n""" - """This content should not change in any way. \n""" + """This content should not change in any way.\n""" """#| cell option like line\n""" - """#comment like line \n""" + """#comment like line\n""" """code that won't get formatted\n""" - """ax.set_rticks([0.5,1,1.5,2]) \n""" + """ax.set_rticks([0.5,1,1.5,2])\n""" ) assert result == expected From ed08156f646ef6527c9a2183da32f899aea09a95 Mon Sep 17 00:00:00 2001 From: hguturu Date: Wed, 19 Mar 2025 12:14:18 -0700 Subject: [PATCH 06/14] docs: Added reference to qmd in README.md --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index 53e6088b..f61e111a 100644 --- a/README.md +++ b/README.md @@ -126,6 +126,12 @@ reformatted my_notebook.md All done! ✨ 🍰 ✨ 1 files reformatted. ``` +Quarto format markdown ``.qmd`` files are also supported via [quarto-cli](https://github.com/quarto-dev/quarto-cli) (requires `jupytext` and `quarto-cli` to be installed): + +```console +$ nbqa "ruff check --select=I --fix" my_notebook.qmd +Found 1 error (1 fixed, 0 remaining). +``` See [command-line examples](https://nbqa.readthedocs.io/en/latest/examples.html) for examples involving [doctest](https://docs.python.org/3/library/doctest.html), [flake8](https://flake8.pycqa.org/en/latest/), [mypy](http://mypy-lang.org/), [pylint](https://github.com/PyCQA/pylint), [autopep8](https://github.com/hhatto/autopep8), [pydocstyle](http://www.pydocstyle.org/en/stable/), [yapf](https://github.com/google/yapf), and [ruff](https://github.com/charliermarsh/ruff/). From 37a39905fc75d6cd25b67ac1f7e472f561374559 Mon Sep 17 00:00:00 2001 From: hguturu Date: Wed, 19 Mar 2025 12:26:32 -0700 Subject: [PATCH 07/14] build: added quarto-cli to requirements-dev and toolchain extra --- requirements-dev.txt | 1 + setup.cfg | 1 + 2 files changed, 2 insertions(+) diff --git a/requirements-dev.txt b/requirements-dev.txt index 45632468..96b1aafb 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -16,5 +16,6 @@ pytest pytest-cov pytest-randomly pyupgrade +quarto-cli ruff yapf diff --git a/setup.cfg b/setup.cfg index f7be9f59..02bea3c3 100644 --- a/setup.cfg +++ b/setup.cfg @@ -53,6 +53,7 @@ toolchain = mypy pylint pyupgrade + quarto-cli ruff [flake8] From 5e9cb65eea0c1b637a2a598e71cdc8c225d14106 Mon Sep 17 00:00:00 2001 From: hguturu Date: Mon, 24 Mar 2025 11:36:35 -0700 Subject: [PATCH 08/14] fix: change qmd lines back without breaking other tools --- nbqa/replace_source.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/nbqa/replace_source.py b/nbqa/replace_source.py index 38a825a7..87bed9c7 100644 --- a/nbqa/replace_source.py +++ b/nbqa/replace_source.py @@ -28,7 +28,7 @@ SEPARATOR = {True: MARKDOWN_SEPARATOR, False: CODE_SEPARATOR} -def _restore_quarto_cell_options(source: str) -> str: +def _restore_quarto_cell_options(lines: list[str]) -> list[str]: """ Restore the cell option comments #| at the start of the cell. @@ -39,16 +39,15 @@ def _restore_quarto_cell_options(source: str) -> str: Parameters ---------- - source - Portion of Python file between cell separators. + lines + Lines after all pre-processing restored. Returns ------- - str - New source with leading '# |' restored to '#|'. + list[str] + Lines with leading '# |' restored to '#|'. """ new_lines = [] - lines = source.splitlines() i = 0 for i, line in enumerate(lines): if not line.startswith("# |"): @@ -56,7 +55,7 @@ def _restore_quarto_cell_options(source: str) -> str: new_lines.append("#|" + line[3:]) if i < len(lines): new_lines.extend(lines[i:]) - return "\n".join(new_lines) + return new_lines def _restore_semicolon( @@ -154,7 +153,6 @@ def _get_new_source( source = _restore_semicolon( pycell, code_cell_number, notebook_info.trailing_semicolons ) - source = _restore_quarto_cell_options(source) return _reinstate_magics( source, notebook_info.temporary_lines.get(code_cell_number, []), @@ -257,6 +255,8 @@ def _write_notebook( config = load_jupytext_config(os.path.abspath(temp_notebook)) for cell in notebook_json["cells"]: + if ext == ".qmd": + cell["source"] = _restore_quarto_cell_options(cell["source"]) cell["source"] = "".join(cell["source"]) jupytext.jupytext.write(notebook_json, temp_notebook, config=config) From fb9b38339d564b342a62e86bb8afabf2f63485ff Mon Sep 17 00:00:00 2001 From: hguturu Date: Mon, 24 Mar 2025 12:44:20 -0700 Subject: [PATCH 09/14] fix: _restore_quarto_cell_options so that existing non-cell options don't get turned into cell options --- nbqa/replace_source.py | 34 +++++++++++++++++++---------- tests/data/notebook_for_testing.qmd | 1 + tests/test_jupytext.py | 1 + 3 files changed, 25 insertions(+), 11 deletions(-) diff --git a/nbqa/replace_source.py b/nbqa/replace_source.py index 87bed9c7..3a0448a3 100644 --- a/nbqa/replace_source.py +++ b/nbqa/replace_source.py @@ -28,7 +28,7 @@ SEPARATOR = {True: MARKDOWN_SEPARATOR, False: CODE_SEPARATOR} -def _restore_quarto_cell_options(lines: list[str]) -> list[str]: +def _restore_quarto_cell_options(og_lines: list[str], lines: list[str]) -> list[str]: """ Restore the cell option comments #| at the start of the cell. @@ -39,23 +39,33 @@ def _restore_quarto_cell_options(lines: list[str]) -> list[str]: Parameters ---------- + og_lines + Original input lines. lines - Lines after all pre-processing restored. + Lines after all formatting and magics restored. Returns ------- list[str] Lines with leading '# |' restored to '#|'. """ - new_lines = [] + restored_lines = [] i = 0 + # iteration logic should be safe since all cell options are at the start + # and single lines so shouldn't suffer multi-line formatting before + # breaking out for i, line in enumerate(lines): - if not line.startswith("# |"): - break # only leading '#!' are cell options - new_lines.append("#|" + line[3:]) - if i < len(lines): - new_lines.extend(lines[i:]) - return new_lines + if not line.startswith("# |") or not og_lines[i].startswith("#|"): + # only leading '#|' are cell options + # if we encounter a line without, then all following are not + # we also skip if the formatting tool hasn't changed '#|' to '# |' + # since that means the formatter is handling qmd lines properly + i -= 1 # rolling back so the logic below works without branching + break + restored_lines.append("#|" + line[3:]) + + restored_lines.extend(lines[i + 1 :]) + return restored_lines def _restore_semicolon( @@ -255,8 +265,6 @@ def _write_notebook( config = load_jupytext_config(os.path.abspath(temp_notebook)) for cell in notebook_json["cells"]: - if ext == ".qmd": - cell["source"] = _restore_quarto_cell_options(cell["source"]) cell["source"] = "".join(cell["source"]) jupytext.jupytext.write(notebook_json, temp_notebook, config=config) @@ -310,6 +318,8 @@ def mutate( # pylint: disable=too-many-locals,too-many-arguments ) if not new_source: cells_to_remove.append(cell_number) + if notebook.endswith(".qmd"): + new_source = _restore_quarto_cell_options(new_source) cell["source"] = new_source if original_notebook_json == notebook_json: @@ -409,6 +419,8 @@ def diff( # pylint: disable=too-many-arguments newlinesbefore[python_file], newlinesafter[python_file], ) + if notebook.endswith(".qmd"): + new_source = _restore_quarto_cell_options(cell["source"], new_source) cell["source"][-1] += "\n" if new_source: new_source[-1] += "\n" diff --git a/tests/data/notebook_for_testing.qmd b/tests/data/notebook_for_testing.qmd index f8cfc842..0f3c80c3 100644 --- a/tests/data/notebook_for_testing.qmd +++ b/tests/data/notebook_for_testing.qmd @@ -9,6 +9,7 @@ jupyter: python3 ```{python} #| label: fig-polar #| fig-cap: A line plot on a polar axis +# | Should not be changed back to #| # This is a comment # | Looks like a cell option, but treat like # comment since not at top diff --git a/tests/test_jupytext.py b/tests/test_jupytext.py index cf9a9217..b0121a5c 100644 --- a/tests/test_jupytext.py +++ b/tests/test_jupytext.py @@ -235,6 +235,7 @@ def test_qmd(tmp_test_data: Path) -> None: """```{python}\n""" """#| label: fig-polar\n""" """#| fig-cap: A line plot on a polar axis\n""" + """# | Should not be changed back to #|\n""" """# This is a comment\n""" """# | Looks like a cell option, but treat like\n""" """# comment since not at top\n""" From 5f5836a4749dc6cbfe289ec9ad39b57369e849c6 Mon Sep 17 00:00:00 2001 From: hguturu Date: Mon, 24 Mar 2025 12:45:38 -0700 Subject: [PATCH 10/14] fix: _restore_quarto_cell_options so that existing non-cell options don't get turned into cell options --- nbqa/replace_source.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nbqa/replace_source.py b/nbqa/replace_source.py index 3a0448a3..096c15a2 100644 --- a/nbqa/replace_source.py +++ b/nbqa/replace_source.py @@ -319,7 +319,7 @@ def mutate( # pylint: disable=too-many-locals,too-many-arguments if not new_source: cells_to_remove.append(cell_number) if notebook.endswith(".qmd"): - new_source = _restore_quarto_cell_options(new_source) + new_source = _restore_quarto_cell_options(cell["source"], new_source) cell["source"] = new_source if original_notebook_json == notebook_json: From 4bbd7e7e2d0e2b47c3fd94bdb604922e4771053d Mon Sep 17 00:00:00 2001 From: hguturu Date: Mon, 24 Mar 2025 12:57:15 -0700 Subject: [PATCH 11/14] fix: logic in _restore_quarto_cell_options --- nbqa/replace_source.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/nbqa/replace_source.py b/nbqa/replace_source.py index 096c15a2..34818abb 100644 --- a/nbqa/replace_source.py +++ b/nbqa/replace_source.py @@ -60,10 +60,9 @@ def _restore_quarto_cell_options(og_lines: list[str], lines: list[str]) -> list[ # if we encounter a line without, then all following are not # we also skip if the formatting tool hasn't changed '#|' to '# |' # since that means the formatter is handling qmd lines properly - i -= 1 # rolling back so the logic below works without branching + restored_lines.append(line) break restored_lines.append("#|" + line[3:]) - restored_lines.extend(lines[i + 1 :]) return restored_lines From 93f3d2a00ade6c2fffa759449d98bc09bd20b3e3 Mon Sep 17 00:00:00 2001 From: hguturu Date: Mon, 24 Mar 2025 13:52:19 -0700 Subject: [PATCH 12/14] refactor: changed how _restore_quarto_cell_options is applied to not break other formats --- tests/data/notebook_for_testing.qmd | 16 +++++++++++++--- tests/test_jupytext.py | 2 +- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/tests/data/notebook_for_testing.qmd b/tests/data/notebook_for_testing.qmd index 0f3c80c3..25b02805 100644 --- a/tests/data/notebook_for_testing.qmd +++ b/tests/data/notebook_for_testing.qmd @@ -1,15 +1,25 @@ --- -title: "Quarto Basics" +title: Quarto Basics format: html: code-fold: true -jupyter: python3 +jupyter: + jupytext: + text_representation: + extension: .qmd + format_name: quarto + format_version: '1.0' + jupytext_version: 1.16.7 + kernelspec: + display_name: Python 3 + language: python + name: python3 --- ```{python} #| label: fig-polar #| fig-cap: A line plot on a polar axis -# | Should not be changed back to #| +# Should not be changed back to #| # This is a comment # | Looks like a cell option, but treat like # comment since not at top diff --git a/tests/test_jupytext.py b/tests/test_jupytext.py index b0121a5c..936768f8 100644 --- a/tests/test_jupytext.py +++ b/tests/test_jupytext.py @@ -235,7 +235,7 @@ def test_qmd(tmp_test_data: Path) -> None: """```{python}\n""" """#| label: fig-polar\n""" """#| fig-cap: A line plot on a polar axis\n""" - """# | Should not be changed back to #|\n""" + """# Should not be changed back to #|\n""" """# This is a comment\n""" """# | Looks like a cell option, but treat like\n""" """# comment since not at top\n""" From b0cb95d747f2e45b329d1ccee7e9826d419b9c00 Mon Sep 17 00:00:00 2001 From: hguturu Date: Mon, 24 Mar 2025 16:10:09 -0700 Subject: [PATCH 13/14] refactor: removed special handling of qmd cell options, since # | are supported --- nbqa/replace_source.py | 43 ----------------------------- tests/data/notebook_for_testing.qmd | 20 +++++++------- tests/test_jupytext.py | 11 ++++---- 3 files changed, 16 insertions(+), 58 deletions(-) diff --git a/nbqa/replace_source.py b/nbqa/replace_source.py index 34818abb..639c9a81 100644 --- a/nbqa/replace_source.py +++ b/nbqa/replace_source.py @@ -28,45 +28,6 @@ SEPARATOR = {True: MARKDOWN_SEPARATOR, False: CODE_SEPARATOR} -def _restore_quarto_cell_options(og_lines: list[str], lines: list[str]) -> list[str]: - """ - Restore the cell option comments #| at the start of the cell. - - This comment like is typically changed to '# |' by third party tools. - - This is for the cell options in quarto format as described at - https://quarto.org/docs/reference/cells/cells-jupyter.html. - - Parameters - ---------- - og_lines - Original input lines. - lines - Lines after all formatting and magics restored. - - Returns - ------- - list[str] - Lines with leading '# |' restored to '#|'. - """ - restored_lines = [] - i = 0 - # iteration logic should be safe since all cell options are at the start - # and single lines so shouldn't suffer multi-line formatting before - # breaking out - for i, line in enumerate(lines): - if not line.startswith("# |") or not og_lines[i].startswith("#|"): - # only leading '#|' are cell options - # if we encounter a line without, then all following are not - # we also skip if the formatting tool hasn't changed '#|' to '# |' - # since that means the formatter is handling qmd lines properly - restored_lines.append(line) - break - restored_lines.append("#|" + line[3:]) - restored_lines.extend(lines[i + 1 :]) - return restored_lines - - def _restore_semicolon( source: str, cell_number: int, @@ -317,8 +278,6 @@ def mutate( # pylint: disable=too-many-locals,too-many-arguments ) if not new_source: cells_to_remove.append(cell_number) - if notebook.endswith(".qmd"): - new_source = _restore_quarto_cell_options(cell["source"], new_source) cell["source"] = new_source if original_notebook_json == notebook_json: @@ -418,8 +377,6 @@ def diff( # pylint: disable=too-many-arguments newlinesbefore[python_file], newlinesafter[python_file], ) - if notebook.endswith(".qmd"): - new_source = _restore_quarto_cell_options(cell["source"], new_source) cell["source"][-1] += "\n" if new_source: new_source[-1] += "\n" diff --git a/tests/data/notebook_for_testing.qmd b/tests/data/notebook_for_testing.qmd index 25b02805..d3b5bedf 100644 --- a/tests/data/notebook_for_testing.qmd +++ b/tests/data/notebook_for_testing.qmd @@ -18,20 +18,20 @@ jupyter: ```{python} #| label: fig-polar -#| fig-cap: A line plot on a polar axis -# Should not be changed back to #| -# This is a comment -# | Looks like a cell option, but treat like -# comment since not at top +# | fig-cap: A line plot on a polar axis +#| Additional content +# This is a comment that stops cell options +#| fig-subcap: A line plot on a polar axis +# | Additional content import numpy as np -import matplotlib.pyplot as plt +import matplotlib.pyplot as plt -r = np.arange(0,2,0.01) -theta = 2*np.pi*r -fig, ax = plt.subplots(subplot_kw={'projection': 'polar'}) +r = np.arange(0, 2, 0.01) +theta = 2 * np.pi * r +fig, ax = plt.subplots(subplot_kw={"projection": "polar"}) ax.plot(theta, r) -ax.set_rticks([0.5,1,1.5,2]) +ax.set_rticks([0.5, 1, 1.5, 2]) ax.grid(True) plt.show() ``` diff --git a/tests/test_jupytext.py b/tests/test_jupytext.py index 936768f8..818c8319 100644 --- a/tests/test_jupytext.py +++ b/tests/test_jupytext.py @@ -234,11 +234,12 @@ def test_qmd(tmp_test_data: Path) -> None: """\n""" """```{python}\n""" """#| label: fig-polar\n""" - """#| fig-cap: A line plot on a polar axis\n""" - """# Should not be changed back to #|\n""" - """# This is a comment\n""" - """# | Looks like a cell option, but treat like\n""" - """# comment since not at top\n""" + """#| fig-cap: |-\n""" + """#| A line plot on a polar axis\n""" + """#| Additional content\n""" + """# This is a comment that stops cell options\n""" + """# | fig-subcap: A line plot on a polar axis\n""" + """# | Additional content\n""" """\n""" """import numpy as np\n""" """import matplotlib.pyplot as plt\n""" From 7a8dc82fcab008e30eb635fd307d0cabe5c5cd2e Mon Sep 17 00:00:00 2001 From: hguturu Date: Fri, 11 Apr 2025 15:09:55 -0700 Subject: [PATCH 14/14] fix: test_jupytext.py::test_qmd to be more robust to jupytext version --- tests/test_jupytext.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_jupytext.py b/tests/test_jupytext.py index 818c8319..cdb6591b 100644 --- a/tests/test_jupytext.py +++ b/tests/test_jupytext.py @@ -225,7 +225,7 @@ def test_qmd(tmp_test_data: Path) -> None: """ extension: .qmd\n""" """ format_name: quarto\n""" """ format_version: '1.0'\n""" - """ jupytext_version: 1.16.7\n""" + f" jupytext_version: {jupytext.__version__}\n" """ kernelspec:\n""" """ display_name: Python 3\n""" """ language: python\n"""