From d7d0298984950039bda4c04e84c542b371ce8669 Mon Sep 17 00:00:00 2001 From: Pieter Eendebak Date: Fri, 28 Nov 2025 15:55:03 +0100 Subject: [PATCH 1/9] Improve performance of is_function --- src/qcodes/utils/function_helpers.py | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/src/qcodes/utils/function_helpers.py b/src/qcodes/utils/function_helpers.py index 1fca19de39ed..14488d12cba0 100644 --- a/src/qcodes/utils/function_helpers.py +++ b/src/qcodes/utils/function_helpers.py @@ -1,5 +1,4 @@ from asyncio import iscoroutinefunction -from inspect import signature def is_function(f: object, arg_count: int, coroutine: bool = False) -> bool: @@ -29,16 +28,4 @@ def is_function(f: object, arg_count: int, coroutine: bool = False) -> bool: # otherwise the user should make an explicit function. return arg_count == 1 - try: - sig = signature(f) - except ValueError: - # some built-in functions/methods don't describe themselves to inspect - # we already know it's a callable and coroutine is correct. - return True - - try: - inputs = [0] * arg_count - sig.bind(*inputs) - return True - except TypeError: - return False + return arg_count == (f.__code__.co_argcount) From 41b99566bdda653b52a352033c99d88e26c2c9dd Mon Sep 17 00:00:00 2001 From: Pieter Eendebak Date: Fri, 28 Nov 2025 16:02:20 +0100 Subject: [PATCH 2/9] check for bound methods --- src/qcodes/utils/function_helpers.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/qcodes/utils/function_helpers.py b/src/qcodes/utils/function_helpers.py index 14488d12cba0..816690ef8d3d 100644 --- a/src/qcodes/utils/function_helpers.py +++ b/src/qcodes/utils/function_helpers.py @@ -28,4 +28,7 @@ def is_function(f: object, arg_count: int, coroutine: bool = False) -> bool: # otherwise the user should make an explicit function. return arg_count == 1 - return arg_count == (f.__code__.co_argcount) + if getattr(f, '__self__', None) is not None: + # bound method + return arg_count == f.__code__.co_argcount - 1 + return arg_count == f.__code__.co_argcount From 1660ee72439104fcb4847f31e3d3b4c4ae1784af Mon Sep 17 00:00:00 2001 From: Pieter Eendebak Date: Mon, 1 Dec 2025 11:56:45 +0100 Subject: [PATCH 3/9] handle more cases --- src/qcodes/utils/function_helpers.py | 36 ++++++++++++++++++++++++---- tests/utils/test_isfunction.py | 15 +++++++++++- 2 files changed, 46 insertions(+), 5 deletions(-) diff --git a/src/qcodes/utils/function_helpers.py b/src/qcodes/utils/function_helpers.py index 816690ef8d3d..bbc074565418 100644 --- a/src/qcodes/utils/function_helpers.py +++ b/src/qcodes/utils/function_helpers.py @@ -1,4 +1,5 @@ from asyncio import iscoroutinefunction +from inspect import signature, CO_VARARGS def is_function(f: object, arg_count: int, coroutine: bool = False) -> bool: @@ -28,7 +29,34 @@ def is_function(f: object, arg_count: int, coroutine: bool = False) -> bool: # otherwise the user should make an explicit function. return arg_count == 1 - if getattr(f, '__self__', None) is not None: - # bound method - return arg_count == f.__code__.co_argcount - 1 - return arg_count == f.__code__.co_argcount + if (func_code := getattr(f, '__code__', None)): + # handle objects like functools.partial(f, ...) + ndefaults = len(f.__defaults__) if f.__defaults__ is not None else 0 + + if func_code.co_flags & CO_VARARGS: + # we have *args + return True + + if getattr(f, '__self__', None) is not None: + # bound method + min_positional = func_code.co_argcount - 1 - ndefaults + max_positional = func_code.co_argcount - 1 + else: + min_positional = func_code.co_argcount - ndefaults + max_positional = func_code.co_argcount + ev = min_positional <= arg_count <= max_positional + return ev + + try: + sig = signature(f) + except ValueError: + # some built-in functions/methods don't describe themselves to inspect + # we already know it's a callable and coroutine is correct. + return True + + try: + inputs = [0] * arg_count + sig.bind(*inputs) + return True + except TypeError: + return False diff --git a/tests/utils/test_isfunction.py b/tests/utils/test_isfunction.py index fc2a3dc368be..dd693496ccd6 100644 --- a/tests/utils/test_isfunction.py +++ b/tests/utils/test_isfunction.py @@ -1,5 +1,5 @@ from typing import NoReturn - +from functools import partial import pytest from qcodes.utils import is_function @@ -35,6 +35,19 @@ def f2(a: object, b: object) -> NoReturn: with pytest.raises(TypeError): is_function(f0, -1) +def test_function_partial() -> None: + def f0(one_arg : int) -> int: + return one_arg + f = partial(f0, 1) + assert is_function(f, 0) + assert not is_function(f, 1) + +def test_function_varargs() -> None: + def f(*args) -> int: + return None + assert is_function(f, 0) + assert is_function(f, 1) + assert is_function(f, 100) class AClass: def method_a(self) -> NoReturn: From 5913406bf28b9863c6c05570a2bff681121b69d7 Mon Sep 17 00:00:00 2001 From: Pieter Eendebak Date: Mon, 1 Dec 2025 15:20:29 +0100 Subject: [PATCH 4/9] mypy --- src/qcodes/utils/function_helpers.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/qcodes/utils/function_helpers.py b/src/qcodes/utils/function_helpers.py index bbc074565418..0424993bef98 100644 --- a/src/qcodes/utils/function_helpers.py +++ b/src/qcodes/utils/function_helpers.py @@ -31,7 +31,8 @@ def is_function(f: object, arg_count: int, coroutine: bool = False) -> bool: if (func_code := getattr(f, '__code__', None)): # handle objects like functools.partial(f, ...) - ndefaults = len(f.__defaults__) if f.__defaults__ is not None else 0 + func_defaults = getattr(f, '__defaults__', None) + number_of_defaults = len(func_defaults) if func_defaults is not None else 0 if func_code.co_flags & CO_VARARGS: # we have *args @@ -39,10 +40,10 @@ def is_function(f: object, arg_count: int, coroutine: bool = False) -> bool: if getattr(f, '__self__', None) is not None: # bound method - min_positional = func_code.co_argcount - 1 - ndefaults + min_positional = func_code.co_argcount - 1 - number_of_defaults max_positional = func_code.co_argcount - 1 else: - min_positional = func_code.co_argcount - ndefaults + min_positional = func_code.co_argcount - number_of_defaults max_positional = func_code.co_argcount ev = min_positional <= arg_count <= max_positional return ev From 95a74b06b721af284c387cd9a23d68e7604ea862 Mon Sep 17 00:00:00 2001 From: Pieter Eendebak Date: Tue, 2 Dec 2025 08:51:54 +0100 Subject: [PATCH 5/9] more tests --- src/qcodes/utils/function_helpers.py | 11 ++++++----- tests/utils/test_isfunction.py | 10 +++++++++- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/src/qcodes/utils/function_helpers.py b/src/qcodes/utils/function_helpers.py index 0424993bef98..981e1d433f37 100644 --- a/src/qcodes/utils/function_helpers.py +++ b/src/qcodes/utils/function_helpers.py @@ -34,17 +34,18 @@ def is_function(f: object, arg_count: int, coroutine: bool = False) -> bool: func_defaults = getattr(f, '__defaults__', None) number_of_defaults = len(func_defaults) if func_defaults is not None else 0 - if func_code.co_flags & CO_VARARGS: - # we have *args - return True - if getattr(f, '__self__', None) is not None: # bound method min_positional = func_code.co_argcount - 1 - number_of_defaults max_positional = func_code.co_argcount - 1 else: min_positional = func_code.co_argcount - number_of_defaults - max_positional = func_code.co_argcount + max_positional = func_code.co_argcount + + if func_code.co_flags & CO_VARARGS: + # we have *args + max_positional = 10e10 + ev = min_positional <= arg_count <= max_positional return ev diff --git a/tests/utils/test_isfunction.py b/tests/utils/test_isfunction.py index dd693496ccd6..f18f8af4a0fc 100644 --- a/tests/utils/test_isfunction.py +++ b/tests/utils/test_isfunction.py @@ -43,12 +43,20 @@ def f0(one_arg : int) -> int: assert not is_function(f, 1) def test_function_varargs() -> None: - def f(*args) -> int: + def f(*args) -> None: return None assert is_function(f, 0) assert is_function(f, 1) assert is_function(f, 100) + def f(a, b=1, *args) -> None: + return None + assert not is_function(f, 0) + assert is_function(f, 1) + assert is_function(f, 2) + assert is_function(f, 100) + + class AClass: def method_a(self) -> NoReturn: raise RuntimeError("function should not get called") From 9d75e07ebd27ca3c1b4f8b75aac185afb89a37bc Mon Sep 17 00:00:00 2001 From: Pieter Eendebak Date: Tue, 2 Dec 2025 08:52:19 +0100 Subject: [PATCH 6/9] more tests --- src/qcodes/utils/function_helpers.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/qcodes/utils/function_helpers.py b/src/qcodes/utils/function_helpers.py index 981e1d433f37..89c39554df07 100644 --- a/src/qcodes/utils/function_helpers.py +++ b/src/qcodes/utils/function_helpers.py @@ -1,5 +1,5 @@ from asyncio import iscoroutinefunction -from inspect import signature, CO_VARARGS +from inspect import CO_VARARGS, signature def is_function(f: object, arg_count: int, coroutine: bool = False) -> bool: @@ -37,7 +37,7 @@ def is_function(f: object, arg_count: int, coroutine: bool = False) -> bool: if getattr(f, '__self__', None) is not None: # bound method min_positional = func_code.co_argcount - 1 - number_of_defaults - max_positional = func_code.co_argcount - 1 + max_positional = func_code.co_argcount - 1 else: min_positional = func_code.co_argcount - number_of_defaults max_positional = func_code.co_argcount @@ -45,10 +45,10 @@ def is_function(f: object, arg_count: int, coroutine: bool = False) -> bool: if func_code.co_flags & CO_VARARGS: # we have *args max_positional = 10e10 - + ev = min_positional <= arg_count <= max_positional return ev - + try: sig = signature(f) except ValueError: From 674bfaa7a5e5ec806d8ddaf6a0126c8440926efe Mon Sep 17 00:00:00 2001 From: Pieter Eendebak Date: Tue, 2 Dec 2025 16:46:25 +0100 Subject: [PATCH 7/9] ci --- src/qcodes/utils/function_helpers.py | 12 ++++++------ tests/utils/test_isfunction.py | 17 +++++++++++------ 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/src/qcodes/utils/function_helpers.py b/src/qcodes/utils/function_helpers.py index 89c39554df07..d754646a7ef6 100644 --- a/src/qcodes/utils/function_helpers.py +++ b/src/qcodes/utils/function_helpers.py @@ -29,22 +29,22 @@ def is_function(f: object, arg_count: int, coroutine: bool = False) -> bool: # otherwise the user should make an explicit function. return arg_count == 1 - if (func_code := getattr(f, '__code__', None)): + if func_code := getattr(f, "__code__", None): # handle objects like functools.partial(f, ...) - func_defaults = getattr(f, '__defaults__', None) + func_defaults = getattr(f, "__defaults__", None) number_of_defaults = len(func_defaults) if func_defaults is not None else 0 - if getattr(f, '__self__', None) is not None: + if getattr(f, "__self__", None) is not None: # bound method min_positional = func_code.co_argcount - 1 - number_of_defaults max_positional = func_code.co_argcount - 1 else: - min_positional = func_code.co_argcount - number_of_defaults + min_positional = func_code.co_argcount - number_of_defaults max_positional = func_code.co_argcount if func_code.co_flags & CO_VARARGS: - # we have *args - max_positional = 10e10 + # we have *args + max_positional = 10e10 ev = min_positional <= arg_count <= max_positional return ev diff --git a/tests/utils/test_isfunction.py b/tests/utils/test_isfunction.py index f18f8af4a0fc..0745d16b4be6 100644 --- a/tests/utils/test_isfunction.py +++ b/tests/utils/test_isfunction.py @@ -35,26 +35,31 @@ def f2(a: object, b: object) -> NoReturn: with pytest.raises(TypeError): is_function(f0, -1) + def test_function_partial() -> None: - def f0(one_arg : int) -> int: + def f0(one_arg: int) -> int: return one_arg + f = partial(f0, 1) assert is_function(f, 0) assert not is_function(f, 1) + def test_function_varargs() -> None: def f(*args) -> None: return None + assert is_function(f, 0) assert is_function(f, 1) assert is_function(f, 100) - def f(a, b=1, *args) -> None: + def g(a, b=1, *args) -> None: return None - assert not is_function(f, 0) - assert is_function(f, 1) - assert is_function(f, 2) - assert is_function(f, 100) + + assert not is_function(g, 0) + assert is_function(g, 1) + assert is_function(g, 2) + assert is_function(g, 100) class AClass: From ae577b1e3f7bf83bfd3594dce1bd92135b4cfb83 Mon Sep 17 00:00:00 2001 From: Pieter Eendebak Date: Wed, 10 Dec 2025 13:55:50 +0100 Subject: [PATCH 8/9] isort-a --- tests/utils/test_isfunction.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/utils/test_isfunction.py b/tests/utils/test_isfunction.py index 0745d16b4be6..8da545c8db79 100644 --- a/tests/utils/test_isfunction.py +++ b/tests/utils/test_isfunction.py @@ -1,5 +1,6 @@ -from typing import NoReturn from functools import partial +from typing import NoReturn + import pytest from qcodes.utils import is_function From f920bafbb8841e07b1ec1c39da8290f01b1e9ec3 Mon Sep 17 00:00:00 2001 From: Pieter Eendebak Date: Sun, 28 Dec 2025 22:25:56 +0100 Subject: [PATCH 9/9] trigger