Bug Report: nbdev Parser IndexError on @patch_to with Keyword Arguments
Description
The nbdev AST parser fails with an IndexError when it encounters the @patch_to decorator used with a keyword argument (e.g., cls=ClassName) instead of a positional argument. This occurs during processes like nbdev_export that involve indexing symbols.
Root Cause
In nbdev/doclinks.py, the patch_name function attempts to extract the class name being patched. It explicitly assumes the class is the first positional argument in the decorator's AST representation.
File: nbdev/doclinks.py
Code Snippet:
elif nm=='patch_to': a = d.args[0] # <--- CRASHES HERE
If d.args is empty (which happens when a keyword argument is used), d.args[0] raises an IndexError.
Reproduction Proof
As demonstrated in the reproduction script above:
- Input:
@patch_to(MyClass) -> d.args contains 1 element. Result: Success.
- Input:
@patch_to(cls=MyClass) -> d.args is empty; d.keywords contains the data. Result: IndexError.
Impact
Users cannot use standard Python keyword argument syntax for the @patch_to decorator without breaking the nbdev export/documentation pipeline.
Recommended Fix
The parser should check both d.args and d.keywords. A robust fix would be:
elif nm=='patch_to':
a = d.args[0] if d.args else [k.value for k in d.keywords if k.arg=='cls'][0]
Reproduce with
import os
import nbformat as nbf
import traceback
from nbdev.doclinks import nbdev_export
# 1. Setup a clean minimal nbdev project structure
!rm -rf /content/reproduction_project
!mkdir -p /content/reproduction_project/reproduction_project
%cd /content/reproduction_project
# 2. Manually create pyproject.toml
with open('pyproject.toml', 'w') as f:
f.write("""[project]\nname = \"reproduction_project\"\nauthors = [{name=\"Test\"}]\n\n[tool.nbdev]\nlib_name = \"reproduction_project\"\nuser = \"testuser\"\nlib_path = \"reproduction_project\"\nnbs_path = \".\"\nrecursive = false\ntst_flags = \"notest\"\n""")
# 3. Create a notebook with the bug-triggering code and required default_exp
nb = nbf.v4.new_notebook()
# Adding default_exp to satisfy nbdev requirements
code = """#| default_exp core\n#| export\nfrom fastcore.utils import patch_to\n\nclass MyClass: pass\n\n@patch_to(cls=MyClass)\ndef my_method(self): \n return \"Hello World\"\n"""
nb['cells'] = [nbf.v4.new_code_cell(code)]
with open('00_core.ipynb', 'w') as f:
nbf.write(nb, f)
print("\n--- Attempting nbdev_export (Expected to trigger IndexError) ---")
try:
nbdev_export()
except Exception as e:
print(f"\nCaught Error: {type(e).__name__}: {e}")
traceback.print_exc()
I get
--- Attempting nbdev_export (Expected to trigger IndexError) ---
Caught Error: IndexError: list index out of range
Traceback (most recent call last):
File "/tmp/ipykernel_2846/2623198774.py", line 26, in <cell line: 0>
nbdev_export()
File "/usr/local/lib/python3.12/dist-packages/fastcore/script.py", line 161, in _f
if not mod: return func(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.12/dist-packages/nbdev/doclinks.py", line 156, in nbdev_export
_build_modidx()
File "/usr/local/lib/python3.12/dist-packages/nbdev/doclinks.py", line 112, in _build_modidx
try: res['syms'].update(_get_modidx((dest.parent/file).resolve(), code_root, nbs_path=nbs_path))
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.12/dist-packages/nbdev/doclinks.py", line 91, in _get_modidx
if isinstance(tree, _def_types): _stor(patch_name(tree))
^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.12/dist-packages/nbdev/doclinks.py", line 51, in patch_name
elif nm=='patch_to': a = d.args[0]
~~~~~~^^^
IndexError: list index out of range
Bug Report:
nbdevParserIndexErroron@patch_towith Keyword ArgumentsDescription
The
nbdevAST parser fails with anIndexErrorwhen it encounters the@patch_todecorator used with a keyword argument (e.g.,cls=ClassName) instead of a positional argument. This occurs during processes likenbdev_exportthat involve indexing symbols.Root Cause
In
nbdev/doclinks.py, thepatch_namefunction attempts to extract the class name being patched. It explicitly assumes the class is the first positional argument in the decorator's AST representation.File:
nbdev/doclinks.pyCode Snippet:
If
d.argsis empty (which happens when a keyword argument is used),d.args[0]raises anIndexError.Reproduction Proof
As demonstrated in the reproduction script above:
@patch_to(MyClass)->d.argscontains 1 element. Result: Success.@patch_to(cls=MyClass)->d.argsis empty;d.keywordscontains the data. Result: IndexError.Impact
Users cannot use standard Python keyword argument syntax for the
@patch_todecorator without breaking thenbdevexport/documentation pipeline.Recommended Fix
The parser should check both
d.argsandd.keywords. A robust fix would be:Reproduce with
I get