Reputation: 20329
I am currently working on a command line interface with argparse
. I receive the parsed arguments as a Namespace
object from arg_parse
. Basically the arguments coming in form the CLI should be passed down to 2 functions. Now I am looking for a smart way to split the Namespace
such that I can simply use unpacking.
The following example should illustrate what I would need:
import argparse
ns1 = argparse.Namespace(foo=1)
ns2 = argparse.Namespace(bar=2)
ns3 = argparse.Namespace(foo=1, bar=2)
def f(foo): return foo
def g(bar): return 10 * bar
f(**vars(ns1)) # works
g(**vars(ns2)) # works
f(**vars(ns3)) # does not work, 'bar' is not an argument of f
I could hard code the filter like in
f_args = ["foo"]
g_args = ["bar"]
f(**{k:v for k, v in vars(ns3).items() if k in f_args}) # works
g(**{k:v for k, v in vars(ns3).items() if k in g_args}) # works
But this feels error prone (if the signature of f
will change, I have to remember to change f_args
as well.). I could however use inspect.getfullargspec
to automatize this part like in:
import inspect
f_args = inspect.getfullargspec(f).args
But this all feels hackish to me and I was wondering whether I am not overlooking an easy pattern for this?
Update
As pointed out by @doer_uvc one approach would be to treat my functions with a "catch all" parameter like in:
def f(foo, **kwargs): return foo
While this is a viable solution, I would be curious to find solutions where I do not need to touch the signature of f
itself.
Upvotes: 1
Views: 190
Reputation: 1395
If writing functions are within your scope then make your functions accept any number of keyword arguments and check for arguments of interest within function.
class MissingArgumentError(ValueError):
pass
def f(**kwargs):
if not 'foo' in kwargs:
raise MissingArgumentError('Missing keyword argument `foo`')
return kwargs['foo']
def g(**kwargs):
if not 'bar' in kwargs:
raise MissingArgumentError('Missing keyword argument `bar`')
return 10 * kwargs['bar']
In this method, even if your function signature changes, nothing else needs to be changed.
If you have to write multiple such functions then rather than repeating logic of argument checking, you can use decorators
.
def argverifier(compulsory_args:list):
def actual_decorator(function):
def inner(**kwargs):
for arg in compulsory_args:
if arg not in kwargs:
raise MissingArgumentError(f'Missing keyword argument : {arg}')
return function(**kwargs)
return inner
return actual_decorator
@argverifier(compulsory_args=['foo'])
def f(**kwargs): return kwargs['foo']
@argverifier(compulsory_args=['bar'])
def g(**kwargs): return 10 * kwargs['bar']
If you do not wish to change signature of a function then the viable solution is to write a function that extracts arguments using inspect
module as you suggested.
from functools import partial
import inspect
def f(foo): return foo
def g(bar): return 10 * bar
def argextractor(args, kwargs):
return {k:v for k, v in kwargs.items() if k in args}
f_argextractor = partial(argextractor, inspect.getfullargspec(f).args)
g_argextractor = partial(argextractor, inspect.getfullargspec(g).args)
f(**f_argextractor(vars(ns3)))
g(**g_argextractor(vars(ns3)))
Upvotes: 1
Reputation: 50116
There is no shorthand for this. However, one can easily build this using inspect.signature
– these are not hacks, Python signatures can get very complicated and inspect
exists for just such purposes.
import inspect
def apply(call: 'Callable', kwargs: 'Dict[str, Any]'):
"""Apply all appropriate ``kwargs`` to ``call``"""
call_kwargs = inspect.signature(call).parameters.keys()
matching_kwargs = {name: kwargs[name] for name in kwargs.keys() & call_kwargs}
return call(**matching_kwargs)
This inspects the parameters expected by call
, and picks those supplied by kwargs
. If kwargs
is missing any parameters to call
, the standard exception of missing parameters is thrown.
>>> def f(foo):
... return foo
...
>>> apply(f, {'foo': 2, 'bar': 3})
2
>>> apply(f, {'baz': 2, 'bar': 3})
…
TypeError: f() missing 1 required positional argument: 'foo'
Upvotes: 2