def lay_arguments_and_execute(callable_obj, argument_map, signature_obj):
    """Lay arguments, execute callable and return result.

    Works by building a string representing a call to the
    received callable with all its arguments laid out
    properly according to the kind of the corresponding
    parameters.

    Such string is then, executed with the builtin function
    eval() and its return value is returned from this
    function.

    Such kinds of parameters refer to whether a parameter
    is positional-only, keyword-only, both positional
    and keyword compatible, etc., for instance. They are
    listed in the following attributes of the standard
    library inspect module:

    '_POSITIONAL_ONLY',
    '_POSITIONAL_OR_KEYWORD',
    '_KEYWORD_ONLY',
    '_VAR_POSITIONAL'
    '_VAR_KEYWORD'.

    If a parameter is of kind inspect._KEYWORD_ONLY, for
    instance, the string will be built so that its
    argument is passed as a keyword in the call.

    Note that instead of obtaining the signature from
    the callable itself we chose to receive it as an
    argument. This is so because we speed up the function
    by eliminating the step of obtaining the signature and
    this also enable us to use a different signature for
    the given callable, as long as it is compatible with
    the callable.


    Parameters
    ==========

    callable_obj (callable)
        callable to be executed.

    argument_map  (dict)
        has values for the parameters, to use in the call.

    signature_obj (obj returned from inspect.signature())
        object returned from passing the callable object
        to the inspect.signature() function; it is used
        to obtain information about the parameters (or lack
        thereof) of the callable.


    Doctests
    ========

    >>> from inspect import signature
    >>> from pprint  import pprint

    >>> def hello(a, *b, c, **d):
    ...     return a, b, c, d
    >>> sig = signature(hello)

    >>> kwargs = {
    ...   'a': 11,
    ...   'b': ['hello', 'world'],
    ...   'c': 'the letter c',
    ...   'd': {
    ...     'food': 'pie'
    ...   }
    ... }
    >>> values = lay_arguments_and_execute(
    ... hello, kwargs, sig)
    >>> for arg_name, arg_value in zip("abcd", values):
    ...     print(arg_name, ":", arg_value)
    a : 11
    b : ('hello', 'world')
    c : the letter c
    d : {'food': 'pie'}

    >>> # if we didn't use the lay_arguments_and_execute
    >>> # function, that is, just passed the arguments
    >>> # like this: hello(**kwargs), it wouldn't assign
    >>> # the values properly. For instance, the 'b'
    >>> # argument would be empty, its value would be
    >>> # assigned to the 'd' parameter instead, along with
    >>> # the original value of 'd':

    >>> values = hello(**kwargs)
    >>> values[1] # value of 'b'
    ()
    >>> d = values[3] # value of 'd'
    >>> d == {'b': ['hello', 'world'], 'd': {'food': 'pie'}}
    True

    >>> # also, depending on the signature of the function
    >>> # using 'function(**kwargs)' would even raise an
    >>> # error if the function had any parameter of
    >>> # inspect._POSITIONAL_ONLY kind:

    >>> pow_kwargs = {'x': 4, 'y': 2}
    >>> pow(**kwargs)
    Traceback (most recent call last):
    ...
    TypeError: pow() takes no keyword arguments

    >>> # however, we can overcome such problem by using
    >>> # our lay_arguments_and_execute function:

    >>> pow_sig = signature(pow)
    >>> lay_arguments_and_execute(pow, pow_kwargs, pow_sig)
    16

    >>> # it also works when the function has no parameters
    >>> # at all
    >>> def no_param(): return 'this'
    >>> no_param_sig = signature(no_param)
    >>> lay_arguments_and_execute(no_param, {}, no_param_sig)
    'this'

    """
    ### start a string to be built representing the
    ### laid out arguments
    layout_string = ""

    ### iterate over each parameter filling the layout
    ### string with more content representing each argument
    ### being properly laid out in it according to the
    ### kind of its corresponding parameter

    for param_name, param_obj in signature_obj.parameters.items():

        ## if the argument for the parameter wasn't provided,
        ## that is, isn't present on the argument map, skip
        ## this iteration by using the 'continue' statement
        if param_name not in argument_map:
            continue

        ## create a base text from the parameter name,
        ## representing the retrieval of the value for
        ## the parameter from the argument map
        param_value_retrieval = f"argument_map['{param_name}'],"

        ## alter the value retrieval text according to the
        ## kind of the parameter

        # parameters before '*'

        if param_obj.kind in (
            param_obj.POSITIONAL_ONLY,
            param_obj.POSITIONAL_OR_KEYWORD,
        ):
            layout_string += param_value_retrieval

        # a parameter with a single star: *args

        elif param_obj.kind == param_obj.VAR_POSITIONAL:
            layout_string += "*" + param_value_retrieval

        # parameters after either bare '*' or '*args'

        elif param_obj.kind == param_obj.KEYWORD_ONLY:

            layout_string += param_name + "=" + param_value_retrieval

        # a parameter with double stars: **kwargs

        elif param_obj.kind == param_obj.VAR_KEYWORD:
            layout_string += "**" + param_value_retrieval

    ### now format the layout string as a proper call
    ### and, since the string is now complete and
    ### represents a valid call using variables present
    ### in this function's name space, perform the call
    ### using eval(), returning the call's return value
    return eval(f"callable_obj({layout_string})")
