mirror of
https://github.com/Gnucash/gnucash.git
synced 2025-02-20 11:48:30 -06:00
default_arguments_decorator until now only allows positional argument defaults. This adds keyword defaults. The keywords can be mapped to the positional arguments by optional argument kargs_pos so interactions between keyword and positional arg defaults can raise a TypeError. Some more information in the docstring is included. In addition the docstring of the wrapped function will be modified to contain information about the defaults.
392 lines
16 KiB
Python
392 lines
16 KiB
Python
# function_class.py -- Library for making python classes from a set
|
|
# of functions.
|
|
#
|
|
# Copyright (C) 2008 ParIT Worker Co-operative <paritinfo@parit.ca>
|
|
# This program is free software; you can redistribute it and/or
|
|
# modify it under the terms of the GNU General Public License as
|
|
# published by the Free Software Foundation; either version 2 of
|
|
# the License, or (at your option) any later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with this program; if not, contact:
|
|
# Free Software Foundation Voice: +1-617-542-5942
|
|
# 51 Franklin Street, Fifth Floor Fax: +1-617-542-2652
|
|
# Boston, MA 02110-1301, USA gnu@gnu.org
|
|
#
|
|
# @author Mark Jenkins, ParIT Worker Co-operative <mark@parit.ca>
|
|
|
|
## @file
|
|
# @brief Library for making python classes from a set of functions.
|
|
# @author Mark Jenkins, ParIT Worker Co-operative <mark@parit.ca>
|
|
# @author Jeff Green, ParIT Worker Co-operative <jeff@parit.ca>
|
|
# @ingroup python_bindings
|
|
|
|
INSTANCE_ARGUMENT = "instance"
|
|
|
|
class ClassFromFunctions(object):
|
|
"""Inherit this class to give yourself a python class that wraps a set of
|
|
functions that together constitute the methods of the class.
|
|
|
|
The method functions must all have as a first argument an object
|
|
holding the instance data. There must also be a function that
|
|
returns a new instance of the class, the constructor.
|
|
|
|
Your subclass must define
|
|
_module - The module where the method functions, including the
|
|
constructor can be found
|
|
_new_instance - The name of a function that serves as a constructor,
|
|
returning the instance data.
|
|
|
|
To access the instance data, use the read-only property instance.
|
|
|
|
To add some functions from _module as methods, call classmethods like
|
|
add_method and add_methods_with_prefix.
|
|
"""
|
|
def __new__(cls, *args, **kargs):
|
|
# why reimplement __new__? Because later on we're going to
|
|
# use new to avoid creating new instances when existing instances
|
|
# already exist with the same __instance value, or equivalent __instance
|
|
# values, where this is desirable...
|
|
return super(ClassFromFunctions, cls).__new__(cls)
|
|
|
|
def __init__(self, *args, **kargs):
|
|
"""Construct a new instance, using either the function
|
|
self._module[self._new_instance] or using existing instance
|
|
data. (specified with the keyword argument, instance)
|
|
|
|
if instance argument is None it will be ignored and the
|
|
constructor will be called to get a new instance
|
|
|
|
Pass the arguments that should be passed on to
|
|
self._module[self._new_instance]. Any arguments of that
|
|
are instances of ClassFromFunctions will be switched with the instance
|
|
data. (by calling the .instance property)
|
|
"""
|
|
if INSTANCE_ARGUMENT in kargs and kargs[INSTANCE_ARGUMENT] is not None:
|
|
self.__instance = kargs[INSTANCE_ARGUMENT]
|
|
else:
|
|
self.__instance = getattr(self._module, self._new_instance)(
|
|
*process_list_convert_to_instance(args),
|
|
**process_dict_convert_to_instance(kargs))
|
|
|
|
def get_instance(self):
|
|
"""Get the instance data.
|
|
|
|
You can also call the instance property
|
|
"""
|
|
return self.__instance
|
|
|
|
instance = property(get_instance)
|
|
|
|
# CLASS METHODS
|
|
|
|
@classmethod
|
|
def add_method(cls, function_name, method_name):
|
|
"""! Add the function, method_name to this class as a method named name
|
|
|
|
arguments:
|
|
@param cls Class: class to add methods to
|
|
@param function_name string: name of the function to add
|
|
@param method_name string: name of the method that function will be called
|
|
|
|
function will be wrapped by method_function"""
|
|
|
|
def method_function(self, *meth_func_args, **meth_func_kargs):
|
|
"""! wrapper method for function
|
|
|
|
arguments:
|
|
@param self: FunctionClass instance. Will be turned to its instance property.
|
|
@param *meth_func_args: arguments to be passed to function. All FunctionClass
|
|
objects will be turned to their respective instances.
|
|
@param **meth_func_kargs: keyword arguments to be passed to function. All
|
|
FunctionClass objects will be turned to their respective instances."""
|
|
return getattr(self._module, function_name)(
|
|
self.instance,
|
|
*process_list_convert_to_instance(meth_func_args),
|
|
**process_dict_convert_to_instance(meth_func_kargs)
|
|
)
|
|
|
|
setattr(cls, method_name, method_function)
|
|
setattr(method_function, "__name__", method_name)
|
|
return method_function
|
|
|
|
@classmethod
|
|
def ya_add_classmethod(cls, function_name, method_name):
|
|
"""! Add the function, method_name to this class as a classmethod named name
|
|
|
|
Taken from function_class and modified from add_method() to add classmethod
|
|
instead of method and not to turn self argument to self.instance.
|
|
|
|
arguments:
|
|
@param cls Class: class to add methods to
|
|
@param function_name string: name of the function to add
|
|
@param method_name string: name of the classmethod that function will be called
|
|
|
|
function will be wrapped by method_function"""
|
|
|
|
def method_function(self, *meth_func_args, **meth_func_kargs):
|
|
"""! wrapper method for function
|
|
|
|
arguments:
|
|
@param self: FunctionClass instance.
|
|
@param *meth_func_args: arguments to be passed to function. All FunctionClass
|
|
objects will be turned to their respective instances.
|
|
@param **meth_func_kargs: keyword arguments to be passed to function. All
|
|
FunctionClass objects will be turned to their respective instances."""
|
|
return getattr(self._module, function_name)(
|
|
self,
|
|
*process_list_convert_to_instance(meth_func_args),
|
|
**process_dict_convert_to_instance(meth_func_kargs)
|
|
)
|
|
|
|
setattr(cls, method_name, classmethod(method_function))
|
|
setattr(method_function, "__name__", method_name)
|
|
return method_function
|
|
|
|
@classmethod
|
|
def ya_add_method(cls, function_name, method_name):
|
|
"""! Add the function, method_name to this class as a method named name
|
|
|
|
Taken from function_class. Modified to not turn self to self.instance
|
|
as add_method() does.
|
|
|
|
arguments:
|
|
@param cls Class: class to add methods to
|
|
@param function_name string: name of the function to add
|
|
@param method_name string: name of the method that function will be called
|
|
|
|
function will be wrapped by method_function"""
|
|
|
|
def method_function(self, *meth_func_args, **meth_func_kargs):
|
|
"""! wrapper method for function
|
|
|
|
arguments:
|
|
@param self: FunctionClass instance.
|
|
@param *meth_func_args: arguments to be passed to function. All FunctionClass
|
|
objects will be turned to their respective instances.
|
|
@param **meth_func_kargs: keyword arguments to be passed to function. All
|
|
FunctionClass objects will be turned to their respective instances."""
|
|
return getattr(self._module, function_name)(
|
|
self,
|
|
*process_list_convert_to_instance(meth_func_args),
|
|
**process_dict_convert_to_instance(meth_func_kargs)
|
|
)
|
|
|
|
setattr(cls, method_name, method_function)
|
|
setattr(method_function, "__name__", method_name)
|
|
return method_function
|
|
|
|
@classmethod
|
|
def add_methods_with_prefix(cls, prefix, exclude=[]):
|
|
"""Add a group of functions with the same prefix, exclude methods
|
|
in array exclude.
|
|
"""
|
|
for function_name, function_value, after_prefix in \
|
|
extract_attributes_with_prefix(cls._module, prefix):
|
|
|
|
if not (function_name in exclude):
|
|
cls.add_method(function_name, after_prefix)
|
|
|
|
@classmethod
|
|
def add_constructor_and_methods_with_prefix(cls, prefix, constructor, exclude=[]):
|
|
"""Add a group of functions with the same prefix, and set the
|
|
_new_instance attribute to prefix + constructor. Don't add methods
|
|
in array exclude.
|
|
"""
|
|
cls.add_methods_with_prefix(prefix, exclude=exclude)
|
|
cls._new_instance = prefix + constructor
|
|
|
|
@classmethod
|
|
def decorate_functions(cls, decorator, *args):
|
|
for function_name in args:
|
|
setattr( cls, function_name,
|
|
decorator( getattr(cls, function_name) ) )
|
|
|
|
@classmethod
|
|
def decorate_method(cls, decorator, method_name, *args, **kargs):
|
|
"""! decorate method method_name of class cls with decorator decorator
|
|
|
|
in difference to decorate_functions() this allows to provide additional
|
|
arguments for the decorator function.
|
|
|
|
arguments:
|
|
@param cls: class
|
|
@param decorator: function to decorate method
|
|
@param method_name: name of method to decorate (string)
|
|
@param *args: positional arguments for decorator
|
|
@param **kargs: keyword arguments for decorator"""
|
|
setattr(cls, method_name,
|
|
decorator(getattr(cls, method_name), *args, **kargs))
|
|
|
|
def method_function_returns_instance(method_function, cls):
|
|
"""A function decorator that is used to decorate method functions that
|
|
return instance data, to return instances instead.
|
|
|
|
You can't use this decorator with @, because this function has a second
|
|
argument.
|
|
"""
|
|
assert( 'instance' == INSTANCE_ARGUMENT )
|
|
def new_function(*args, **kargs):
|
|
kargs_cls = { INSTANCE_ARGUMENT : method_function(*args, **kargs) }
|
|
if kargs_cls['instance'] == None:
|
|
return None
|
|
else:
|
|
return cls( **kargs_cls )
|
|
|
|
return new_function
|
|
|
|
def method_function_returns_instance_list(method_function, cls):
|
|
def new_function(*args, **kargs):
|
|
return [ cls( **{INSTANCE_ARGUMENT: item} )
|
|
for item in method_function(*args, **kargs) ]
|
|
return new_function
|
|
|
|
def methods_return_instance_lists(cls, function_dict):
|
|
for func_name, instance_name in iter(function_dict.items()):
|
|
setattr(cls, func_name,
|
|
method_function_returns_instance_list(
|
|
getattr(cls, func_name), instance_name))
|
|
|
|
def default_arguments_decorator(function, *args, **kargs):
|
|
"""! Decorates a function to give it default, positional and keyword arguments
|
|
|
|
mimics python behavior when setting defaults in function/method arguments.
|
|
arguments can be set for positional or keyword arguments.
|
|
|
|
kargs_pos contains positions of the keyword arguments.
|
|
@exception A TypeError will be raised if an argument is set as a positional and keyword argument
|
|
at the same time.
|
|
@note It might be possible to get keyword argument positional information using
|
|
introspection to avoid having to specify them manually
|
|
|
|
a keyword argument default will be overwritten by a positional argument at the
|
|
actual function call
|
|
|
|
this function modifies the docstring of the wrapped funtion to reflect
|
|
the defaults.
|
|
|
|
You can't use this decorator with @, because this function has more
|
|
than one argument.
|
|
|
|
arguments:
|
|
@param *args: optional positional defaults
|
|
@param kargs_pos: dict with keyword arguments as key and their position in the argument list as value
|
|
@param **kargs: optional keyword defaults
|
|
|
|
@return new_function wrapping original function
|
|
"""
|
|
|
|
def new_function(*function_args, **function_kargs):
|
|
kargs_pos = {}
|
|
if "kargs_pos" in kargs:
|
|
kargs_pos = kargs.pop("kargs_pos")
|
|
new_argset = list(function_args)
|
|
new_argset.extend(args[len(function_args) :])
|
|
new_kargset = {**kargs, **function_kargs}
|
|
for karg_pos in kargs_pos:
|
|
if karg_pos in new_kargset:
|
|
pos_karg = kargs_pos[karg_pos]
|
|
if pos_karg < len(new_argset):
|
|
new_kargset.pop(karg_pos)
|
|
|
|
return function(*new_argset, **new_kargset)
|
|
|
|
kargs_pos = {} if "kargs_pos" not in kargs else kargs["kargs_pos"]
|
|
for karg_pos in kargs_pos:
|
|
if karg_pos in kargs:
|
|
pos_karg = kargs_pos[karg_pos]
|
|
if pos_karg < len(args):
|
|
raise TypeError(
|
|
"default_arguments_decorator() got multiple values for argument '%s'"
|
|
% karg_pos
|
|
)
|
|
|
|
if new_function.__doc__ is None:
|
|
new_function.__doc__ = ""
|
|
if len(args):
|
|
firstarg = True
|
|
new_function.__doc__ += "positional argument defaults:\n"
|
|
for arg in args:
|
|
if not firstarg:
|
|
new_function.__doc__ += ", "
|
|
else:
|
|
new_function.__doc__ += " "
|
|
firstarg = False
|
|
new_function.__doc__ += str(arg)
|
|
new_function.__doc__ += "\n"
|
|
if len(kargs):
|
|
new_function.__doc__ += "keyword argument defaults:\n"
|
|
for karg in kargs:
|
|
if karg != "kargs_pos":
|
|
new_function.__doc__ += (
|
|
" " + str(karg) + " = " + str(kargs[karg]) + "\n"
|
|
)
|
|
if kargs_pos:
|
|
new_function.__doc__ += "keyword argument positions:\n"
|
|
for karg in kargs_pos:
|
|
new_function.__doc__ += (
|
|
" " + str(karg) + " is at pos " + str(kargs_pos[karg]) + "\n"
|
|
)
|
|
if len(args) or len(kargs):
|
|
new_function.__doc__ += (
|
|
"(defaults have been set by default_arguments_decorator method)"
|
|
)
|
|
return new_function
|
|
|
|
|
|
def return_instance_if_value_has_it(value):
|
|
"""Return value.instance if value is an instance of ClassFromFunctions,
|
|
else return value
|
|
"""
|
|
if isinstance(value, ClassFromFunctions):
|
|
return value.instance
|
|
else:
|
|
return value
|
|
|
|
def process_list_convert_to_instance( value_list ):
|
|
"""Return a list built from value_list, where if a value is in an instance
|
|
of ClassFromFunctions, we put value.instance in the list instead.
|
|
|
|
Things that are not instances of ClassFromFunctions are returned to
|
|
the new list unchanged.
|
|
"""
|
|
return [ return_instance_if_value_has_it(value)
|
|
for value in value_list ]
|
|
|
|
def process_dict_convert_to_instance(value_dict):
|
|
"""Return a dict built from value_dict, where if a value is in an instance
|
|
of ClassFromFunctions, we put value.instance in the dict instead.
|
|
|
|
Things that are not instances of ClassFromFunctions are returned to
|
|
the new dict unchanged.
|
|
"""
|
|
return {
|
|
key: return_instance_if_value_has_it(value) for key, value in value_dict.items()
|
|
}
|
|
|
|
|
|
def extract_attributes_with_prefix(obj, prefix):
|
|
"""Generator that iterates through the attributes of an object and
|
|
for any attribute that matches a prefix, this yields
|
|
the attribute name, the attribute value, and the text that appears
|
|
after the prefix in the name
|
|
"""
|
|
for attr_name, attr_value in iter(obj.__dict__.items()):
|
|
if attr_name.startswith(prefix):
|
|
after_prefix = attr_name[ len(prefix): ]
|
|
yield attr_name, attr_value, after_prefix
|
|
|
|
def methods_return_instance(cls, function_dict):
|
|
"""Iterates through a dictionary of function name strings and instance names
|
|
and sets the function to return the associated instance
|
|
"""
|
|
for func_name, instance_name in iter(function_dict.items()):
|
|
setattr(cls, func_name,
|
|
method_function_returns_instance( getattr(cls, func_name), instance_name))
|
|
|