Source code for rex.utilities.fun_utils

# -*- coding: utf-8 -*-
"""
Utilities for parsing function signatures to make easy CLI's or to call
functions programmatically
"""
import inspect
from inspect import signature
import logging

logger = logging.getLogger(__name__)


[docs]def arg_to_str(arg): """Format input as str w/ appropriate quote types for python call Returns -------- out : str String rep of the input arg with proper quotation for formatting in python -c '{commands}' cli calls. For example, int, float, None -> '0' str -> \"string\" """ if isinstance(arg, str): return f'"{arg}"' else: return f'{arg}'
[docs]def has_class(obj): """Determine whether an object is a method that is bound to a class Returns ------- out : bool Whether the input object belongs to a class. For example, MyClass.bound_method will return True, whereas standalone_fun will return False. """ if hasattr(obj, '__qualname__'): if len(obj.__qualname__.split('.')) > 1: return True else: return False
[docs]def get_class(obj): """Get the name of the class that the method object is bound to. Returns an empty string if the method object is not bound to a class. Returns ------- out : str The class name the input object method belongs to. For example, MyClass.bound_method will return "MyClass", whereas standalone_fun will return an empty string. """ class_name = '' if has_class(obj): class_name = obj.__qualname__.split('.')[0] return class_name
[docs]def is_standalone_fun(obj): """Determine whether an object is a standalone function without a class Returns ------- out : bool Whether the input object is a standalone function. For example, MyClass.bound_method will return False, whereas standalone_fun will return True. """ return inspect.isfunction(obj) and not has_class(obj)
[docs]def get_fun_str(fun): """Get the function string from a function object including the ClassName.function if the function is bound Returns ------- out : str The function string to call the input function. For example MyClass.bound_method will return "MyClass.bound_method", whereas standalone_fun will return "standalone_fun". """ fun_name = fun.__name__ if is_standalone_fun(fun): return fun_name elif has_class(fun): class_name = get_class(fun) return f'{class_name}.{fun_name}' else: return fun_name
[docs]def get_arg_str(fun, config): """Get a string representation of positional and keyword arguments required by an input function and provided in the config dictionary. Example ------- If the function signature is my_fun(a, b, c=0) and config is {'a': 1, 'b': 2, 'c': 3}, the returned arg_str will be "1, 2, c=3". The function can also take *args or **kwargs, which will be taken from the "args" and "kwargs" keys in the config. "args" must be mapped to a list, and "kwargs" must be mapped to a dictionary. Parameters ---------- fun : obj A callable object with a function signature. The function signature will be parsed for args and kwargs which will be taken from the config. config : dict A namespace of arguments to run fun. Not all entries in config may be used, but all required inputs to fun must be provided in config. Can include "args" and "kwargs" which must be mapped to a list and a dictionary, respectively. Returns ------- arg_str : str Argument string that can be used to call fun programmatically, e.g. fun(arg_str) """ sig = signature(fun) arg_strs = [] for arg_name, value in sig.parameters.items(): is_self = arg_name == 'self' is_kw = value.default != value.empty is_star_arg = str(value).startswith('*') and str(value).count('*') == 1 is_star_kwa = str(value).startswith('*') and str(value).count('*') == 2 not_required = (is_self or is_kw or is_star_arg or is_star_kwa) required = not not_required if arg_name in config: if not is_kw and not (is_star_arg or is_star_kwa): arg = arg_to_str(config[arg_name]) arg_strs.append(f'{arg}') elif is_kw and not (is_star_arg or is_star_kwa): arg = arg_to_str(config[arg_name]) arg_strs.append(f'{arg_name}={arg}') elif is_star_arg: msg = '"args" key in config must be mapped to a list!' assert isinstance(config[arg_name], (list, tuple)), msg arg_strs += [f'{arg_to_str(star_arg)}' for star_arg in config[arg_name]] elif is_star_kwa: msg = '"kwargs" key in config must be mapped to a dict!' assert isinstance(config[arg_name], dict), msg arg_strs += [f'{star_name}={arg_to_str(star_kw)}' for star_name, star_kw in config[arg_name].items()] elif required: msg = (f'Positional argument "{arg_name}" ' 'needs to be defined in config!') logger.error(msg) raise KeyError(msg) arg_str = ', '.join(arg_strs) return arg_str
[docs]def get_fun_call_str(fun, config, quote_char='\"'): """Get a string that will call a function using args and kwargs from a generic config. Example ------- If the function signature is my_fun(a, b, c=0) and config is {'a': 1, 'b': 2, 'c': 3}, the returned call string will be "my_fun(1, 2, c=3)". The function can also take *args or **kwargs, which will be taken from the "args" and "kwargs" keys in the config. "args" must be mapped to a list, and "kwargs" must be mapped to a dictionary. Parameters ---------- fun : obj A callable object with a function signature. The function signature will be parsed for args and kwargs which will be taken from the config. config : dict A namespace of arguments to run fun. Not all entries in config may be used, but all required inputs to fun must be provided in config. Can include "args" and "kwargs" which must be mapped to a list and a dictionary, respectively. quote_char : str Character to use for string quotes in the fun_call_str output. Returns ------- fun_call_str : str A string representation of a function call e.g. "fun(arg1, arg2, kw1=kw1)" where arg1, arg2, and kw1 were found in the config. """ fun_str = get_fun_str(fun) arg_str = get_arg_str(fun, config) call_str = f'{fun_str}({arg_str})' if quote_char: call_str = call_str.replace('"', quote_char) call_str = call_str.replace("'", quote_char) call_str = call_str.replace("/'", quote_char) call_str = call_str.replace('/"', quote_char) return call_str