Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions pandera/decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,11 @@ def _get_fn_argnames(fn: Callable) -> list[str]:

arg_spec_args = inspect.getfullargspec(fn).args

# Functions with only keyword-only args, *args, or **kwargs have
# no positional args → nothing to introspect for self/cls.
if not arg_spec_args:
return arg_spec_args

first_arg_is_self = arg_spec_args[0] == "self"
first_arg_is_cls = arg_spec_args[0] == "cls"
is_py_newer_than_39 = sys.version_info[:2] >= (3, 9)
Expand Down
85 changes: 85 additions & 0 deletions tests/pandas/test_get_fn_argnames_empty.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
"""
Regression tests for _get_fn_argnames IndexError on keyword-only functions
(GitHub issue #2422).

_get_fn_argnames() in pandera/decorators.py accessed arg_spec_args[0]
without checking if the list was empty. Functions with only keyword-only
args (``def f(*, x)``), variadic args (``def f(*args)``), or keyword-
variadic args (``def f(**kwargs)``) have an empty ``args`` list from
``inspect.getfullargspec``, causing IndexError.

The fix adds an early return when arg_spec_args is empty.
"""

import pytest
from pandera.decorators import _get_fn_argnames


class TestGetFnArgnamesEmptyArgs:
"""Functions with no positional args must not crash."""

def test_keyword_only_args(self):
"""def f(*, df, output_col) → returns [] without IndexError."""

def process(*, df, output_col):
pass

result = _get_fn_argnames(process)
assert result == []

def test_variadic_args_only(self):
"""def f(*args) → returns [] without IndexError."""

def process(*args):
pass

result = _get_fn_argnames(process)
assert result == []

def test_kwargs_only(self):
"""def f(**kwargs) → returns [] without IndexError."""

def process(**kwargs):
pass

result = _get_fn_argnames(process)
assert result == []

def test_no_args_at_all(self):
"""def f() → returns [] without IndexError."""

def process():
pass

result = _get_fn_argnames(process)
assert result == []


class TestGetFnArgnamesNormalCases:
"""Normal functions must still work correctly."""

def test_regular_function(self):
def process(df, output_col):
pass

result = _get_fn_argnames(process)
assert result == ["df", "output_col"]

def test_method_excludes_self(self):
class MyClass:
def process(self, df, output_col):
pass

obj = MyClass()
result = _get_fn_argnames(obj.process)
assert result == ["df", "output_col"]
assert "self" not in result

def test_mixed_positional_and_keyword_only(self):
"""def f(a, b, *, c) → returns ['a', 'b'] (keyword-only excluded)."""

def process(a, b, *, c):
pass

result = _get_fn_argnames(process)
assert result == ["a", "b"]