I want to validate (at runtime, this is not a typing question), that a function passed as an argument takes only 1 positional variable (basically that function will be called with a string as input and returns a truthy).
Naively, this is what I had:
def check(v_in : Callable):
"""check that the function can be called with 1 positional parameter supplied"""
sig = signature(v_in)
len_param = len(sig.parameters)
if not len_param == 1:
raise ValueError(
f"expecting 1 parameter, of type `str` for {v_in}. got {len_param}"
)
return v_in
If I check the following function, it is OK, which is good:
def f_ok1_1param(v : str):
pass
but the next fails, though the *args
would receive 1 param just fine and **kwargs
would be empty
def f_ok4_vargs_kwargs(*args,**kwargs):
pass
from rich import inspect as rinspect
try:
check(f_ok1_1param)
print("\n\npasses:")
rinspect(f_ok1_1param, title=False,docs=False)
except (Exception,) as e:
print("\n\nfails:")
rinspect(f_ok1_1param, title=False,docs=False)
try:
check(f_ok4_vargs_kwargs)
print("\n\npasses:")
rinspect(f_ok4_vargs_kwargs, title=False,docs=False)
except (Exception,) as e:
print("\n\nfails:")
rinspect(f_ok4_vargs_kwargs, title=False,docs=False)
which passes the first, and fails the second, instead of passing both:
passes:
╭─────────── <function f_ok1_1param at 0x1013c0f40> ───────────╮
│ def f_ok1_1param(v: str): │
│ │
│ 37 attribute(s) not shown. Run inspect(inspect) for options. │
╰──────────────────────────────────────────────────────────────╯
fails:
╭──────── <function f_ok4_vargs_kwargs at 0x101512200> ────────╮
│ def f_ok4_vargs_kwargs(*args, **kwargs): │
│ │
│ 37 attribute(s) not shown. Run inspect(inspect) for options. │
╰──────────────────────────────────────────────────────────────╯
all the different combination of signatures are defined below:
def f_ok1_1param(v : str):
pass
def f_ok2_1param(v):
pass
def f_ok3_vargs(*v):
pass
def f_ok4_p_vargs(p, *v):
pass
def f_ok4_vargs_kwargs(*args,**kwargs):
pass
def f_ok5_p_varg_kwarg(param,*args,**kwargs):
pass
def f_bad1_2params(p1, p2):
pass
def f_bad2_kwargs(**kwargs):
pass
def f_bad3_noparam():
pass
Now, I did already check a bit more about the Parameters:
rinspect(signature(f_ok4_vargs_kwargs).parameters["args"])
╭─────────────────── <class 'inspect.Parameter'> ───────────────────╮
│ Represents a parameter in a function signature. │
│ │
│ ╭───────────────────────────────────────────────────────────────╮ │
│ │ <Parameter "*args"> │ │
│ ╰───────────────────────────────────────────────────────────────╯ │
│ │
│ KEYWORD_ONLY = <_ParameterKind.KEYWORD_ONLY: 3> │
│ kind = <_ParameterKind.VAR_POSITIONAL: 2> │
│ name = 'args' │
│ POSITIONAL_ONLY = <_ParameterKind.POSITIONAL_ONLY: 0> │
│ POSITIONAL_OR_KEYWORD = <_ParameterKind.POSITIONAL_OR_KEYWORD: 1> │
│ VAR_KEYWORD = <_ParameterKind.VAR_KEYWORD: 4> │
│ VAR_POSITIONAL = <_ParameterKind.VAR_POSITIONAL: 2> │
╰───────────────────────────────────────────────────────────────────╯
I suppose checking Parameter.kind
vs _ParameterKind
enumeration of each parameter is how this needs to be approached, but I wonder if I am overthinking this or if something already exists to do this, either in inspect
or if the typing
protocol
support can be used to do, but at runtime.
Note, theoretically def f_ok_cuz_default(p, p2 = None):
would also work, but let's ignore that for now.
p.s. The motivation is providing a custom callback function in a validation framework. The call location is deep in the framework and that particular argument can also be a string (which gets converted to a regex). It can even be None. Easiest here is just to stick a def myfilter(*args,**kwargs): breakpoint
. Or myfilter(foo)
. Then look at what you get from the framework and adjust body. It’s one thing to have exceptions in your function, another for the framework to accept it but then error before calling into it. So a quick “will this work when we call it?” is more user friendly.