From d76a8a37e1491cde8f060c7dfffc2b1f5ec8171c Mon Sep 17 00:00:00 2001 From: Muhammad Yasirroni Date: Tue, 23 Dec 2025 18:50:15 +1100 Subject: [PATCH] initial working ruff per cell --- nbdev/_modidx.py | 2 + nbdev/config.py | 2 + nbdev/doclinks.py | 2 +- nbdev/export.py | 60 ++++++++++++++++++- nbs/api/01_config.ipynb | 2 + nbs/api/04_export.ipynb | 119 +++++++++++++++++++++++++++++++++++++- nbs/api/05_doclinks.ipynb | 4 +- pyproject.toml | 1 + settings.ini | 4 +- tests/export_procs.ipynb | 11 +++- 10 files changed, 198 insertions(+), 9 deletions(-) diff --git a/nbdev/_modidx.py b/nbdev/_modidx.py index 944cb3c09..9b9de40e2 100644 --- a/nbdev/_modidx.py +++ b/nbdev/_modidx.py @@ -86,6 +86,8 @@ 'nbdev.export.black_format': ('api/export.html#black_format', 'nbdev/export.py'), 'nbdev.export.nb_export': ('api/export.html#nb_export', 'nbdev/export.py'), 'nbdev.export.optional_procs': ('api/export.html#optional_procs', 'nbdev/export.py'), + 'nbdev.export.ruff_fix': ('api/export.html#ruff_fix', 'nbdev/export.py'), + 'nbdev.export.ruff_format': ('api/export.html#ruff_format', 'nbdev/export.py'), 'nbdev.export.scrub_magics': ('api/export.html#scrub_magics', 'nbdev/export.py')}, 'nbdev.extract_attachments': {}, 'nbdev.frontmatter': { 'nbdev.frontmatter.FrontmatterProc': ('api/frontmatter.html#frontmatterproc', 'nbdev/frontmatter.py'), diff --git a/nbdev/config.py b/nbdev/config.py index 991557e98..078b6a709 100644 --- a/nbdev/config.py +++ b/nbdev/config.py @@ -54,6 +54,8 @@ def _apply_defaults( language='English', # Language PyPI classifier recursive:bool_arg=True, # Include subfolders in notebook globs? black_formatting:bool_arg=False, # Format libraries with black? + ruff_formatting:bool_arg=False, # Format libraries with ruff format? + ruff_fixing:bool_arg=False, # Fix libraries with ruff check --fix? readme_nb='index.ipynb', # Notebook to export as repo readme title='%(lib_name)s', # Quarto website title allowed_metadata_keys='', # Preserve the list of keys in the main notebook metadata diff --git a/nbdev/doclinks.py b/nbdev/doclinks.py index dc59f67da..0991987f6 100644 --- a/nbdev/doclinks.py +++ b/nbdev/doclinks.py @@ -146,7 +146,7 @@ def nbglob_cli( @delegates(nbglob_cli) def nbdev_export( path:str=None, # Path or filename - procs:Param("tokens naming the export processors to use.", nargs="*", choices=optional_procs())="black_format", + procs:Param("tokens naming the export processors to use.", nargs="*", choices=optional_procs())=["black_format", "ruff_format", "ruff_fix"], **kwargs): "Export notebooks in `path` to Python modules" if os.environ.get('IN_TEST',0): return diff --git a/nbdev/export.py b/nbdev/export.py index c6e338111..b7da3d8fc 100644 --- a/nbdev/export.py +++ b/nbdev/export.py @@ -3,7 +3,7 @@ # AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/api/04_export.ipynb. # %% auto 0 -__all__ = ['ExportModuleProc', 'black_format', 'scrub_magics', 'optional_procs', 'nb_export'] +__all__ = ['ExportModuleProc', 'black_format', 'ruff_format', 'ruff_fix', 'scrub_magics', 'optional_procs', 'nb_export'] # %% ../nbs/api/04_export.ipynb from .config import * @@ -47,6 +47,64 @@ def black_format(cell, # Cell to format try: cell.source = _format_str(cell.source).strip() except: pass +# %% ../nbs/api/04_export.ipynb +def ruff_format(cell, # Cell to format + force=False): # Turn ruff formatting on regardless of settings.ini + "Processor to format code with `ruff format`" + try: cfg = get_config() + except FileNotFoundError: return + if (not cfg.ruff_formatting and not force) or cell.cell_type != 'code': return + + try: import subprocess + except: raise ImportError("subprocess is required") + + try: + subprocess.run(['ruff', '--version'], capture_output=True, check=True) + except (subprocess.CalledProcessError, FileNotFoundError): + raise ImportError("You must install ruff: `pip install ruff` if you wish to use ruff formatting with nbdev") + else: + try: + result = subprocess.run( + ['ruff', 'format', '-'], + input=cell.source, + capture_output=True, + text=True, + check=False + ) + if result.returncode == 0: + cell.source = result.stdout.strip() + except: + pass + +# %% ../nbs/api/04_export.ipynb +def ruff_fix(cell, # Cell to lint and fix + force=False): # Turn ruff fixing on regardless of settings.ini + "Processor to lint and auto-fix code with `ruff check --fix`" + try: cfg = get_config() + except FileNotFoundError: return + if (not cfg.ruff_fixing and not force) or cell.cell_type != 'code': return + + try: import subprocess + except: raise ImportError("subprocess is required") + + try: + subprocess.run(['ruff', '--version'], capture_output=True, check=True) + except (subprocess.CalledProcessError, FileNotFoundError): + raise ImportError("You must install ruff: `pip install ruff` if you wish to use ruff formatting with nbdev") + else: + try: + result = subprocess.run( + ['ruff', 'check', '--fix', '-'], + input=cell.source, + capture_output=True, + text=True, + check=False + ) + if result.returncode == 0 or result.returncode == 1: # 1 means fixes were applied + cell.source = result.stdout.strip() if result.stdout else cell.source + except: + pass + # %% ../nbs/api/04_export.ipynb # includes the newline, because calling .strip() would affect all cells. _magics_pattern = re.compile(r'^\s*(%%|%).*\n?', re.MULTILINE) diff --git a/nbs/api/01_config.ipynb b/nbs/api/01_config.ipynb index 44d740639..7408fca04 100644 --- a/nbs/api/01_config.ipynb +++ b/nbs/api/01_config.ipynb @@ -144,6 +144,8 @@ " language='English', # Language PyPI classifier\n", " recursive:bool_arg=True, # Include subfolders in notebook globs?\n", " black_formatting:bool_arg=False, # Format libraries with black?\n", + " ruff_formatting:bool_arg=False, # Format libraries with ruff format?\n", + " ruff_fixing:bool_arg=False, # Fix libraries with ruff check --fix?\n", " readme_nb='index.ipynb', # Notebook to export as repo readme\n", " title='%(lib_name)s', # Quarto website title\n", " allowed_metadata_keys='', # Preserve the list of keys in the main notebook metadata\n", diff --git a/nbs/api/04_export.ipynb b/nbs/api/04_export.ipynb index 454457bc1..7305cb5ee 100644 --- a/nbs/api/04_export.ipynb +++ b/nbs/api/04_export.ipynb @@ -107,6 +107,27 @@ "### Optional export processors" ] }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "False False False\n" + ] + } + ], + "source": [ + "print(\n", + " get_config().black_formatting,\n", + " get_config().ruff_formatting,\n", + " get_config().ruff_fixing,\n", + ")" + ] + }, { "cell_type": "code", "execution_count": null, @@ -136,7 +157,101 @@ "source": [ "_cell = read_nb('../../tests/export_procs.ipynb')['cells'][0]\n", "black_format(_cell, force=True)\n", - "test_eq(_cell.source, 'j = [1, 2, 3]')" + "test_eq(_cell.source, 'import ast\\n\\nj = [1, 2, 3]\\nfor i in j:\\n str(1)')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#| export\n", + "def ruff_format(cell, # Cell to format\n", + " force=False): # Turn ruff formatting on regardless of settings.ini\n", + " \"Processor to format code with `ruff format`\"\n", + " try: cfg = get_config()\n", + " except FileNotFoundError: return\n", + " if (not cfg.ruff_formatting and not force) or cell.cell_type != 'code': return\n", + " \n", + " try: import subprocess\n", + " except: raise ImportError(\"subprocess is required\")\n", + " \n", + " try:\n", + " subprocess.run(['ruff', '--version'], capture_output=True, check=True)\n", + " except (subprocess.CalledProcessError, FileNotFoundError):\n", + " raise ImportError(\"You must install ruff: `pip install ruff` if you wish to use ruff formatting with nbdev\")\n", + " else:\n", + " try:\n", + " result = subprocess.run(\n", + " ['ruff', 'format', '-'],\n", + " input=cell.source,\n", + " capture_output=True,\n", + " text=True,\n", + " check=False\n", + " )\n", + " if result.returncode == 0:\n", + " cell.source = result.stdout.strip()\n", + " except:\n", + " pass" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "_cell = read_nb('../../tests/export_procs.ipynb')['cells'][0]\n", + "ruff_format(_cell, force=True)\n", + "test_eq(_cell.source, 'import ast\\n\\nj = [1, 2, 3]\\nfor i in j:\\n str(1)')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#| export\n", + "def ruff_fix(cell, # Cell to lint and fix\n", + " force=False): # Turn ruff fixing on regardless of settings.ini\n", + " \"Processor to lint and auto-fix code with `ruff check --fix`\"\n", + " try: cfg = get_config()\n", + " except FileNotFoundError: return\n", + " if (not cfg.ruff_fixing and not force) or cell.cell_type != 'code': return\n", + " \n", + " try: import subprocess\n", + " except: raise ImportError(\"subprocess is required\")\n", + "\n", + " try:\n", + " subprocess.run(['ruff', '--version'], capture_output=True, check=True)\n", + " except (subprocess.CalledProcessError, FileNotFoundError):\n", + " raise ImportError(\"You must install ruff: `pip install ruff` if you wish to use ruff formatting with nbdev\")\n", + " else:\n", + " try:\n", + " result = subprocess.run(\n", + " ['ruff', 'check', '--fix', '-'],\n", + " input=cell.source,\n", + " capture_output=True,\n", + " text=True,\n", + " check=False\n", + " )\n", + " if result.returncode == 0 or result.returncode == 1: # 1 means fixes were applied\n", + " cell.source = result.stdout.strip() if result.stdout else cell.source\n", + " except:\n", + " pass" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "_cell = read_nb('../../tests/export_procs.ipynb')['cells'][0]\n", + "ruff_fix(_cell, force=True)\n", + "test_eq(_cell.source, 'j = [1,\\n 2,\\n 3\\n]\\nfor i in j:\\n str(1)')" ] }, { @@ -221,7 +336,7 @@ "outputs": [], "source": [ "# every optional processor should be explicitly listed here\n", - "test_eq(optional_procs(), ['black_format', 'scrub_magics'])" + "test_eq(optional_procs(), ['black_format', 'ruff_format', 'ruff_fix', 'scrub_magics'])" ] }, { diff --git a/nbs/api/05_doclinks.ipynb b/nbs/api/05_doclinks.ipynb index b5dd803d6..024844aeb 100644 --- a/nbs/api/05_doclinks.ipynb +++ b/nbs/api/05_doclinks.ipynb @@ -365,7 +365,7 @@ "@delegates(nbglob_cli)\n", "def nbdev_export(\n", " path:str=None, # Path or filename\n", - " procs:Param(\"tokens naming the export processors to use.\", nargs=\"*\", choices=optional_procs())=\"black_format\",\n", + " procs:Param(\"tokens naming the export processors to use.\", nargs=\"*\", choices=optional_procs())=[\"black_format\", \"ruff_format\", \"ruff_fix\"],\n", " **kwargs):\n", " \"Export notebooks in `path` to Python modules\"\n", " if os.environ.get('IN_TEST',0): return\n", @@ -385,7 +385,7 @@ "source": [ "`procs` names the optional processors you wish to run on the exported cells of your notebook.\n", "\n", - "N.B.: the `black_format` processor is passed in by default. But it is a no-op, unless `black_formatting=True` is set in your `settings.ini` configuration. You can omit it from `nbdev_export` on the command line by passing in `--procs`." + "N.B.: the `[\"black_format\", \"ruff_format\", \"ruff_fix\"]` processor is passed in by default. But it is a no-op, unless `black_formatting=True`, `ruff_formatting=True`, or `ruff_fixing=True` is set in your `settings.ini` configuration. You can omit it from `nbdev_export` on the command line by passing in `--procs`." ] }, { diff --git a/pyproject.toml b/pyproject.toml index a4eca02de..fee87ac7d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,3 +9,4 @@ dynamic = [ "keywords", "description", "version", "dependencies", "optional-depe [tool.uv] cache-keys = [{ file = "pyproject.toml" }, { file = "settings.ini" }, { file = "setup.py" }] + diff --git a/settings.ini b/settings.ini index a201476e7..aef380320 100644 --- a/settings.ini +++ b/settings.ini @@ -19,7 +19,7 @@ requirements = fastcore>=1.8.14 execnb>=0.1.12 astunparse ghapi>=1.0.3 watchdog pip_requirements = PyYAML conda_requirements = pyyaml conda_user = fastai -dev_requirements = ipywidgets nbdev-numpy nbdev-stdlib pandas matplotlib black svg.py nbclassic pysymbol_llm llms-txt sphinx plum-dispatch +dev_requirements = ipywidgets nbdev-numpy nbdev-stdlib pandas matplotlib black ruff svg.py nbclassic pysymbol_llm llms-txt sphinx plum-dispatch console_scripts = nbdev_create_config=nbdev.config:nbdev_create_config nbdev_update=nbdev.sync:nbdev_update nbdev_update_license=nbdev.cli:nbdev_update_license @@ -63,6 +63,8 @@ git_url = https://github.com/AnswerDotAI/nbdev lib_path = nbdev title = nbdev black_formatting = False +ruff_formatting = False +ruff_fixing = False readme_nb = getting_started.ipynb allowed_metadata_keys = allowed_cell_metadata_keys = diff --git a/tests/export_procs.ipynb b/tests/export_procs.ipynb index c57cebf41..c634dffbd 100644 --- a/tests/export_procs.ipynb +++ b/tests/export_procs.ipynb @@ -7,10 +7,13 @@ "metadata": {}, "outputs": [], "source": [ + "import ast\n", "j = [1,\n", " 2,\n", " 3\n", - "]" + "]\n", + "for i in j:\n", + " str(1);" ] }, { @@ -36,9 +39,13 @@ ], "metadata": { "kernelspec": { - "display_name": "Python 3 (ipykernel)", + "display_name": "env", "language": "python", "name": "python3" + }, + "language_info": { + "name": "python", + "version": "3.13.3" } }, "nbformat": 4,