Lay arguments, execute callable and return result.

    Works by building a string representing a call to the
    received callable with all its arguments laid out
    properly according to the kind of the corresponding
    parameters.

    Such string is then, executed with the builtin function
    eval() and its return value is returned from this
    function.

    Such kinds of parameters refer to whether a parameter
    is positional-only, keyword-only, both positional
    and keyword compatible, etc., for instance. They are
    listed in the following attributes of the standard
    library inspect module:

    '_POSITIONAL_ONLY',
    '_POSITIONAL_OR_KEYWORD',
    '_KEYWORD_ONLY',
    '_VAR_POSITIONAL'
    '_VAR_KEYWORD'.

    If a parameter is of kind inspect._KEYWORD_ONLY, for
    instance, the string will be built so that its
    argument is passed as a keyword in the call.

    Note that instead of obtaining the signature from
    the callable itself we chose to receive it as an
    argument. This is so because we speed up the function
    by eliminating the step of obtaining the signature and
    this also enable us to use a different signature for
    the given callable, as long as it is compatible with
    the callable.


    Parameters
    ==========

    callable_obj (callable)
        callable to be executed.

    argument_map  (dict)
        has values for the parameters, to use in the call.

    signature_obj (obj returned from inspect.signature())
        object returned from passing the callable object
        to the inspect.signature() function; it is used
        to obtain information about the parameters (or lack
        thereof) of the callable.


    Doctests
    ========

    >>> from inspect import signature
    >>> from pprint  import pprint

    >>> def hello(a, *b, c, **d):
    ...     return a, b, c, d
    >>> sig = signature(hello)

    >>> kwargs = {
    ...   'a': 11,
    ...   'b': ['hello', 'world'],
    ...   'c': 'the letter c',
    ...   'd': {
    ...     'food': 'pie'
    ...   }
    ... }
    >>> values = lay_arguments_and_execute(
    ... hello, kwargs, sig)
    >>> for arg_name, arg_value in zip("abcd", values):
    ...     print(arg_name, ":", arg_value)
    a : 11
    b : ('hello', 'world')
    c : the letter c
    d : {'food': 'pie'}

    >>> # if we didn't use the lay_arguments_and_execute
    >>> # function, that is, just passed the arguments
    >>> # like this: hello(**kwargs), it wouldn't assign
    >>> # the values properly. For instance, the 'b'
    >>> # argument would be empty, its value would be
    >>> # assigned to the 'd' parameter instead, along with
    >>> # the original value of 'd':

    >>> values = hello(**kwargs)
    >>> values[1] # value of 'b'
    ()
    >>> d = values[3] # value of 'd'
    >>> d == {'b': ['hello', 'world'], 'd': {'food': 'pie'}}
    True

    >>> # also, depending on the signature of the function
    >>> # using 'function(**kwargs)' would even raise an
    >>> # error if the function had any parameter of
    >>> # inspect._POSITIONAL_ONLY kind:

    >>> pow_kwargs = {'x': 4, 'y': 2}
    >>> pow(**kwargs)
    Traceback (most recent call last):
    ...
    TypeError: pow() takes no keyword arguments

    >>> # however, we can overcome such problem by using
    >>> # our lay_arguments_and_execute function:

    >>> pow_sig = signature(pow)
    >>> lay_arguments_and_execute(pow, pow_kwargs, pow_sig)
    16

    >>> # it also works when the function has no parameters
    >>> # at all
    >>> def no_param(): return 'this'
    >>> no_param_sig = signature(no_param)
    >>> lay_arguments_and_execute(no_param, {}, no_param_sig)
    'this'

    
nodezator/graphman/utils.py