From 57f6b2a97ac467d8d825bbea12b326bc9f2f6957 Mon Sep 17 00:00:00 2001 From: Samaresh Kumar Singh Date: Fri, 14 Nov 2025 11:02:36 -0600 Subject: [PATCH 1/3] Fix callable detection to support all callable objects (functools.partial, etc) Fixes #2008 The old implementation checked for __code__ attribute directly, which doesn't exist for callable objects like functools.partial. This caused an AttributeError when trying to use such objects with find_max_global and find_min_global functions. Changed to use Python's inspect.signature() which properly handles all callable objects including: - Regular functions - Lambda functions - functools.partial objects - Class methods - Any object with __call__ method The fix includes a fallback to the old __code__ method for backward compatibility in case inspect.signature fails for any reason. --- test_callable_fix.py | 46 ++++++++++++++++++++++++ tools/python/src/global_optimization.cpp | 41 ++++++++++++++++++--- 2 files changed, 82 insertions(+), 5 deletions(-) create mode 100644 test_callable_fix.py diff --git a/test_callable_fix.py b/test_callable_fix.py new file mode 100644 index 0000000000..8abecee607 --- /dev/null +++ b/test_callable_fix.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python3 +"""Test script to verify the fix for issue #2008""" +from functools import partial + +def test_function(x, y): + """Simple test function""" + return -(x**2 + y**2) + +def test_single_arg(x): + """Single argument function""" + return -(x**2) + +# Test with functools.partial +partial_func = partial(test_function, 2) + +print("Testing functools.partial with dlib.find_max_global...") +print(f"partial_func type: {type(partial_func)}") +print(f"partial_func callable: {callable(partial_func)}") + +# This should work after the fix +try: + import dlib + result = dlib.find_max_global(partial_func, [0.], [10.], 100) + print(f"✓ Success! Result: {result}") +except AttributeError as e: + print(f"✗ Failed with AttributeError: {e}") +except Exception as e: + print(f"✗ Failed with error: {e}") + +# Test with regular function (should still work) +print("\nTesting regular function with dlib.find_max_global...") +try: + import dlib + result = dlib.find_max_global(test_single_arg, [0.], [10.], 100) + print(f"✓ Success! Result: {result}") +except Exception as e: + print(f"✗ Failed with error: {e}") + +# Test with lambda (should still work) +print("\nTesting lambda with dlib.find_max_global...") +try: + import dlib + result = dlib.find_max_global(lambda x: -(x**2), [0.], [10.], 100) + print(f"✓ Success! Result: {result}") +except Exception as e: + print(f"✗ Failed with error: {e}") diff --git a/tools/python/src/global_optimization.cpp b/tools/python/src/global_optimization.cpp index beb0f0afd1..20d90fd373 100644 --- a/tools/python/src/global_optimization.cpp +++ b/tools/python/src/global_optimization.cpp @@ -48,11 +48,42 @@ py::list mat_to_list ( size_t num_function_arguments(py::object f, size_t expected_num) { - const auto code_object = f.attr(hasattr(f,"func_code") ? "func_code" : "__code__"); - const auto num = code_object.attr("co_argcount").cast(); - if (num < expected_num && (code_object.attr("co_flags").cast() & CO_VARARGS)) - return expected_num; - return num; + // Use Python's inspect module to get signature, which works with all callable objects + // including functools.partial, lambdas, and regular functions + auto inspect = py::module_::import("inspect"); + + try { + auto sig = inspect.attr("signature")(f); + auto params = sig.attr("parameters"); + auto num = py::len(params); + + // Check if function accepts *args (VAR_POSITIONAL) + bool has_var_args = false; + for (auto item : params) { + auto param = item.second.cast(); + auto kind = param.attr("kind"); + // inspect.Parameter.VAR_POSITIONAL == 2 + if (kind.cast() == 2) { + has_var_args = true; + break; + } + } + + if (num < expected_num && has_var_args) + return expected_num; + return num; + } catch (const py::error_already_set&) { + // Fallback to old method if inspect.signature fails + // This maintains backward compatibility + if (!hasattr(f, "__code__") && !hasattr(f, "func_code")) { + throw; + } + const auto code_object = f.attr(hasattr(f,"func_code") ? "func_code" : "__code__"); + const auto num = code_object.attr("co_argcount").cast(); + if (num < expected_num && (code_object.attr("co_flags").cast() & CO_VARARGS)) + return expected_num; + return num; + } } double call_func(py::object f, const matrix& args) From 5bf8fd3e00fc686504bd2b8003fbcb46dfce12c8 Mon Sep 17 00:00:00 2001 From: Samaresh Kumar Singh Date: Fri, 14 Nov 2025 15:47:33 -0600 Subject: [PATCH 2/3] =?UTF-8?q?Update=20the=20code=20to=20loop=20through?= =?UTF-8?q?=20params.values()=20instead=20of=20params=20itself,=20since=20?= =?UTF-8?q?parameters=20is=20a=20dictionary-like=20object.=20The=20earlier?= =?UTF-8?q?=20code=20tried=20to=20access=20.second=20on=20the=20iterator,?= =?UTF-8?q?=20which=20isn=E2=80=99t=20valid=20when=20working=20with=20dict?= =?UTF-8?q?=20values?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Samaresh Kumar Singh --- tools/python/src/global_optimization.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/python/src/global_optimization.cpp b/tools/python/src/global_optimization.cpp index 20d90fd373..935079d382 100644 --- a/tools/python/src/global_optimization.cpp +++ b/tools/python/src/global_optimization.cpp @@ -59,8 +59,8 @@ size_t num_function_arguments(py::object f, size_t expected_num) // Check if function accepts *args (VAR_POSITIONAL) bool has_var_args = false; - for (auto item : params) { - auto param = item.second.cast(); + for (auto item : params.attr("values")()) { + auto param = item.cast(); auto kind = param.attr("kind"); // inspect.Parameter.VAR_POSITIONAL == 2 if (kind.cast() == 2) { From 87484360fd65ef9968163d3e665994fafe2b57c1 Mon Sep 17 00:00:00 2001 From: Samaresh Kumar Singh Date: Sat, 15 Nov 2025 19:07:47 -0600 Subject: [PATCH 3/3] Fix parameter counting to exclude *args and **kwargs The previous code was counting VAR_POSITIONAL (*args) and VAR_KEYWORD (**kwargs) parameters in the total count, which caused functions like 'lambda a, b, c, *args' to be counted as having 4 parameters instead of 3. Now only regular parameters are counted, while still checking for the presence of *args to allow variable argument functions. Also removed test_callable_fix.py as it was causing pytest errors. Signed-off-by: Samaresh Kumar Singh --- test_callable_fix.py | 46 ------------------------ tools/python/src/global_optimization.cpp | 19 +++++----- 2 files changed, 11 insertions(+), 54 deletions(-) delete mode 100644 test_callable_fix.py diff --git a/test_callable_fix.py b/test_callable_fix.py deleted file mode 100644 index 8abecee607..0000000000 --- a/test_callable_fix.py +++ /dev/null @@ -1,46 +0,0 @@ -#!/usr/bin/env python3 -"""Test script to verify the fix for issue #2008""" -from functools import partial - -def test_function(x, y): - """Simple test function""" - return -(x**2 + y**2) - -def test_single_arg(x): - """Single argument function""" - return -(x**2) - -# Test with functools.partial -partial_func = partial(test_function, 2) - -print("Testing functools.partial with dlib.find_max_global...") -print(f"partial_func type: {type(partial_func)}") -print(f"partial_func callable: {callable(partial_func)}") - -# This should work after the fix -try: - import dlib - result = dlib.find_max_global(partial_func, [0.], [10.], 100) - print(f"✓ Success! Result: {result}") -except AttributeError as e: - print(f"✗ Failed with AttributeError: {e}") -except Exception as e: - print(f"✗ Failed with error: {e}") - -# Test with regular function (should still work) -print("\nTesting regular function with dlib.find_max_global...") -try: - import dlib - result = dlib.find_max_global(test_single_arg, [0.], [10.], 100) - print(f"✓ Success! Result: {result}") -except Exception as e: - print(f"✗ Failed with error: {e}") - -# Test with lambda (should still work) -print("\nTesting lambda with dlib.find_max_global...") -try: - import dlib - result = dlib.find_max_global(lambda x: -(x**2), [0.], [10.], 100) - print(f"✓ Success! Result: {result}") -except Exception as e: - print(f"✗ Failed with error: {e}") diff --git a/tools/python/src/global_optimization.cpp b/tools/python/src/global_optimization.cpp index 935079d382..803c650f70 100644 --- a/tools/python/src/global_optimization.cpp +++ b/tools/python/src/global_optimization.cpp @@ -55,23 +55,26 @@ size_t num_function_arguments(py::object f, size_t expected_num) try { auto sig = inspect.attr("signature")(f); auto params = sig.attr("parameters"); - auto num = py::len(params); - // Check if function accepts *args (VAR_POSITIONAL) + // Count only regular parameters, excluding VAR_POSITIONAL (*args) and VAR_KEYWORD (**kwargs) + size_t num_regular_params = 0; bool has_var_args = false; + for (auto item : params.attr("values")()) { auto param = item.cast(); - auto kind = param.attr("kind"); - // inspect.Parameter.VAR_POSITIONAL == 2 - if (kind.cast() == 2) { + auto kind = param.attr("kind").cast(); + // inspect.Parameter.VAR_POSITIONAL == 2, VAR_KEYWORD == 4 + if (kind == 2) { has_var_args = true; - break; + } else if (kind != 4) { + // Count all parameters except VAR_POSITIONAL and VAR_KEYWORD + num_regular_params++; } } - if (num < expected_num && has_var_args) + if (num_regular_params < expected_num && has_var_args) return expected_num; - return num; + return num_regular_params; } catch (const py::error_already_set&) { // Fallback to old method if inspect.signature fails // This maintains backward compatibility