This is an automated email from the ASF dual-hosted git repository.

junrushao pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tvm-ffi.git


The following commit(s) were added to refs/heads/main by this push:
     new 6bc1a8e  [FEAT] Further robustify kwargs wrapper (#311)
6bc1a8e is described below

commit 6bc1a8ebb228bd0a6357e1e1da86a8febebf07ba
Author: Tianqi Chen <[email protected]>
AuthorDate: Thu Dec 4 14:07:50 2025 -0500

    [FEAT] Further robustify kwargs wrapper (#311)
    
    This PR further robustifies kwargs wrapper
    to support ignore_arg_names and explicit error for keywords in
    arguments.
---
 python/tvm_ffi/utils/kwargs_wrapper.py    | 179 ++++++++++++++++--------------
 tests/python/utils/test_kwargs_wrapper.py | 134 +++++++++++++++-------
 tests/scripts/benchmark_kwargs_wrapper.py |   2 +-
 3 files changed, 190 insertions(+), 125 deletions(-)

diff --git a/python/tvm_ffi/utils/kwargs_wrapper.py 
b/python/tvm_ffi/utils/kwargs_wrapper.py
index 5f6395b..13561e6 100644
--- a/python/tvm_ffi/utils/kwargs_wrapper.py
+++ b/python/tvm_ffi/utils/kwargs_wrapper.py
@@ -24,7 +24,8 @@ from __future__ import annotations
 
 import functools
 import inspect
-from typing import Any, Callable
+import keyword
+from typing import Any, Callable, Iterable
 
 # Sentinel object for missing arguments
 MISSING = object()
@@ -51,6 +52,10 @@ def _validate_argument_names(names: list[str], arg_type: 
str) -> None:
             raise TypeError(
                 f"{arg_type} name must be a string, got {type(name).__name__}: 
{name!r}"
             )
+        if keyword.iskeyword(name):
+            raise ValueError(
+                f"Invalid {arg_type.lower()} name: {name!r} is a Python 
keyword and cannot be used as a parameter name"
+            )
         if not name.isidentifier():
             raise ValueError(
                 f"Invalid {arg_type.lower()} name: {name!r} is not a valid 
Python identifier"
@@ -58,61 +63,61 @@ def _validate_argument_names(names: list[str], arg_type: 
str) -> None:
 
 
 def _validate_wrapper_args(
-    args_names: list[str],
-    args_defaults: tuple,
-    kwargsonly_names: list[str],
-    kwargsonly_defaults: dict[str, Any],
+    arg_names: list[str],
+    arg_defaults: tuple,
+    kwonly_names: list[str],
+    kwonly_defaults: dict[str, Any],
     reserved_names: set[str],
 ) -> None:
     """Validate all input arguments for make_kwargs_wrapper.
 
     Parameters
     ----------
-    args_names
+    arg_names
         List of positional argument names.
-    args_defaults
+    arg_defaults
         Tuple of default values for positional arguments.
-    kwargsonly_names
+    kwonly_names
         List of keyword-only argument names.
-    kwargsonly_defaults
+    kwonly_defaults
         Dictionary of default values for keyword-only arguments.
     reserved_names
         Set of reserved internal names that cannot be used as argument names.
 
     """
-    # Validate args_names are valid Python identifiers and unique
-    _validate_argument_names(args_names, "Argument")
+    # Validate arg_names are valid Python identifiers and unique
+    _validate_argument_names(arg_names, "Argument")
 
-    # Validate args_defaults is a tuple
-    if not isinstance(args_defaults, tuple):
-        raise TypeError(f"args_defaults must be a tuple, got 
{type(args_defaults).__name__}")
+    # Validate arg_defaults is a tuple
+    if not isinstance(arg_defaults, tuple):
+        raise TypeError(f"arg_defaults must be a tuple, got 
{type(arg_defaults).__name__}")
 
-    # Validate args_defaults length doesn't exceed args_names length
-    if len(args_defaults) > len(args_names):
+    # Validate arg_defaults length doesn't exceed arg_names length
+    if len(arg_defaults) > len(arg_names):
         raise ValueError(
-            f"args_defaults has {len(args_defaults)} values but only "
-            f"{len(args_names)} positional arguments"
+            f"arg_defaults has {len(arg_defaults)} values but only "
+            f"{len(arg_names)} positional arguments"
         )
 
-    # Validate kwargsonly_names are valid identifiers and unique
-    _validate_argument_names(kwargsonly_names, "Keyword-only argument")
+    # Validate kwonly_names are valid identifiers and unique
+    _validate_argument_names(kwonly_names, "Keyword-only argument")
 
-    # Validate kwargsonly_defaults keys are in kwargsonly_names
-    kwargsonly_names_set = set(kwargsonly_names)
-    for key in kwargsonly_defaults:
-        if key not in kwargsonly_names_set:
+    # Validate kwonly_defaults keys are in kwonly_names
+    kwonly_names_set = set(kwonly_names)
+    for key in kwonly_defaults:
+        if key not in kwonly_names_set:
             raise ValueError(
-                f"Default provided for '{key}' which is not in 
kwargsonly_names: {kwargsonly_names}"
+                f"Default provided for '{key}' which is not in kwonly_names: 
{kwonly_names}"
             )
 
     # Validate no overlap between positional and keyword-only arguments
-    args_names_set = set(args_names)
-    overlap = args_names_set & kwargsonly_names_set
+    arg_names_set = set(arg_names)
+    overlap = arg_names_set & kwonly_names_set
     if overlap:
         raise ValueError(f"Arguments cannot be both positional and 
keyword-only: {overlap}")
 
     # Validate no conflict between user argument names and internal names
-    all_user_names = args_names_set | kwargsonly_names_set
+    all_user_names = arg_names_set | kwonly_names_set
     conflicts = all_user_names & reserved_names
     if conflicts:
         raise ValueError(
@@ -123,11 +128,11 @@ def _validate_wrapper_args(
 
 def make_kwargs_wrapper(
     target_func: Callable,
-    args_names: list[str],
-    args_defaults: tuple = (),
-    kwargsonly_names: list[str] | None = None,
-    kwargsonly_defaults: dict[str, Any] | None = None,
-    prototype_func: Callable | None = None,
+    arg_names: list[str],
+    arg_defaults: tuple = (),
+    kwonly_names: list[str] | None = None,
+    kwonly_defaults: dict[str, Any] | None = None,
+    prototype: Callable | None = None,
 ) -> Callable:
     """Create a wrapper with kwargs support for a function that only accepts 
positional arguments.
 
@@ -138,27 +143,27 @@ def make_kwargs_wrapper(
     target_func
         The underlying function to be called by the wrapper. This function 
must only
         accept positional arguments.
-    args_names
+    arg_names
         A list of ALL positional argument names in order. These define the 
positional
-        parameters that the wrapper will accept. Must not overlap with 
kwargsonly_names.
-    args_defaults
-        A tuple of default values for positional arguments, right-aligned to 
args_names
+        parameters that the wrapper will accept. Must not overlap with 
kwonly_names.
+    arg_defaults
+        A tuple of default values for positional arguments, right-aligned to 
arg_names
         (matching Python's __defaults__ behavior). The length of this tuple 
determines
         how many trailing arguments have defaults.
-        Example: (10, 20) with args_names=['a', 'b', 'c', 'd'] means c=10, 
d=20.
+        Example: (10, 20) with arg_names=['a', 'b', 'c', 'd'] means c=10, d=20.
         Empty tuple () means no defaults.
-    kwargsonly_names
+    kwonly_names
         A list of keyword-only argument names. These arguments can only be 
passed by name,
         not positionally, and appear after a '*' separator in the signature. 
Can include both
-        required and optional keyword-only arguments. Must not overlap with 
args_names.
+        required and optional keyword-only arguments. Must not overlap with 
arg_names.
         Example: ['debug', 'timeout'] creates wrapper(..., *, debug, timeout).
-    kwargsonly_defaults
+    kwonly_defaults
         Optional dictionary of default values for keyword-only arguments 
(matching Python's
-        __kwdefaults__ behavior). Keys must be a subset of kwargsonly_names. 
Keyword-only
+        __kwdefaults__ behavior). Keys must be a subset of kwonly_names. 
Keyword-only
         arguments not in this dict are required.
-        Example: {'timeout': 30} with kwargsonly_names=['debug', 'timeout'] 
means 'debug'
+        Example: {'timeout': 30} with kwonly_names=['debug', 'timeout'] means 
'debug'
         is required and 'timeout' is optional.
-    prototype_func
+    prototype
         Optional prototype function to copy metadata (__name__, __doc__, 
__module__,
         __qualname__, __annotations__) from. If None, no metadata is copied.
 
@@ -175,26 +180,24 @@ def make_kwargs_wrapper(
 
     """
     # Normalize inputs
-    if kwargsonly_names is None:
-        kwargsonly_names = []
-    if kwargsonly_defaults is None:
-        kwargsonly_defaults = {}
+    if kwonly_names is None:
+        kwonly_names = []
+    if kwonly_defaults is None:
+        kwonly_defaults = {}
 
     # Internal variable names used in generated code to avoid user argument 
conflicts
     _INTERNAL_TARGET_FUNC = "__i_target_func"
     _INTERNAL_MISSING = "__i_MISSING"
-    _INTERNAL_DEFAULTS_DICT = "__i_args_defaults"
+    _INTERNAL_DEFAULTS_DICT = "__i_arg_defaults"
     _INTERNAL_NAMES = {_INTERNAL_TARGET_FUNC, _INTERNAL_MISSING, 
_INTERNAL_DEFAULTS_DICT}
 
     # Validate all input arguments
-    _validate_wrapper_args(
-        args_names, args_defaults, kwargsonly_names, kwargsonly_defaults, 
_INTERNAL_NAMES
-    )
+    _validate_wrapper_args(arg_names, arg_defaults, kwonly_names, 
kwonly_defaults, _INTERNAL_NAMES)
 
     # Build positional defaults dictionary (right-aligned)
-    # Example: args_names=["a","b","c","d"], args_defaults=(10,20) -> {"c":10, 
"d":20}
-    args_defaults_dict = (
-        dict(zip(args_names[-len(args_defaults) :], args_defaults)) if 
args_defaults else {}
+    # Example: arg_names=["a","b","c","d"], arg_defaults=(10,20) -> {"c":10, 
"d":20}
+    arg_defaults_dict = (
+        dict(zip(arg_names[-len(arg_defaults) :], arg_defaults)) if 
arg_defaults else {}
     )
 
     # Build wrapper signature and call arguments
@@ -236,19 +239,19 @@ def make_kwargs_wrapper(
             )
 
     # Handle positional arguments
-    for name in args_names:
-        if name in args_defaults_dict:
-            _add_param_with_default(name, args_defaults_dict[name])
+    for name in arg_names:
+        if name in arg_defaults_dict:
+            _add_param_with_default(name, arg_defaults_dict[name])
         else:
             arg_parts.append(name)
             call_parts.append(name)
 
     # Handle keyword-only arguments
-    if kwargsonly_names:
+    if kwonly_names:
         arg_parts.append("*")  # Separator for keyword-only args
-        for name in kwargsonly_names:
-            if name in kwargsonly_defaults:
-                _add_param_with_default(name, kwargsonly_defaults[name])
+        for name in kwonly_names:
+            if name in kwonly_defaults:
+                _add_param_with_default(name, kwonly_defaults[name])
             else:
                 # Required keyword-only arg (no default)
                 arg_parts.append(name)
@@ -276,9 +279,9 @@ def wrapper({arg_list}):
     exec(code_str, exec_globals, namespace)
     new_func = namespace["wrapper"]
 
-    # Copy metadata from prototype_func if provided
-    if prototype_func is not None:
-        functools.update_wrapper(new_func, prototype_func, updated=())
+    # Copy metadata from prototype if provided
+    if prototype is not None:
+        functools.update_wrapper(new_func, prototype, updated=())
 
     return new_func
 
@@ -286,7 +289,8 @@ def wrapper({arg_list}):
 def make_kwargs_wrapper_from_signature(
     target_func: Callable,
     signature: inspect.Signature,
-    prototype_func: Callable | None = None,
+    prototype: Callable | None = None,
+    exclude_arg_names: Iterable[str] | None = None,
 ) -> Callable:
     """Create a wrapper with kwargs support for a function that only accepts 
positional arguments.
 
@@ -300,9 +304,13 @@ def make_kwargs_wrapper_from_signature(
         The underlying function to be called by the wrapper.
     signature
         An inspect.Signature object describing the desired wrapper signature.
-    prototype_func
+    prototype
         Optional prototype function to copy metadata (__name__, __doc__, 
__module__,
         __qualname__, __annotations__) from. If None, no metadata is copied.
+    exclude_arg_names
+        Optional iterable of argument names to ignore when extracting 
parameters from the signature.
+        These arguments will not be included in the generated wrapper. If a 
name in this iterable
+        does not exist in the signature, it is silently ignored.
 
     Returns
     -------
@@ -314,16 +322,23 @@ def make_kwargs_wrapper_from_signature(
         If the signature contains *args or **kwargs.
 
     """
+    # Normalize exclude_arg_names to a set for efficient lookup
+    skip_set = set(exclude_arg_names) if exclude_arg_names is not None else 
set()
+
     # Extract positional and keyword-only parameters
-    args_names = []
-    args_defaults_list = []
-    kwargsonly_names = []
-    kwargsonly_defaults = {}
+    arg_names = []
+    arg_defaults_list = []
+    kwonly_names = []
+    kwonly_defaults = {}
 
     # Track when we start seeing defaults for positional args
     has_seen_positional_default = False
 
     for param_name, param in signature.parameters.items():
+        # Skip arguments that are in the skip list
+        if param_name in skip_set:
+            continue
+
         if param.kind == inspect.Parameter.VAR_POSITIONAL:
             raise ValueError("*args not supported in wrapper generation")
         elif param.kind == inspect.Parameter.VAR_KEYWORD:
@@ -332,10 +347,10 @@ def make_kwargs_wrapper_from_signature(
             inspect.Parameter.POSITIONAL_ONLY,
             inspect.Parameter.POSITIONAL_OR_KEYWORD,
         ):
-            args_names.append(param_name)
+            arg_names.append(param_name)
             if param.default is not inspect.Parameter.empty:
                 has_seen_positional_default = True
-                args_defaults_list.append(param.default)
+                arg_defaults_list.append(param.default)
             elif has_seen_positional_default:
                 # Required arg after optional arg (invalid in Python)
                 raise ValueError(
@@ -343,18 +358,18 @@ def make_kwargs_wrapper_from_signature(
                     f"parameters with defaults"
                 )
         elif param.kind == inspect.Parameter.KEYWORD_ONLY:
-            kwargsonly_names.append(param_name)
+            kwonly_names.append(param_name)
             if param.default is not inspect.Parameter.empty:
-                kwargsonly_defaults[param_name] = param.default
+                kwonly_defaults[param_name] = param.default
 
-    # Convert defaults list to tuple (right-aligned to args_names)
-    args_defaults = tuple(args_defaults_list)
+    # Convert defaults list to tuple (right-aligned to arg_names)
+    arg_defaults = tuple(arg_defaults_list)
 
     return make_kwargs_wrapper(
         target_func,
-        args_names,
-        args_defaults,
-        kwargsonly_names,
-        kwargsonly_defaults,
-        prototype_func,
+        arg_names,
+        arg_defaults,
+        kwonly_names,
+        kwonly_defaults,
+        prototype,
     )
diff --git a/tests/python/utils/test_kwargs_wrapper.py 
b/tests/python/utils/test_kwargs_wrapper.py
index 244c50f..aa304cd 100644
--- a/tests/python/utils/test_kwargs_wrapper.py
+++ b/tests/python/utils/test_kwargs_wrapper.py
@@ -36,19 +36,19 @@ def test_basic_wrapper() -> None:
     assert wrapper(1, b=2, c=3) == 6
 
     # Single default argument
-    wrapper = make_kwargs_wrapper(target, ["a", "b", "c"], args_defaults=(10,))
+    wrapper = make_kwargs_wrapper(target, ["a", "b", "c"], arg_defaults=(10,))
     assert wrapper(1, 2) == 13  # c=10
     assert wrapper(1, 2, 3) == 6  # c=3 explicit
     assert wrapper(1, 2, c=5) == 8  # c=5 via keyword
 
     # Multiple defaults (right-aligned)
-    wrapper = make_kwargs_wrapper(target, ["a", "b", "c"], args_defaults=(20, 
30))
+    wrapper = make_kwargs_wrapper(target, ["a", "b", "c"], arg_defaults=(20, 
30))
     assert wrapper(1) == 51  # b=20, c=30
     assert wrapper(1, 2) == 33  # b=2, c=30
     assert wrapper(1, 2, 3) == 6  # all explicit
 
     # All defaults
-    wrapper = make_kwargs_wrapper(target, ["a", "b", "c"], args_defaults=(1, 
2, 3))
+    wrapper = make_kwargs_wrapper(target, ["a", "b", "c"], arg_defaults=(1, 2, 
3))
     assert wrapper() == 6
     assert wrapper(10) == 15
     assert wrapper(10, 20, 30) == 60
@@ -62,7 +62,7 @@ def test_basic_wrapper() -> None:
             return self.base + a + b
 
     calc = Calculator(100)
-    wrapper = make_kwargs_wrapper(calc.add, ["a", "b"], args_defaults=(5,))
+    wrapper = make_kwargs_wrapper(calc.add, ["a", "b"], arg_defaults=(5,))
     assert wrapper(1) == 106
 
 
@@ -76,25 +76,25 @@ def test_keyword_only_arguments() -> None:
     wrapper = make_kwargs_wrapper(
         target,
         ["a", "b"],
-        args_defaults=(),
-        kwargsonly_names=["c", "d"],
-        kwargsonly_defaults={"c": 100, "d": 200},
+        arg_defaults=(),
+        kwonly_names=["c", "d"],
+        kwonly_defaults={"c": 100, "d": 200},
     )
     assert wrapper(1, 2) == 303  # c=100, d=200
     assert wrapper(1, 2, c=10) == 213  # d=200
     assert wrapper(1, 2, c=10, d=20) == 33
 
     wrapper = make_kwargs_wrapper(
-        target, ["a", "b"], args_defaults=(), kwargsonly_names=["c", "d"], 
kwargsonly_defaults={}
+        target, ["a", "b"], arg_defaults=(), kwonly_names=["c", "d"], 
kwonly_defaults={}
     )
     assert wrapper(1, 2, c=10, d=20) == 33  # c and d are required
 
     wrapper = make_kwargs_wrapper(
         target,
         ["a", "b"],
-        args_defaults=(),
-        kwargsonly_names=["c", "d"],
-        kwargsonly_defaults={"d": 100},
+        arg_defaults=(),
+        kwonly_names=["c", "d"],
+        kwonly_defaults={"d": 100},
     )
     assert wrapper(1, 2, c=10) == 113  # c required, d=100
     assert wrapper(1, 2, c=10, d=20) == 33  # both explicit
@@ -102,9 +102,9 @@ def test_keyword_only_arguments() -> None:
     wrapper = make_kwargs_wrapper(
         target,
         ["a", "b", "c"],
-        args_defaults=(10,),
-        kwargsonly_names=["d", "e"],
-        kwargsonly_defaults={"d": 20, "e": 30},
+        arg_defaults=(10,),
+        kwonly_names=["d", "e"],
+        kwonly_defaults={"d": 20, "e": 30},
     )
     assert wrapper(1, 2) == 63  # c=10, d=20, e=30
     assert wrapper(1, 2, 5, d=15) == 53  # c=5 explicit, e=30
@@ -120,7 +120,7 @@ def test_validation_errors() -> None:
 
     # Duplicate keyword-only argument names
     with pytest.raises(ValueError, match="Duplicate keyword-only argument 
names found"):
-        make_kwargs_wrapper(target, ["a"], kwargsonly_names=["b", "c", "b"])
+        make_kwargs_wrapper(target, ["a"], kwonly_names=["b", "c", "b"])
 
     # Invalid argument name types
     with pytest.raises(TypeError, match="Argument name must be a string"):
@@ -130,23 +130,27 @@ def test_validation_errors() -> None:
     with pytest.raises(ValueError, match="not a valid Python identifier"):
         make_kwargs_wrapper(target, ["a", "b-c"])
 
-    # args_defaults not a tuple
-    with pytest.raises(TypeError, match="args_defaults must be a tuple"):
-        make_kwargs_wrapper(target, ["a", "b"], args_defaults=[10])  # type: 
ignore[arg-type]
+    # Python keywords cannot be used as parameter names
+    with pytest.raises(
+        ValueError, match="is a Python keyword and cannot be used as a 
parameter name"
+    ):
+        make_kwargs_wrapper(target, ["a", "if"])
 
-    # args_defaults too long
-    with pytest.raises(ValueError, match=r"args_defaults has .* values but 
only"):
-        make_kwargs_wrapper(target, ["a"], args_defaults=(1, 2, 3))
+    # arg_defaults not a tuple
+    with pytest.raises(TypeError, match="arg_defaults must be a tuple"):
+        make_kwargs_wrapper(target, ["a", "b"], arg_defaults=[10])  # type: 
ignore[arg-type]
+
+    # arg_defaults too long
+    with pytest.raises(ValueError, match=r"arg_defaults has .* values but 
only"):
+        make_kwargs_wrapper(target, ["a"], arg_defaults=(1, 2, 3))
 
     # Overlap between positional and keyword-only
     with pytest.raises(ValueError, match="cannot be both positional and 
keyword-only"):
-        make_kwargs_wrapper(target, ["a", "b"], kwargsonly_names=["b"])
+        make_kwargs_wrapper(target, ["a", "b"], kwonly_names=["b"])
 
-    # kwargsonly_defaults key not in kwargsonly_names
-    with pytest.raises(ValueError, match="not in kwargsonly_names"):
-        make_kwargs_wrapper(
-            target, ["a", "b"], kwargsonly_names=["c"], 
kwargsonly_defaults={"d": 10}
-        )
+    # kwonly_defaults key not in kwonly_names
+    with pytest.raises(ValueError, match="not in kwonly_names"):
+        make_kwargs_wrapper(target, ["a", "b"], kwonly_names=["c"], 
kwonly_defaults={"d": 10})
 
     # Internal name conflict
     with pytest.raises(ValueError, match="conflict with internal names"):
@@ -160,12 +164,12 @@ def test_special_default_values() -> None:
         return (a, b, c)
 
     # None as default
-    wrapper = make_kwargs_wrapper(target, ["a", "b", "c"], 
args_defaults=(None, None))
+    wrapper = make_kwargs_wrapper(target, ["a", "b", "c"], arg_defaults=(None, 
None))
     assert wrapper(1) == (1, None, None)
 
     # Complex objects as defaults (verify object reference is preserved)
     default_list = [1, 2, 3]
-    wrapper = make_kwargs_wrapper(target, ["a", "b", "c"], 
args_defaults=(default_list, None))
+    wrapper = make_kwargs_wrapper(target, ["a", "b", "c"], 
arg_defaults=(default_list, None))
     result = wrapper(1)
     assert result[1] is default_list
 
@@ -184,7 +188,7 @@ def test_wrapper_with_signature() -> None:
     assert wrapper(1, 2, 3) == 26  # 1 + 2 + 3 + 20
     assert wrapper(1, 2, 3, 4) == 10  # 1 + 2 + 3 + 4
 
-    # Test metadata preservation when prototype_func is provided
+    # Test metadata preservation when prototype is provided
     wrapper_with_metadata = make_kwargs_wrapper_from_signature(target, sig, 
source_func)
     assert wrapper_with_metadata.__name__ == "source_func"
     assert wrapper_with_metadata.__doc__ == "Source function documentation."
@@ -218,6 +222,54 @@ def test_wrapper_with_signature() -> None:
     with pytest.raises(ValueError, match=r"\*\*kwargs not supported"):
         make_kwargs_wrapper_from_signature(target, 
inspect.signature(with_kwargs))
 
+    # Test exclude_arg_names - ignore certain arguments from the signature
+    def source_with_skip(a: Any, b: Any, c: int = 10, d: int = 20) -> None:
+        pass
+
+    wrapper = make_kwargs_wrapper_from_signature(
+        target, inspect.signature(source_with_skip), exclude_arg_names=["c"]
+    )
+    # c is ignored, so wrapper should only have a, b, d
+    assert wrapper(1, 2) == 23  # 1 + 2 + 20 (d=20)
+    assert wrapper(1, 2, d=5) == 8  # 1 + 2 + 5
+
+    # Test ignoring multiple arguments
+    wrapper = make_kwargs_wrapper_from_signature(
+        target, inspect.signature(source_with_skip), exclude_arg_names=["b", 
"d"]
+    )
+    # b and d are ignored, so wrapper should only have a, c
+    assert wrapper(1) == 11  # 1 + 10 (c=10)
+    assert wrapper(1, c=5) == 6  # 1 + 5
+
+    # Test ignoring keyword-only arguments
+    def source_kwonly_skip(a: Any, b: Any, *, c: int = 10, d: int = 20) -> 
None:
+        pass
+
+    wrapper = make_kwargs_wrapper_from_signature(
+        target, inspect.signature(source_kwonly_skip), exclude_arg_names=["c"]
+    )
+    # c is skipped, so wrapper should only have a, b, d
+    assert wrapper(1, 2) == 23  # 1 + 2 + 20 (d=20)
+    assert wrapper(1, 2, d=5) == 8  # 1 + 2 + 5
+
+    # Test excluding a non-existent argument (should be silently ignored)
+    wrapper = make_kwargs_wrapper_from_signature(
+        target, inspect.signature(source_with_skip), 
exclude_arg_names=["non_existent"]
+    )
+    # Should be the same as no exclusion
+    assert wrapper(1, 2) == 33  # 1 + 2 + 10 + 20
+    assert wrapper(1, 2, 3, 4) == 10  # 1 + 2 + 3 + 4
+
+    # Test excluding both existing and non-existent arguments
+    wrapper = make_kwargs_wrapper_from_signature(
+        target,
+        inspect.signature(source_with_skip),
+        exclude_arg_names=["c", "non_existent", "also_missing"],
+    )
+    # Only c should be excluded, non-existent names are ignored
+    assert wrapper(1, 2) == 23  # 1 + 2 + 20 (d=20, c excluded)
+    assert wrapper(1, 2, d=5) == 8  # 1 + 2 + 5
+
 
 def test_exception_propagation() -> None:
     """Test that exceptions from the target function are properly 
propagated."""
@@ -232,7 +284,7 @@ def test_exception_propagation() -> None:
         return a + b
 
     # Test with positional defaults
-    wrapper = make_kwargs_wrapper(raising_func, ["a", "b", "c"], 
args_defaults=(10, "valid"))
+    wrapper = make_kwargs_wrapper(raising_func, ["a", "b", "c"], 
arg_defaults=(10, "valid"))
     assert wrapper(5) == 15
 
     with pytest.raises(ValueError, match="a cannot be zero"):
@@ -245,8 +297,8 @@ def test_exception_propagation() -> None:
     wrapper_kwonly = make_kwargs_wrapper(
         raising_func,
         ["a"],
-        kwargsonly_names=["b", "c"],
-        kwargsonly_defaults={"b": 10, "c": "valid"},
+        kwonly_names=["b", "c"],
+        kwonly_defaults={"b": 10, "c": "valid"},
     )
     assert wrapper_kwonly(5) == 15
 
@@ -261,7 +313,7 @@ def test_exception_propagation() -> None:
 
 
 def test_metadata_preservation() -> None:
-    """Test that function metadata is preserved when prototype_func is 
provided."""
+    """Test that function metadata is preserved when prototype is provided."""
 
     def my_function(x: int, y: int = 10) -> int:
         """Document the function."""
@@ -269,9 +321,7 @@ def test_metadata_preservation() -> None:
 
     target = lambda *args: sum(args)
 
-    wrapper = make_kwargs_wrapper(
-        target, ["x", "y"], args_defaults=(10,), prototype_func=my_function
-    )
+    wrapper = make_kwargs_wrapper(target, ["x", "y"], arg_defaults=(10,), 
prototype=my_function)
     assert wrapper.__name__ == "my_function"
     assert wrapper.__doc__ == "Document the function."
     assert wrapper.__annotations__ == my_function.__annotations__
@@ -290,19 +340,19 @@ def test_optimized_default_types() -> None:
         return args
 
     # Test None default (should be optimized - directly embedded)
-    wrapper = make_kwargs_wrapper(target, ["a", "b", "c"], 
args_defaults=(None,))
+    wrapper = make_kwargs_wrapper(target, ["a", "b", "c"], 
arg_defaults=(None,))
     assert wrapper(1, 2) == (1, 2, None)
     assert wrapper(1, 2, 3) == (1, 2, 3)
     assert wrapper(1, 2, c=None) == (1, 2, None)
 
     # Test bool defaults (should be optimized - directly embedded)
-    wrapper = make_kwargs_wrapper(target, ["a", "flag", "debug"], 
args_defaults=(True, False))
+    wrapper = make_kwargs_wrapper(target, ["a", "flag", "debug"], 
arg_defaults=(True, False))
     assert wrapper(1) == (1, True, False)
     assert wrapper(1, False) == (1, False, False)
     assert wrapper(1, flag=False, debug=True) == (1, False, True)
 
     # Test str default (should use MISSING sentinel for safety)
-    wrapper = make_kwargs_wrapper(target, ["a", "b", "name"], 
args_defaults=("default",))
+    wrapper = make_kwargs_wrapper(target, ["a", "b", "name"], 
arg_defaults=("default",))
     assert wrapper(1, 2) == (1, 2, "default")
     assert wrapper(1, 2, "custom") == (1, 2, "custom")
     assert wrapper(1, 2, name="custom") == (1, 2, "custom")
@@ -311,8 +361,8 @@ def test_optimized_default_types() -> None:
     wrapper = make_kwargs_wrapper(
         target,
         ["a"],
-        kwargsonly_names=["b", "flag", "name"],
-        kwargsonly_defaults={"b": None, "flag": True, "name": "default"},
+        kwonly_names=["b", "flag", "name"],
+        kwonly_defaults={"b": None, "flag": True, "name": "default"},
     )
     assert wrapper(1) == (1, None, True, "default")
     assert wrapper(1, b=2) == (1, 2, True, "default")
diff --git a/tests/scripts/benchmark_kwargs_wrapper.py 
b/tests/scripts/benchmark_kwargs_wrapper.py
index 6eb0890..892d06e 100644
--- a/tests/scripts/benchmark_kwargs_wrapper.py
+++ b/tests/scripts/benchmark_kwargs_wrapper.py
@@ -40,7 +40,7 @@ def benchmark_kwargs_wrapper(repeat: int = 1000000) -> None:
     z = 3
 
     # Create wrapper with two optional kwargs
-    wrapper = make_kwargs_wrapper(target_func, ["x", "y", "z"], 
args_defaults=(None, None))
+    wrapper = make_kwargs_wrapper(target_func, ["x", "y", "z"], 
arg_defaults=(None, None))
 
     # Benchmark 1: Direct call to target function (baseline)
     start = time.time()

Reply via email to