AnswerDotAI / AnswerDotAI/nbdev

Bug Report: `nbdev` Parser `IndexError` on `@patch_to` with Keyword Arguments

Open
#1,596 1 comment 0 reactions 0 assignees View on GitHub
bug
Dominant language
Jupyter Notebook
Stars
5.3k
Forks
513
Avg merge
2d 30m
Merged PRs (30d)
8

Description

## 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:**
```python
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:
```python
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

````python
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
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

```

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.