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