1#!/usr/bin/env python
2
3r"""
4This module provides functions which are useful for writing python wrapper functions (i.e. in this context, a
5wrapper function is one whose aim is to call some other function on the caller's behalf but to provide some
6additional functionality over and above what the base function provides).
7"""
8
9import sys
10import inspect
11
12
13def create_wrapper_def_and_call(base_func_name,
14                                wrap_func_name):
15    r"""
16    Return a wrapper function definition line and a base function call line.
17
18    This is a utility for helping to create wrapper functions.
19
20    For example, if there existed a function with the following definition line:
21    def sprint_foo_bar(headers=1):
22
23    And the user wished to write a print_foo_bar wrapper function, they could call
24    create_wrapper_def_and_call as follows:
25
26    func_def_line, call_line = create_wrapper_def_and_call("sprint_foo_bar",
27                                                           "print_foo_bar")
28
29    They would get the following results:
30    func_def_line                   def print_foo_bar(headers=1):
31    call_line                       sprint_foo_bar(headers=headers)
32
33    The func_def_line is suitable as the definition line for the wrapper function.  The call_line is suitable
34    for use in the new wrapper function wherever it wishes to call the base function.  By explicitly
35    specifying each parm in the definition and the call line, we allow the caller of the wrapper function to
36    refer to any given parm by name rather than having to specify parms positionally.
37
38    Description of argument(s):
39    base_func_name                  The name of the base function around which a wrapper is being created.
40    wrap_func_name                  The name of the wrapper function being created.
41    """
42
43    # Get caller's module name.  Note: that for the present we've hard-coded the stack_frame_ix value
44    # because we expect a call stack to this function to be something like this:
45    # caller
46    #   create_print_wrapper_funcs
47    #     create_func_def_string
48    #       create_wrapper_def_and_call
49    stack_frame_ix = 3
50    frame = inspect.stack()[stack_frame_ix]
51    module = inspect.getmodule(frame[0])
52    mod_name = module.__name__
53
54    # Get a reference to the base function.
55    base_func = getattr(sys.modules[mod_name], base_func_name)
56    # Get the argument specification for the base function.
57    base_arg_spec = inspect.getargspec(base_func)
58    base_arg_list = base_arg_spec[0]
59    num_args = len(base_arg_list)
60    # Get the variable argument specification for the base function.
61    var_args = base_arg_spec[1]
62    if var_args is None:
63        var_args = []
64    else:
65        var_args = ["*" + var_args]
66    keyword_args = base_arg_spec[2]
67    if keyword_args is None:
68        keyword_args = []
69    else:
70        keyword_args = ["**" + keyword_args]
71    if base_arg_spec[3] is None:
72        base_default_list = []
73    else:
74        base_default_list = list(base_arg_spec[3])
75    num_defaults = len(base_default_list)
76    num_non_defaults = num_args - num_defaults
77
78    # Create base_arg_default_string which is a reconstruction of the base function's argument list.
79    # Example base_arg_default_string:
80    # headers, last=2, first=[1]
81    # First, create a new list where each entry is of the form "arg=default".
82    base_arg_default_list = list(base_arg_list)
83    for ix in range(num_non_defaults, len(base_arg_default_list)):
84        base_default_ix = ix - num_non_defaults
85        if isinstance(base_default_list[base_default_ix], str):
86            default_string = "'" + base_default_list[base_default_ix] + "'"
87            # Convert "\n" to "\\n".
88            default_string = default_string.replace("\n", "\\n")
89        else:
90            default_string = str(base_default_list[base_default_ix])
91        base_arg_default_list[ix] += "=" + default_string
92    base_arg_default_string =\
93        ', '.join(base_arg_default_list + var_args + keyword_args)
94
95    # Create the argument string which can be used to call the base function.
96    # Example call_arg_string:
97    # headers=headers, last=last, first=first
98    call_arg_string = ', '.join([val + "=" + val for val in base_arg_list]
99                                + var_args + keyword_args)
100
101    # Compose the result values.
102    func_def_line = "def " + wrap_func_name + "(" + base_arg_default_string +\
103        "):"
104    call_line = base_func_name + "(" + call_arg_string + ")"
105
106    return func_def_line, call_line
107
108
109def create_func_def_string(base_func_name,
110                           wrap_func_name,
111                           func_body_template,
112                           replace_dict):
113    r"""
114    Create and return a complete function definition as a string.  The caller may run "exec" on the resulting
115    string to create the desired function.
116
117    Description of argument(s):
118    base_func_name                  The name of the base function around which a wrapper is being created.
119    wrap_func_name                  The name of the wrapper function being created.
120    func_body_template              A function body in the form of a list.  Each list element represents one
121                                    line of a function  This is a template in so far as text substitutions
122                                    will be done on it to arrive at a valid function definition.  This
123                                    template should NOT contain the function definition line (e.g. "def
124                                    func1():").  create_func_def_string will pre-pend the definition line.
125                                    The template should also contain the text "<call_line>" which is to be
126                                    replaced by text which will call the base function with appropriate
127                                    arguments.
128    replace_dict                    A dictionary indicating additional text replacements to be done.  For
129                                    example, if the template contains a "<sub1>" (be sure to include the
130                                    angle brackets), and the dictionary contains a key/value pair of
131                                    'sub1'/'replace1', then all instances of "<sub1>" will be replaced by
132                                    "replace1".
133    """
134
135    # Create the initial function definition list as a copy of the template.
136    func_def = list(func_body_template)
137    # Call create_wrapper_def_and_call to get func_def_line and call_line.
138    func_def_line, call_line = create_wrapper_def_and_call(base_func_name,
139                                                           wrap_func_name)
140    # Insert the func_def_line composed by create_wrapper_def_and_call is the first list entry.
141    func_def.insert(0, func_def_line)
142    # Make sure the replace_dict has a 'call_line'/call_line pair so that any '<call_line>' text gets
143    # replaced as intended.
144    replace_dict['call_line'] = call_line
145
146    # Do the replacements.
147    for key, value in replace_dict.items():
148        func_def = [w.replace("<" + key + ">", value) for w in func_def]
149
150    return '\n'.join(func_def) + "\n"
151