diff --git a/changelog/69901.fixed.md b/changelog/69901.fixed.md new file mode 100644 index 000000000000..c28474dd27fd --- /dev/null +++ b/changelog/69901.fixed.md @@ -0,0 +1 @@ +Fixed `salt.utils.functools` `namespaced_function`/`alias_function` dropping keyword-only argument defaults in copied function diff --git a/salt/utils/functools.py b/salt/utils/functools.py index 224d096957c4..d7dbe4b7e28b 100644 --- a/salt/utils/functools.py +++ b/salt/utils/functools.py @@ -61,6 +61,9 @@ def namespaced_function(function, global_dict, defaults=None, preserve_context=N closure=function.__closure__, ) new_namespaced_function.__dict__.update(function.__dict__) + if function.__kwdefaults__ is not None: + # Only Py 3.13+ accept this in FunctionType.__new__ + new_namespaced_function.__kwdefaults__ = function.__kwdefaults__.copy() return new_namespaced_function @@ -76,6 +79,9 @@ def alias_function(fun, name, doc=None): fun.__closure__, ) alias_fun.__dict__.update(fun.__dict__) + if fun.__kwdefaults__ is not None: + # Only Py 3.13+ accept this in FunctionType.__new__ + alias_fun.__kwdefaults__ = fun.__kwdefaults__.copy() if doc and isinstance(doc, str): alias_fun.__doc__ = doc diff --git a/tests/pytests/functional/utils/functools/test_alias_function.py b/tests/pytests/functional/utils/functools/test_alias_function.py new file mode 100644 index 000000000000..80647f72adee --- /dev/null +++ b/tests/pytests/functional/utils/functools/test_alias_function.py @@ -0,0 +1,11 @@ +from salt.utils.functools import alias_function + + +def test_kwarg_defaults_preserved(): + def func(_arg, *, default="foo"): + return default + + func2 = alias_function(func, "func2") + + assert func(None) == "foo" + assert func2(None) == "foo" # pylint: disable=not-callable diff --git a/tests/pytests/functional/utils/functools/test_namespaced_function.py b/tests/pytests/functional/utils/functools/test_namespaced_function.py index 283a722a87c7..f423f1393250 100644 --- a/tests/pytests/functional/utils/functools/test_namespaced_function.py +++ b/tests/pytests/functional/utils/functools/test_namespaced_function.py @@ -130,3 +130,13 @@ def foo(): "for removal in 3008.0 (Argon) and no longer does anything for the function " "being namespaced." ) + + +def test_kwarg_defaults_preserved(): + def func(_arg, *, default="foo"): + return default + + func2 = namespaced_function(func, globals()) + + assert func(None) == "foo" + assert func2(None) == "foo" # pylint: disable=not-callable