Skip to content

Commit cbbde51

Browse files
committed
Add optional printing of function arguments
1 parent a12fee5 commit cbbde51

File tree

3 files changed

+612
-8
lines changed

3 files changed

+612
-8
lines changed

pyproject.toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,10 @@ select = [
5858
]
5959

6060
[dependency-groups]
61-
dev = ["ruff>=0.11.2"]
61+
dev = [
62+
"ipykernel>=6.29.5",
63+
"ruff>=0.11.2",
64+
]
6265
docs = [
6366
"matplotlib>=3.10.1",
6467
"numpydoc>=1.8.0",

src/stopuhr/funkuhr.py

Lines changed: 161 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,36 @@
11
"""Very high level benchmarking decorator."""
22

3-
from typing import TYPE_CHECKING
3+
from collections.abc import Callable
4+
from inspect import Signature, signature
5+
from typing import TYPE_CHECKING, Any, Literal
46

57
from stopuhr.stopuhr import StopUhr, stopuhr
68

79
if TYPE_CHECKING:
810
import pandas as pd
911

1012

13+
def _get_bound_args(sig: Signature, *args, **kwargs) -> dict[str, Any]:
14+
# Create a dictionary to store parameter bindings
15+
bound_args = {}
16+
17+
# Bind positional arguments
18+
for arg_name, arg_value in zip(sig.parameters.keys(), args):
19+
bound_args[arg_name] = arg_value
20+
21+
# Add keyword arguments
22+
for key, value in kwargs.items():
23+
if key in sig.parameters:
24+
bound_args[key] = value
25+
26+
# Fill in default values for missing parameters
27+
for param_name, param in sig.parameters.items():
28+
if param_name not in bound_args and param.default != param.empty:
29+
bound_args[param_name] = param.default
30+
31+
return bound_args
32+
33+
1134
class FunkUhr:
1235
"""Very high level benchmarking decorator.
1336
@@ -33,6 +56,40 @@ class FunkUhr:
3356
>>> funkuhr.summary()
3457
Busy Function took 0.10 ± 0.00 s (n=5 -> total=0.50s)
3558
59+
Like the stateless decorator, it is possible to add arguments to the message.
60+
61+
.. code-block:: python
62+
>>> from stopuhr import FunkUhr
63+
64+
>>> funkuhr = FunkUhr()
65+
66+
>>> @funkuhr("Busy Function", print_kwargs=["arg1", "arg2"])
67+
>>> def busy_function(arg1, arg2, arg3):
68+
>>> time.sleep(0.1)
69+
70+
>>> for i in range(5):
71+
>>> busy_function(1, 2, 3)
72+
73+
>>> funkuhr.summary()
74+
Busy Function took 0.10 ± 0.00 s (n=5 -> total=0.50s) (with arg1=1, arg2=2)
75+
76+
It is also possible to add all arguments to the message.
77+
78+
.. code-block:: python
79+
>>> from stopuhr import FunkUhr
80+
81+
>>> funkuhr = FunkUhr()
82+
83+
>>> @funkuhr("Busy Function", print_kwargs=True)
84+
>>> def busy_function(arg1, arg2):
85+
>>> time.sleep(0.1)
86+
87+
>>> for i in range(5):
88+
>>> busy_function(1, 2)
89+
90+
>>> funkuhr.summary()
91+
Busy Function took 0.10 ± 0.00 s (n=5 -> total=0.50s) (with arg1=1, arg2=2)
92+
3693
"""
3794

3895
def __init__(self, printer: callable = print):
@@ -62,27 +119,69 @@ def summary(self, res: int = 2):
62119
"""
63120
self.stopuhr.summary(res)
64121

65-
def __call__(self, key: str, res: int = 2, log: bool = True):
122+
def __call__(
123+
self,
124+
key: str,
125+
res: int = 2,
126+
log: bool = True,
127+
print_kwargs: list[str] | Literal["all"] | None = None,
128+
):
66129
"""Decorate a function to measure the time taken in a block.
67130
68131
Args:
69132
key (str): The key to store the duration under.
70133
res (int, optional): The number of decimal places to round to. Defaults to 2.
71134
log (bool, optional): Whether to log the duration. Defaults to True.
135+
print_kwargs (list[str] | bool, optional): The arguments to be added to the `key`.
136+
If a list, only the arguments in the list will be added to the message.
137+
If True, all arguments will added. If False, no arguments will be added.
138+
Additions to the message will have the form: f"{key} (with {arg1=val1, arg2=val2, ...})".
139+
Defaults to False.
140+
141+
Raises:
142+
ValueError: If any of the print_kwargs are not in the functions signature.
72143
73144
"""
74145

75146
def _decorator(func):
147+
func_signature = signature(func)
148+
# Check if any of the print_kwargs are not in the functions signature and raise an error if so
149+
if isinstance(print_kwargs, list) and any(
150+
k not in func_signature.parameters for k in print_kwargs if isinstance(print_kwargs, list)
151+
):
152+
raise ValueError(
153+
f"Not all {print_kwargs=} found in {func_signature.parameters=} of function {func.__name__}"
154+
)
155+
76156
def _inner(*args, **kwargs):
77-
with self.stopuhr(key, res=res, log=log):
157+
_inner_key = key
158+
159+
if isinstance(print_kwargs, list):
160+
bound_args = _get_bound_args(func_signature, *args, **kwargs)
161+
# Check if any of the print_kwargs are not in bound_args and raise an error if so
162+
if any(k not in bound_args for k in print_kwargs):
163+
raise ValueError(f"Not all {print_kwargs=} found in {bound_args=} of function {func.__name__}")
164+
165+
# Filter by print_kwargs
166+
bound_args = {k: bound_args[k] for k in print_kwargs if k in bound_args}
167+
# Make a string of it and add it to the message
168+
bound_args_msg = ", ".join(f"{k}={v}" for k, v in bound_args.items())
169+
_inner_key += f" (with {bound_args_msg})"
170+
elif print_kwargs:
171+
bound_args = _get_bound_args(func_signature, *args, **kwargs)
172+
# Make a string of it and add it to the message
173+
bound_args_msg = ", ".join(f"{k}={v}" for k, v in bound_args.items())
174+
_inner_key += f" (with {bound_args_msg})"
175+
176+
with self.stopuhr(_inner_key, res=res, log=log):
78177
return func(*args, **kwargs)
79178

80179
return _inner
81180

82181
return _decorator
83182

84183

85-
def funkuhr(msg: str, printer: callable = print, res: int = 2):
184+
def funkuhr(msg: str, printer: callable = print, res: int = 2, print_kwargs: list[str] | bool = False):
86185
"""Decorate a function to measure the time taken in a block.
87186
88187
Wraps the `stopuhr` function to provide a decorator for benchmarking functions.
@@ -99,17 +198,73 @@ def funkuhr(msg: str, printer: callable = print, res: int = 2):
99198
>>> busy_function()
100199
Busy Function took 0.20s
101200
201+
It is possible to add arguments to the message.
202+
203+
.. code-block:: python
204+
>>> from stopuhr import funkuhr
205+
206+
>>> @funkuhr("Busy Function", print_kwargs=["arg1", "arg2"])
207+
>>> def busy_function(arg1, arg2, arg3):
208+
>>> time.sleep(0.2)
209+
210+
>>> busy_function(1, 2, 3)
211+
Busy Function took 0.20s (with arg1=1, arg2=2)
212+
213+
It is also possible to add all arguments to the message.
214+
215+
.. code-block:: python
216+
>>> from stopuhr import funkuhr
217+
218+
>>> @funkuhr("Busy Function", print_kwargs=True)
219+
>>> def busy_function(arg1, arg2):
220+
>>> time.sleep(0.2)
221+
222+
>>> busy_function(1, 2)
223+
Busy Function took 0.20s (with arg1=1, arg2=2)
224+
102225
Args:
103226
func (callable): The function to decorate.
104227
msg (str): The message to print.
105228
printer (callable, optional): The function to print with. Defaults to print.
106229
res (int, optional): The number of decimal places to round to. Defaults to 2.
230+
print_kwargs (list[str] | bool, optional): The arguments to be added to the `msg`.
231+
If a list, only the arguments in the list will be added to the message.
232+
If True, all arguments will added. If False, no arguments will be added.
233+
Additions to the message will have the form: f"{msg} (with {arg1=val1, arg2=val2, ...})".
234+
Defaults to False.
235+
236+
Raises:
237+
ValueError: If any of the print_kwargs are not in the functions signature.
107238
108239
"""
109240

110-
def _decorator(func):
241+
def _decorator(func: Callable):
242+
func_signature = signature(func)
243+
# Check if any of the print_kwargs are not in the functions signature and raise an error if so
244+
if isinstance(print_kwargs, list) and any(
245+
k not in func_signature.parameters for k in print_kwargs if isinstance(print_kwargs, list)
246+
):
247+
raise ValueError(
248+
f"Not all {print_kwargs=} found in {func_signature.parameters=} of function {func.__name__}"
249+
)
250+
111251
def _inner(*args, **kwargs):
112-
with stopuhr(msg, printer, res):
252+
_inner_msg = msg
253+
254+
if isinstance(print_kwargs, list):
255+
bound_args = _get_bound_args(func_signature, *args, **kwargs)
256+
# Filter by print_kwargs
257+
bound_args = {k: bound_args[k] for k in print_kwargs if k in bound_args}
258+
# Make a string of it and add it to the message
259+
bound_args_msg = ", ".join(f"{k}={v}" for k, v in bound_args.items())
260+
_inner_msg += f" (with {bound_args_msg})"
261+
elif print_kwargs: # True was passed
262+
bound_args = _get_bound_args(func_signature, *args, **kwargs)
263+
# Make a string of it and add it to the message
264+
bound_args_msg = ", ".join(f"{k}={v}" for k, v in bound_args.items())
265+
_inner_msg += f" (with {bound_args_msg})"
266+
267+
with stopuhr(_inner_msg, printer, res):
113268
return func(*args, **kwargs)
114269

115270
return _inner

0 commit comments

Comments
 (0)