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