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()