Skip to content

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

@Mr-Ruben

Description

@Mr-Ruben

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions