1"""
2Utility functions to modify other functions
3"""
4
5
6import logging
7import types
8
9import salt.utils.args
10from salt.exceptions import SaltInvocationError
11
12log = logging.getLogger(__name__)
13
14
15def namespaced_function(function, global_dict, defaults=None, preserve_context=False):
16    """
17    Redefine (clone) a function under a different globals() namespace scope
18
19        preserve_context:
20            Allow keeping the context taken from orignal namespace,
21            and extend it with globals() taken from
22            new targetted namespace.
23    """
24    if defaults is None:
25        defaults = function.__defaults__
26
27    if preserve_context:
28        _global_dict = function.__globals__.copy()
29        _global_dict.update(global_dict)
30        global_dict = _global_dict
31    new_namespaced_function = types.FunctionType(
32        function.__code__,
33        global_dict,
34        name=function.__name__,
35        argdefs=defaults,
36        closure=function.__closure__,
37    )
38    new_namespaced_function.__dict__.update(function.__dict__)
39    return new_namespaced_function
40
41
42def alias_function(fun, name, doc=None):
43    """
44    Copy a function
45    """
46    alias_fun = types.FunctionType(
47        fun.__code__,
48        fun.__globals__,
49        str(name),
50        fun.__defaults__,
51        fun.__closure__,
52    )
53    alias_fun.__dict__.update(fun.__dict__)
54
55    if doc and isinstance(doc, str):
56        alias_fun.__doc__ = doc
57    else:
58        orig_name = fun.__name__
59        alias_msg = "\nThis function is an alias of ``{}``.\n".format(orig_name)
60        alias_fun.__doc__ = alias_msg + (fun.__doc__ or "")
61
62    return alias_fun
63
64
65def parse_function(function_arguments):
66    """
67    Helper function to parse function_arguments (module.run format)
68    into args and kwargs.
69    This function is similar to salt.utils.data.repack_dictlist, except that this
70    handles mixed (i.e. dict and non-dict) arguments in the input list.
71
72    :param list function_arguments: List of items and dicts with kwargs.
73
74    :rtype: dict
75    :return: Dictionary with ``args`` and ``kwargs`` keyword.
76    """
77    function_args = []
78    function_kwargs = {}
79    for item in function_arguments:
80        if isinstance(item, dict):
81            function_kwargs.update(item)
82        else:
83            function_args.append(item)
84    return {"args": function_args, "kwargs": function_kwargs}
85
86
87def call_function(salt_function, *args, **kwargs):
88    """
89    Calls a function from the specified module.
90
91    :param function salt_function: Function reference to call
92    :return: The result of the function call
93    """
94    argspec = salt.utils.args.get_function_argspec(salt_function)
95    # function_kwargs is initialized to a dictionary of keyword arguments the function to be run accepts
96    function_kwargs = dict(
97        zip(
98            argspec.args[
99                -len(argspec.defaults or []) :
100            ],  # pylint: disable=incompatible-py3-code
101            argspec.defaults or [],
102        )
103    )
104    # expected_args is initialized to a list of positional arguments that the function to be run accepts
105    expected_args = argspec.args[
106        : len(argspec.args or []) - len(argspec.defaults or [])
107    ]
108    function_args, kw_to_arg_type = [], {}
109    for funcset in reversed(args or []):
110        if not isinstance(funcset, dict):
111            # We are just receiving a list of args to the function to be run, so just append
112            # those to the arg list that we will pass to the func.
113            function_args.append(funcset)
114        else:
115            for kwarg_key in funcset.keys():
116                # We are going to pass in a keyword argument. The trick here is to make certain
117                # that if we find that in the *args* list that we pass it there and not as a kwarg
118                if kwarg_key in expected_args:
119                    kw_to_arg_type[kwarg_key] = funcset[kwarg_key]
120                else:
121                    # Otherwise, we're good and just go ahead and pass the keyword/value pair into
122                    # the kwargs list to be run.
123                    function_kwargs.update(funcset)
124    function_args.reverse()
125    # Add kwargs passed as kwargs :)
126    function_kwargs.update(kwargs)
127    for arg in expected_args:
128        if arg in kw_to_arg_type:
129            function_args.append(kw_to_arg_type[arg])
130    _exp_prm = len(argspec.args or []) - len(argspec.defaults or [])
131    _passed_prm = len(function_args)
132    missing = []
133    if _exp_prm > _passed_prm:
134        for arg in argspec.args[_passed_prm:]:
135            if arg not in function_kwargs:
136                missing.append(arg)
137            else:
138                # Found the expected argument as a keyword
139                # increase the _passed_prm count
140                _passed_prm += 1
141    if missing:
142        raise SaltInvocationError("Missing arguments: {}".format(", ".join(missing)))
143    elif _exp_prm > _passed_prm:
144        raise SaltInvocationError(
145            "Function expects {} positional parameters, got only {}".format(
146                _exp_prm, _passed_prm
147            )
148        )
149
150    return salt_function(*function_args, **function_kwargs)
151