1# function_class.py -- Library for making python classes from a set
2#                      of functions.
3#
4# Copyright (C) 2008 ParIT Worker Co-operative <paritinfo@parit.ca>
5# This program is free software; you can redistribute it and/or
6# modify it under the terms of the GNU General Public License as
7# published by the Free Software Foundation; either version 2 of
8# the License, or (at your option) any later version.
9#
10# This program is distributed in the hope that it will be useful,
11# but WITHOUT ANY WARRANTY; without even the implied warranty of
12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13# GNU General Public License for more details.
14#
15# You should have received a copy of the GNU General Public License
16# along with this program; if not, contact:
17# Free Software Foundation           Voice:  +1-617-542-5942
18# 51 Franklin Street, Fifth Floor    Fax:    +1-617-542-2652
19# Boston, MA  02110-1301,  USA       gnu@gnu.org
20#
21# @author Mark Jenkins, ParIT Worker Co-operative <mark@parit.ca>
22
23##  @file
24#   @brief Library for making python classes from a set of functions.
25#   @author Mark Jenkins, ParIT Worker Co-operative <mark@parit.ca>
26#   @author Jeff Green,   ParIT Worker Co-operative <jeff@parit.ca>
27#   @ingroup python_bindings
28
29INSTANCE_ARGUMENT = "instance"
30
31class ClassFromFunctions(object):
32    """Inherit this class to give yourself a python class that wraps a set of
33    functions that together constitute the methods of the class.
34
35    The method functions must all have as a first argument an object
36    holding the instance data. There must also be a function that
37    returns a new instance of the class, the constructor.
38
39    Your subclass must define
40    _module - The module where the method functions, including the
41    constructor can be found
42    _new_instance - The name of a function that serves as a constructor,
43    returning the instance data.
44
45    To access the instance data, use the read-only property instance.
46
47    To add some functions from _module as methods, call classmethods like
48    add_method and add_methods_with_prefix.
49    """
50    def __new__(cls, *args, **kargs):
51        # why reimplement __new__? Because later on we're going to
52        # use new to avoid creating new instances when existing instances
53        # already exist with the same __instance value, or equivalent __instance
54        # values, where this is desirable...
55        return super(ClassFromFunctions, cls).__new__(cls)
56
57    def __init__(self, *args, **kargs):
58        """Construct a new instance, using either the function
59        self._module[self._new_instance] or using existing instance
60        data. (specified with the keyword argument, instance)
61
62        if instance argument is None it will be ignored and the
63        constructor will be called to get a new instance
64
65        Pass the arguments that should be passed on to
66        self._module[self._new_instance]. Any arguments of that
67        are instances of ClassFromFunctions will be switched with the instance
68        data. (by calling the .instance property)
69        """
70        if INSTANCE_ARGUMENT in kargs and kargs[INSTANCE_ARGUMENT] is not None:
71            self.__instance = kargs[INSTANCE_ARGUMENT]
72        else:
73            self.__instance = getattr(self._module, self._new_instance)(
74                *process_list_convert_to_instance(args),
75                **process_dict_convert_to_instance(kargs))
76
77    def get_instance(self):
78        """Get the instance data.
79
80        You can also call the instance property
81        """
82        return self.__instance
83
84    instance = property(get_instance)
85
86    # CLASS METHODS
87
88    @classmethod
89    def add_method(cls, function_name, method_name):
90        """! Add the function, method_name to this class as a method named name
91
92        arguments:
93        @param cls Class: class to add methods to
94        @param function_name string: name of the function to add
95        @param method_name string: name of the method that function will be called
96
97        function will be wrapped by method_function"""
98
99        def method_function(self, *meth_func_args, **meth_func_kargs):
100            """! wrapper method for function
101
102            arguments:
103            @param self: FunctionClass instance. Will be turned to its instance property.
104            @param *meth_func_args: arguments to be passed to function. All FunctionClass
105                objects will be turned to their respective instances.
106            @param **meth_func_kargs: keyword arguments to be passed to function. All
107                FunctionClass objects will be turned to their respective instances."""
108            return getattr(self._module, function_name)(
109                self.instance,
110                *process_list_convert_to_instance(meth_func_args),
111                **process_dict_convert_to_instance(meth_func_kargs)
112            )
113
114        setattr(cls, method_name, method_function)
115        setattr(method_function, "__name__", method_name)
116        return method_function
117
118    @classmethod
119    def ya_add_classmethod(cls, function_name, method_name):
120        """! Add the function, method_name to this class as a classmethod named name
121
122        Taken from function_class and modified from add_method() to add classmethod
123        instead of method and not to turn self argument to self.instance.
124
125        arguments:
126        @param cls Class: class to add methods to
127        @param function_name string: name of the function to add
128        @param method_name string: name of the classmethod that function will be called
129
130        function will be wrapped by method_function"""
131
132        def method_function(self, *meth_func_args, **meth_func_kargs):
133            """! wrapper method for function
134
135            arguments:
136            @param self: FunctionClass instance.
137            @param *meth_func_args: arguments to be passed to function. All FunctionClass
138                objects will be turned to their respective instances.
139            @param **meth_func_kargs: keyword arguments to be passed to function. All
140                FunctionClass objects will be turned to their respective instances."""
141            return getattr(self._module, function_name)(
142                self,
143                *process_list_convert_to_instance(meth_func_args),
144                **process_dict_convert_to_instance(meth_func_kargs)
145            )
146
147        setattr(cls, method_name, classmethod(method_function))
148        setattr(method_function, "__name__", method_name)
149        return method_function
150
151    @classmethod
152    def ya_add_method(cls, function_name, method_name):
153        """! Add the function, method_name to this class as a method named name
154
155        Taken from function_class. Modified to not turn self to self.instance
156        as add_method() does.
157
158        arguments:
159        @param cls Class: class to add methods to
160        @param function_name string: name of the function to add
161        @param method_name string: name of the method that function will be called
162
163        function will be wrapped by method_function"""
164
165        def method_function(self, *meth_func_args, **meth_func_kargs):
166            """! wrapper method for function
167
168            arguments:
169            @param self: FunctionClass instance.
170            @param *meth_func_args: arguments to be passed to function. All FunctionClass
171                objects will be turned to their respective instances.
172            @param **meth_func_kargs: keyword arguments to be passed to function. All
173                FunctionClass objects will be turned to their respective instances."""
174            return getattr(self._module, function_name)(
175                self,
176                *process_list_convert_to_instance(meth_func_args),
177                **process_dict_convert_to_instance(meth_func_kargs)
178            )
179
180        setattr(cls, method_name, method_function)
181        setattr(method_function, "__name__", method_name)
182        return method_function
183
184    @classmethod
185    def add_methods_with_prefix(cls, prefix, exclude=[]):
186        """Add a group of functions with the same prefix, exclude methods
187        in array exclude.
188        """
189        for function_name, function_value, after_prefix in \
190                extract_attributes_with_prefix(cls._module, prefix):
191
192            if not (function_name in exclude):
193                cls.add_method(function_name, after_prefix)
194
195    @classmethod
196    def add_constructor_and_methods_with_prefix(cls, prefix, constructor, exclude=[]):
197        """Add a group of functions with the same prefix, and set the
198        _new_instance attribute to prefix + constructor. Don't add methods
199        in array exclude.
200        """
201        cls.add_methods_with_prefix(prefix, exclude=exclude)
202        cls._new_instance = prefix + constructor
203
204    @classmethod
205    def decorate_functions(cls, decorator, *args):
206        for function_name in args:
207            setattr( cls, function_name,
208                     decorator( getattr(cls, function_name) ) )
209
210    @classmethod
211    def decorate_method(cls, decorator, method_name, *args, **kargs):
212        """! decorate method method_name of class cls with decorator decorator
213
214        in difference to decorate_functions() this allows to provide additional
215        arguments for the decorator function.
216
217        arguments:
218            @param cls: class
219            @param decorator: function to decorate method
220            @param method_name: name of method to decorate (string)
221            @param *args: positional arguments for decorator
222            @param **kargs: keyword arguments for decorator"""
223        setattr(cls, method_name,
224                    decorator(getattr(cls, method_name), *args, **kargs))
225
226def method_function_returns_instance(method_function, cls):
227    """A function decorator that is used to decorate method functions that
228    return instance data, to return instances instead.
229
230    You can't use this decorator with @, because this function has a second
231    argument.
232    """
233    assert( 'instance' == INSTANCE_ARGUMENT )
234    def new_function(*args, **kargs):
235        kargs_cls = { INSTANCE_ARGUMENT : method_function(*args, **kargs) }
236        if kargs_cls['instance'] == None:
237            return None
238        else:
239            return cls( **kargs_cls )
240
241    return new_function
242
243def method_function_returns_instance_list(method_function, cls):
244    def new_function(*args, **kargs):
245        return [ cls( **{INSTANCE_ARGUMENT: item} )
246                 for item in method_function(*args, **kargs) ]
247    return new_function
248
249def methods_return_instance_lists(cls, function_dict):
250    for func_name, instance_name in iter(function_dict.items()):
251        setattr(cls, func_name,
252                method_function_returns_instance_list(
253                getattr(cls, func_name), instance_name))
254
255def default_arguments_decorator(function, *args, **kargs):
256    """! Decorates a function to give it default, positional and keyword arguments
257
258    mimics python behavior when setting defaults in function/method arguments.
259    arguments can be set for positional or keyword arguments.
260
261    kargs_pos contains positions of the keyword arguments.
262    @exception A TypeError will be raised if an argument is set as a positional and keyword argument
263    at the same time.
264    @note It might be possible to get keyword argument positional information using
265    introspection to avoid having to specify them manually
266
267    a keyword argument default will be overwritten by a positional argument at the
268    actual function call
269
270    this function modifies the docstring of the wrapped function to reflect
271    the defaults.
272
273    You can't use this decorator with @, because this function has more
274    than one argument.
275
276    arguments:
277    @param *args: optional positional defaults
278    @param kargs_pos: dict with keyword arguments as key and their position in the argument list as value
279    @param **kargs: optional keyword defaults
280
281    @return new_function wrapping original function
282    """
283
284    def new_function(*function_args, **function_kargs):
285        kargs_pos = {}
286        if "kargs_pos" in kargs:
287            kargs_pos = kargs.pop("kargs_pos")
288        new_argset = list(function_args)
289        new_argset.extend(args[len(function_args) :])
290        new_kargset = {**kargs, **function_kargs}
291        for karg_pos in kargs_pos:
292            if karg_pos in new_kargset:
293                pos_karg = kargs_pos[karg_pos]
294                if pos_karg < len(new_argset):
295                    new_kargset.pop(karg_pos)
296
297        return function(*new_argset, **new_kargset)
298
299    kargs_pos = {} if "kargs_pos" not in kargs else kargs["kargs_pos"]
300    for karg_pos in kargs_pos:
301        if karg_pos in kargs:
302            pos_karg = kargs_pos[karg_pos]
303            if pos_karg < len(args):
304                raise TypeError(
305                    "default_arguments_decorator() got multiple values for argument '%s'"
306                    % karg_pos
307                )
308
309    if new_function.__doc__ is None:
310        new_function.__doc__ = ""
311    if len(args):
312        firstarg = True
313        new_function.__doc__ += "positional argument defaults:\n"
314        for arg in args:
315            if not firstarg:
316                new_function.__doc__ += ", "
317            else:
318                new_function.__doc__ += "  "
319                firstarg = False
320            new_function.__doc__ += str(arg)
321        new_function.__doc__ += "\n"
322    if len(kargs):
323        new_function.__doc__ += "keyword argument defaults:\n"
324        for karg in kargs:
325            if karg != "kargs_pos":
326                new_function.__doc__ += (
327                    "  " + str(karg) + " = " + str(kargs[karg]) + "\n"
328                )
329        if kargs_pos:
330            new_function.__doc__ += "keyword argument positions:\n"
331            for karg in kargs_pos:
332                new_function.__doc__ += (
333                    "  " + str(karg) + " is at pos " + str(kargs_pos[karg]) + "\n"
334                )
335    if len(args) or len(kargs):
336        new_function.__doc__ += (
337            "(defaults have been set by default_arguments_decorator method)"
338        )
339    return new_function
340
341
342def return_instance_if_value_has_it(value):
343    """Return value.instance if value is an instance of ClassFromFunctions,
344    else return value
345    """
346    if isinstance(value, ClassFromFunctions):
347        return value.instance
348    else:
349        return value
350
351def process_list_convert_to_instance( value_list ):
352    """Return a list built from value_list, where if a value is in an instance
353    of ClassFromFunctions, we put value.instance in the list instead.
354
355    Things that are not instances of ClassFromFunctions are returned to
356    the new list unchanged.
357    """
358    return [ return_instance_if_value_has_it(value)
359             for value in value_list ]
360
361def process_dict_convert_to_instance(value_dict):
362    """Return a dict built from value_dict, where if a value is in an instance
363    of ClassFromFunctions, we put value.instance in the dict instead.
364
365    Things that are not instances of ClassFromFunctions are returned to
366    the new dict unchanged.
367    """
368    return {
369        key: return_instance_if_value_has_it(value) for key, value in value_dict.items()
370    }
371
372
373def extract_attributes_with_prefix(obj, prefix):
374    """Generator that iterates through the attributes of an object and
375    for any attribute that matches a prefix, this yields
376    the attribute name, the attribute value, and the text that appears
377    after the prefix in the name
378    """
379    for attr_name, attr_value in iter(obj.__dict__.items()):
380        if attr_name.startswith(prefix):
381            after_prefix = attr_name[ len(prefix): ]
382            yield attr_name, attr_value, after_prefix
383
384def methods_return_instance(cls, function_dict):
385    """Iterates through a dictionary of function name strings and instance names
386    and sets the function to return the associated instance
387    """
388    for func_name, instance_name in iter(function_dict.items()):
389        setattr(cls, func_name,
390            method_function_returns_instance( getattr(cls, func_name), instance_name))
391
392