├── .cache └── v │ └── cache │ └── lastfailed ├── .gitignore ├── LICENSE ├── README.rst ├── conftest.py ├── genericfuncs.py ├── setup.py ├── test └── test_basic.py └── user.py /.cache/v/cache/lastfailed: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .cache/ 3 | *.pyc 4 | tests/*.pyc 5 | user.py 6 | dist/ 7 | genericfuncs.egg-info/ 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Aviv Cohn 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | 2 | genericfuncs 3 | ============ 4 | 5 | :code:`genericfuncs` enables you to create functions which execute different 6 | implementations depending on the arguments. 7 | 8 | This module can be seen as a powerful improvement over Python 3's :code:`singledispatch`: 9 | 10 | * Allows dispatch over any boolean callable, not just type checks. 11 | * Allows dispatch over any number of arguments, not just the first argument. 12 | 13 | 14 | Basic usage 15 | *********** 16 | 17 | .. code-block:: python 18 | 19 | from genericfuncs import generic 20 | 21 | @generic 22 | def func(a): 23 | # default implementation 24 | raise ValueError() 25 | 26 | @func.when(lambda a: a.startswith('foo')) 27 | def _foo(a): 28 | return a.upper() 29 | 30 | @func.when(lambda a: a.startswith('bar')) 31 | def _bar(a): 32 | return a.lower() 33 | 34 | The first predicate that returns True has its mapped implementation invoked. 35 | Predicates are checked in order of definition. 36 | 37 | 38 | Installation 39 | ************ 40 | 41 | .. code-block:: bash 42 | 43 | pip install genericfuncs 44 | 45 | 46 | Advanced 47 | ******** 48 | 49 | Arguments are injected into predicates and implementations by their name. 50 | This means a predicate or implementation is able to specify only the arguments it needs. For example: 51 | 52 | .. code-block:: python 53 | 54 | @generic 55 | def multiple_params_func(a, b, c): 56 | return 0 # default implementation 57 | 58 | @multiple_params_func.when(lambda b: b > 10) # only inject argument `b` to the predicate 59 | def _when_b_greater_than_10(a): # only inject `a` to the implementation 60 | return a * 10 61 | 62 | @multiple_params_func.when(lambda a, b: a % b == 0) # only inject `a` and `b` 63 | def _when_a_divisible_by_b(a, b, c): # use all arguments 64 | return a / b * c 65 | 66 | However the call site must list all mandatory arguments, as usual in Python: 67 | 68 | .. code-block:: python 69 | 70 | multiple_params_func(10, 20, 30) # --> 100 [_when_b_great_than_10() invoked] 71 | multiple_params_func(4, 2, 'bla') # --> 'blabla' [_when_a_divisible_by_b() invoked] 72 | multiple_params_func(1, 2, 3) # --> 0 [default implementation invoked] 73 | -------------------------------------------------------------------------------- /conftest.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AvivC/genericfuncs/eb11d0379c41847bcfc53f3d569f72436eb84875/conftest.py -------------------------------------------------------------------------------- /genericfuncs.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals, division, print_function, absolute_import 2 | 3 | import collections 4 | import inspect 5 | from collections import namedtuple 6 | import functools 7 | from itertools import imap 8 | 9 | 10 | class generic(object): 11 | """ 12 | A decorator to turn functions into generic functions. 13 | Upon invocation of a generic function, the registered predicates are invoked 14 | in order of registration, passing in the arguments to the function. 15 | The first predicate returning True, has its mapped implementation invoked. 16 | 17 | A predicate may be any one of the following: 18 | Types, any boolean callable, or lists of predicates (with AND relations): 19 | 20 | @genericfuncs.generic 21 | def func(a): 22 | # default implementation 23 | raise TypeError() 24 | 25 | @func.when(int) # dispatch on type 26 | def _when_int(a): 27 | return a * a 28 | 29 | @func.when(lambda a: a == 'magic') # dispatch on arbitrary predicates 30 | def _when_magic_word(a): 31 | return a.upper() 32 | 33 | @func.when([float, lambda a: a < 0]) # dispatch on multiple predicates 34 | def _when_float_and_negative(a): 35 | return a * -1 36 | 37 | func(10) --> 100 # _when_int invoked 38 | func('magic') --> 'MAGIC' # _when_magic_word invoked 39 | func(-5.5) --> 5.5 # _when_float_and_negative 40 | func(Something()) --> TypeError raised # default implementation invoked 41 | 42 | Arguments are injected into predicates by their name. This allows predicates to list which of the 43 | arguments they want to consider. 44 | 45 | The same goes for implementations - the parameters are injected by name, and not all arguments must be listed. 46 | 47 | @generic 48 | def multiple_params_func(a, b, c): 49 | return a + b + c # default implementation 50 | 51 | @multiple_params_func.when(lambda b: b > 10) # only inject paramter `b` to the predicate 52 | def _when_b_greater_than_10(a): # only inject `a` 53 | return a * 10 54 | 55 | @multiple_params_func.when(lambda a, b: a % b == 0) # inject only `a` and `b` 56 | def _when_a_divisible_by_c(a, b, c): 57 | return a / b * c 58 | 59 | The call site however, must always list all mandatory arguments, as always. 60 | 61 | multiple_params_func(10, 20, 30) --> 100 # _when_b_great_than_10 invoked 62 | multiple_params_func(4, 2, 'bla') --> 'blabla' # _when_a_divisible_by_c invoked 63 | multiple_params_func(0, 0, 0) --> 0 # default implementation invoked 64 | 65 | More info about the when() decorator can be found in its docs. 66 | """ 67 | 68 | def __init__(self, wrapped): 69 | # allow passing in ready _FunctionInfo objects 70 | self._base_func = wrapped if isinstance(wrapped, _FunctionInfo) else _FunctionInfo(wrapped) 71 | self._predicates_and_funcs = [] 72 | # functools.update_wrapper(self, wrapped) 73 | 74 | def __call__(self, *args, **kwargs): 75 | self._validate_args(args, kwargs) 76 | 77 | for predicate, function in self._predicates_and_funcs: 78 | if predicate(*args, **kwargs): 79 | return function(*args, **kwargs) 80 | 81 | return self._base_func(*args, **kwargs) 82 | 83 | def _validate_args(self, args, kwargs): 84 | if any(kwarg not in self._base_func.args for kwarg in kwargs): 85 | raise ValueError('One or more keyword arguments don\'t exist in the generic function.') 86 | if len(args) > len(self._base_func.args): 87 | raise ValueError('Received too many positional arguments.') 88 | 89 | def when(self, predicate_source, type=None): 90 | """ 91 | A decorator used to register an implementation to a generic function. 92 | The decorator takes a predicate, to which the implementation will be mapped. 93 | Upon invocation of the generic function, the first implementation whose 94 | predicate returned True will be invoked. 95 | 96 | :param predicate_source: The predicate may be any one of the following options: 97 | A type (meaning an `isinstance()` check), a callable that returns a boolean, 98 | or a list of predicates (with AND relations between them): 99 | """ 100 | predicate = self.make_predicate(predicate_source, prepend_typecheck=type) 101 | 102 | def dec(func): 103 | impl_info = _PartialFunction(func, self._base_func) 104 | 105 | if not self._all_params_valid(predicate): 106 | raise ValueError('Argument specified in predicate doesn\'t exist in base function.') 107 | if not self._all_params_valid(impl_info): 108 | raise ValueError('Argument specified in implementation doesn\'t exist in base function.') 109 | 110 | self._predicates_and_funcs.append(_PredicateFunctionMappping(predicate, impl_info)) 111 | return func 112 | 113 | return dec 114 | 115 | def make_predicate(self, predicate_source, prepend_typecheck=None): 116 | if isinstance(predicate_source, collections.Callable): 117 | predicate = self._make_predicate_from_callable(predicate_source) 118 | elif isinstance(predicate_source, dict): 119 | predicate = self._make_predicate_from_dict(predicate_source) 120 | elif isinstance(predicate_source, collections.Iterable): # this check must appear after the dict check 121 | predicate = self._make_predicate_from_iterable(predicate_source) 122 | else: 123 | raise TypeError('Input to when() is not a callable, a dict or an iterable of callables.') 124 | 125 | if prepend_typecheck is None: 126 | return predicate 127 | else: 128 | return self._prepend_typecheck_to_predicate(prepend_typecheck, predicate) 129 | 130 | def _make_predicate_from_callable(self, predicate_source): 131 | if isinstance(predicate_source, _PartialFunction): 132 | return predicate_source # allow passing already ready predicates, but return them as is 133 | 134 | elif inspect.isfunction(predicate_source) or inspect.ismethod(predicate_source): 135 | return _PartialFunction(predicate_source, self._base_func) 136 | 137 | elif isinstance(predicate_source, type): 138 | return self._make_type_predicate(predicate_source) 139 | 140 | else: # different callable object 141 | return _PartialFunction(predicate_source.__call__, self._base_func) 142 | 143 | def _make_predicate_from_dict(self, predicate_dict): 144 | def predicate(*args, **kwargs): 145 | for arg_name, arg_predicate_source in predicate_dict.iteritems(): 146 | arg_value = self._base_func.get_arg_value(arg_name, args, kwargs) 147 | 148 | gen = generic(_FunctionInfo(args=[arg_name])) 149 | arg_predicate = gen.make_predicate(arg_predicate_source) 150 | 151 | if not arg_predicate(arg_value): 152 | return False 153 | return True 154 | 155 | return _PartialFunction(predicate, self._base_func, self._base_func.args) 156 | 157 | def _make_type_predicate(self, predicate): 158 | if isinstance(predicate, type): 159 | def type_predicate(*args, **kwargs): 160 | return all(isinstance(obj, predicate) for obj in args) \ 161 | and all(isinstance(obj, predicate) for key, obj in kwargs.iteritems()) 162 | 163 | elif isinstance(predicate, dict): 164 | if any((not isinstance(value, (type, collections.Iterable))) for value in predicate.values()): 165 | raise TypeError('In a dict that maps arguments to expected types, ' 166 | 'the values must be either types or iterables of types.') 167 | 168 | def type_predicate(*args, **kwargs): 169 | for arg_name, expected_arg_type in predicate.iteritems(): 170 | arg_value = self._base_func.get_arg_value(arg_name, args, kwargs) 171 | 172 | if isinstance(expected_arg_type, collections.Iterable): 173 | expected_arg_type = tuple(expected_arg_type) 174 | 175 | if not isinstance(arg_value, expected_arg_type): 176 | return False 177 | return True 178 | 179 | else: 180 | raise TypeError('A type predicate may be created from a type or a dictionary.') 181 | 182 | return _PartialFunction(type_predicate, self._base_func, args=self._base_func.args) 183 | 184 | def _make_predicate_from_iterable(self, predicates, aggregator=all): 185 | partial_func_predicates = map(self.make_predicate, predicates) 186 | 187 | def composed_predicates(*args, **kwargs): 188 | return aggregator(predicate(*args, **kwargs) for predicate in partial_func_predicates) 189 | 190 | return _PartialFunction(composed_predicates, self._base_func, args=self._base_func.args) 191 | 192 | def _prepend_typecheck_to_predicate(self, prepend_typecheck, predicate): 193 | if isinstance(prepend_typecheck, (type, dict)): 194 | type_checker = self._make_type_predicate(prepend_typecheck) 195 | elif isinstance(prepend_typecheck, collections.Iterable): 196 | type_checker = self._make_predicate_from_iterable(prepend_typecheck, aggregator=any) 197 | else: 198 | raise ValueError('type optional argument to when() has to be a type or an iterable of types. ' 199 | 'Can\'t be a {}.'.format(type(prepend_typecheck))) 200 | 201 | return self._make_predicate_from_iterable([type_checker, predicate]) 202 | 203 | def _all_params_valid(self, function_info): 204 | return all(arg in self._base_func.args for arg in function_info.args) 205 | 206 | 207 | _PredicateFunctionMappping = namedtuple('PredicateFunctionMappping', ['predicate_info', 'func_info']) 208 | 209 | 210 | class _FunctionInfo(object): 211 | def __init__(self, function=None, args=None): 212 | self._function = function if function is not None else lambda *args, **kwargs: None 213 | 214 | if args is None: 215 | self.args = function.__code__.co_varnames[:function.__code__.co_argcount] 216 | if inspect.ismethod(function): 217 | self.args = self.args[1:] # strip self argument 218 | else: 219 | self.args = args 220 | 221 | def __call__(self, *args, **kwargs): 222 | return self._function(*args, **kwargs) 223 | 224 | def get_arg_value(self, arg_name, input_args, input_kwargs): 225 | try: 226 | return input_kwargs[arg_name] 227 | except KeyError: 228 | pass 229 | try: 230 | arg_index = self.args.index(arg_name) 231 | return input_args[arg_index] 232 | except IndexError: 233 | raise ValueError('Specified argument doesn\'t exist in generic function.') 234 | 235 | 236 | class _PartialFunction(_FunctionInfo): 237 | """A _PartialFunction is a function that is only interested 238 | in some of the arguments to another function (the base function). 239 | For example, the base function might take parameters a, b and c, 240 | but a partial function over it might only be interested in parameters a and c. 241 | 242 | Upon invocation of a partial function, the input arguments are filtered to leave 243 | only those that are of interest to the partial function. 244 | This is done using the argument names specified by the base function (via self._base_function.args) 245 | and the args of the partial function (self.args). 246 | """ 247 | 248 | def __init__(self, function, base_function, args=None): 249 | super(_PartialFunction, self).__init__(function, args) 250 | self._base_function = base_function 251 | 252 | def __call__(self, *args, **kwargs): 253 | partial_args = self._find_arg_values(args) 254 | partial_kwargs = {k: v for k, v in kwargs.iteritems() if k in self.args} 255 | return self._function(*partial_args, **partial_kwargs) 256 | 257 | def _find_arg_values(self, input_arg_values): 258 | unnecessary_arg_names = set(self._base_function.args) - set(self.args) 259 | unnecessary_arg_indexes = [self._base_function.args.index(arg_name) for arg_name in unnecessary_arg_names] 260 | return [arg_value for index, arg_value in enumerate(input_arg_values) 261 | if index not in unnecessary_arg_indexes] 262 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | from os import path 3 | 4 | _here = path.dirname(path.abspath(__file__)) 5 | _readme = open(path.join(_here, 'README.rst')).read() 6 | 7 | setup( 8 | name='genericfuncs', 9 | description='Dynamic dispatch over arbitrary predicates', 10 | long_description=_readme, 11 | version='0.2.0', 12 | url='https://github.com/AvivC/genericfuncs', 13 | author='Aviv Cohn', 14 | author_email='avivcohn123@yahoo.com', 15 | license='MIT', 16 | platform='any', 17 | py_modules=['genericfuncs'], 18 | classifiers=[ 19 | 'Development Status :: 3 - Alpha', 20 | 'Programming Language :: Python', 21 | 'Programming Language :: Python :: 2', 22 | 'Programming Language :: Python :: 2.7', 23 | 'Intended Audience :: Developers', 24 | 'License :: OSI Approved :: MIT License', 25 | 'Topic :: Software Development :: Libraries', 26 | 'Topic :: Software Development :: Libraries :: Python Modules' 27 | ], 28 | keywords='generic functions utility programming development' 29 | ) 30 | -------------------------------------------------------------------------------- /test/test_basic.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | from __future__ import division 3 | 4 | import pytest 5 | import genericfuncs 6 | 7 | 8 | def test_genfunc_with_only_default_impl(): 9 | @genericfuncs.generic 10 | def genfunc(n): 11 | return n * 2 12 | assert genfunc(4) == 8 13 | assert genfunc(n=4) == 8 14 | 15 | 16 | def test_correct_genfunc_impl_invoked(): 17 | @genericfuncs.generic 18 | def genfunc(a, b, c): 19 | return 'default impl' 20 | 21 | @genfunc.when(lambda a, b, c: a > b > c) 22 | def when_a_largerthan_b_largerthan_c(a, b, c): 23 | return 'a > b > c' 24 | 25 | @genfunc.when(long) 26 | def when_all_params_long(a, b, c): 27 | return 'all are long' 28 | 29 | @genfunc.when(lambda a, b, c: a < b < c) 30 | def when_a_lessthan_b_lessthan_c(a, b, c): 31 | return 'a < b < c' 32 | 33 | class C1(object): 34 | def predicate(self, a, b, c): 35 | return a * b * c == 0 36 | 37 | @genfunc.when(C1().predicate) 38 | def when_one_or_more_params_is_zero(a, b, c): 39 | return 'one or more is 0' 40 | 41 | class C2(object): 42 | def __call__(self, a, b, c): 43 | return a == b == c == 8 44 | 45 | @genfunc.when(C2()) 46 | def when_all_equal_eight(a, b, c): 47 | return 'a == b == c == 8' 48 | 49 | assert genfunc(4, 4, 4) == 'default impl' 50 | assert genfunc(5, 3, 2) == 'a > b > c' 51 | assert genfunc(10L, 20L, 1L) == 'all are long' 52 | assert genfunc(1, 10, 30) == 'a < b < c' 53 | assert genfunc(2, 10, 0) == 'one or more is 0' 54 | assert genfunc(8, 8, 8) == 'a == b == c == 8' 55 | 56 | 57 | def test_invalid_predicate_raises_exception(): 58 | @genericfuncs.generic 59 | def genfunc(n): 60 | return n * 2 61 | 62 | with pytest.raises(TypeError): 63 | @genfunc.when(10) 64 | def impl(n): 65 | return 'should never run' 66 | 67 | 68 | def test_parameter_injection(): 69 | @genericfuncs.generic 70 | def genfunc(a, b, c, d): 71 | return locals() 72 | 73 | @genfunc.when(lambda a, b, c, d: a == 1 and b == 2 and c == 3 and d == 4) 74 | def _(a, b, c, d): 75 | return locals() 76 | 77 | @genfunc.when(lambda a, b, c: a == 1 and b == 2 and c == 3 and 'd' not in locals()) 78 | def _(a, b, c): 79 | return locals() 80 | 81 | @genfunc.when(lambda b, c, d: 'a' not in locals() and b == 1 and c == 2 and d == 3) 82 | def _(b, c, d): 83 | return locals() 84 | 85 | @genfunc.when(lambda b, c: 'a' not in locals() and b == 1 and c == 2 and 'd' not in locals()) 86 | def _(b, c): 87 | return locals() 88 | 89 | assert genfunc(1, 2, 3, 4) == {'a': 1, 'b': 2, 'c': 3, 'd': 4} 90 | assert genfunc(1, 2, 3, 0) == {'a': 1, 'b': 2, 'c': 3} 91 | assert genfunc(0, 1, 2, 3) == {'b': 1, 'c': 2, 'd': 3} 92 | assert genfunc(0, 1, 2, 0) == {'b': 1, 'c': 2} 93 | assert genfunc(0, 0, 0, 0) == {'a': 0, 'b': 0, 'c': 0, 'd': 0} 94 | 95 | with pytest.raises(ValueError) as exc_info: 96 | @genfunc.when(lambda a, b, c, d, e: True) 97 | def _(b, c): 98 | return locals() 99 | assert "Argument specified in predicate doesn\'t exist in base function." in str(exc_info) 100 | 101 | with pytest.raises(ValueError) as exc_info: 102 | @genfunc.when(lambda a, b, c, d: True) 103 | def _(b, c, e): 104 | return locals() 105 | assert "Argument specified in implementation doesn\'t exist in base function." in str(exc_info) 106 | 107 | 108 | def test_multiple_predicates(): 109 | @genericfuncs.generic 110 | def genfunc(a, b, c): 111 | return 'default' 112 | 113 | @genfunc.when([lambda a, b, c: a == 10 and b == 20 and c == 30, lambda a, b, c: c > b > a]) 114 | def _(a, b, c): 115 | return locals() 116 | 117 | @genfunc.when([lambda b: b == 50, lambda a, c: c > a]) 118 | def _(a, b, c): 119 | return locals() 120 | 121 | @genfunc.when([lambda b: b == 60, lambda a, c: c > a]) 122 | def _(c): 123 | return locals() 124 | 125 | @genfunc.when([lambda b: b == 'paramb', lambda a, c: a == 'parama' and c == 'paramc', basestring]) 126 | def _(c): 127 | return locals() 128 | 129 | @genfunc.when([float, int]) # should never run 130 | def _(a, b, c): 131 | return locals() 132 | 133 | assert genfunc(10, 20, 30) == {'a': 10, 'b': 20, 'c': 30} 134 | assert genfunc(10, 50, 30) == {'a': 10, 'b': 50, 'c': 30} 135 | assert genfunc(10, 60, 30) == {'c': 30} 136 | assert genfunc('parama', 'paramb', 'paramc') == {'c': 'paramc'} 137 | assert genfunc(10, 1.5, 5) == 'default' 138 | 139 | 140 | def test_genfunc_call_with_keyword_arguments(): 141 | @genericfuncs.generic 142 | def genfunc(a, b, c): 143 | return 'default' 144 | 145 | @genfunc.when(lambda a, b, c: a > b > c) 146 | def _(a, b, c): 147 | return [a, b, c] 148 | 149 | assert genfunc(1, 1, 1) == 'default' 150 | assert genfunc(a=1, b=1, c=1) == 'default' 151 | assert genfunc(1, 1, c=1) == 'default' 152 | assert genfunc(1, b=1, c=1) == 'default' 153 | with pytest.raises(TypeError) as exc_info: 154 | genfunc(1, 1, 1, c=1) 155 | assert 'got multiple values for keyword argument' in str(exc_info) 156 | with pytest.raises(TypeError) as exc_info: 157 | genfunc(1, 1, 1, b=1, c=1) 158 | assert 'got multiple values for keyword argument' in str(exc_info) 159 | 160 | assert genfunc(3, 2, 1) == [3, 2, 1] 161 | assert genfunc(a=3, b=2, c=1) == [3, 2, 1] 162 | assert genfunc(3, 2, c=1) == [3, 2, 1] 163 | with pytest.raises(TypeError) as exc_info: 164 | genfunc(3, 2, 1, b=2, c=1) 165 | assert 'got multiple values for keyword argument' in str(exc_info) 166 | 167 | 168 | def test_invalid_genfunc_calls_raise_error(): 169 | @genericfuncs.generic 170 | def genfunc(a, b, c): 171 | return 'default' 172 | 173 | with pytest.raises(ValueError) as exc_info: 174 | genfunc(1, 2, 3, 4) 175 | assert 'Received too many positional arguments.' in str(exc_info) 176 | 177 | with pytest.raises(ValueError) as exc_info: 178 | genfunc(1, 2, d=3) 179 | assert 'One or more keyword arguments don\'t exist in the generic function.' in str(exc_info) 180 | 181 | 182 | def test_predicates_with_type_precondition_all_same_type(): 183 | @genericfuncs.generic 184 | def genfunc(a): 185 | return 'default' 186 | 187 | @genfunc.when(lambda a: a > 5, type=int) 188 | def _(a): 189 | return 'a > 5' 190 | 191 | @genfunc.when(lambda a: a < 5, type=int) 192 | def _(a): 193 | return 'a < 5' 194 | 195 | assert genfunc(6) == 'a > 5' 196 | assert genfunc(4) == 'a < 5' 197 | assert genfunc(5) == 'default' 198 | 199 | 200 | def test_predicates_with_type_precondition_different_types(): 201 | # normally in Python, trying to evaluate the first predicate here - `lambda a: a % 2 == 0` - 202 | # where type(a) is unicode, would raise a TypeError and crash the program. 203 | # obviously that's because unicode doesn't support operator %. 204 | 205 | # however here, when encountering the first predicate, an exception should not be raised. 206 | # this is because before running the predicate, the precondition created by specifying `type=int` is checked. 207 | # if it is False, the predicate is never run, and a TypeError is never raised. 208 | # this is the point of having the `type=something` option. 209 | 210 | # the `type` optional parameter should be used when predicates that expect different types are specified. 211 | 212 | @genericfuncs.generic 213 | def genfunc(a): 214 | return 'default' 215 | 216 | @genfunc.when(lambda a: a % 2 == 0, type=[int, float]) 217 | def _(a): 218 | return 'a % 2 == 0' 219 | 220 | @genfunc.when(lambda a: a.startswith('bar')) 221 | def _(a): 222 | return 'a.startswith(\'bar\')' 223 | 224 | @genfunc.when(lambda a: a.endswith('foo'), type=basestring) 225 | def _(a): 226 | return 'a.endswith(\'foo\')' 227 | 228 | assert genfunc(8) == 'a % 2 == 0' 229 | assert genfunc(8.0) == 'a % 2 == 0' 230 | # pytest.set_trace() 231 | assert genfunc(a=8) == 'a % 2 == 0' 232 | 233 | assert genfunc('abcfoo') == 'a.endswith(\'foo\')' 234 | assert genfunc(a='abcfoo') == 'a.endswith(\'foo\')' 235 | 236 | with pytest.raises(AttributeError): 237 | genfunc(5) # an AttributeError should be raised inside the second predicate (`a.startswith('bar')`), 238 | # because int objects don't have the `startswith` methodd. 239 | # the predicate is allowed to run, because `type=basestring` wasn't specified. 240 | 241 | assert genfunc('abc') == 'default' 242 | 243 | 244 | def test_type_precondition_as_dict(): 245 | @genericfuncs.generic 246 | def genfunc(a, b): 247 | return 'default' 248 | 249 | @genfunc.when(lambda a, b: a > b, type={'a': int, 'b': int}) 250 | def _(a): 251 | return 'a > b' 252 | 253 | @genfunc.when(lambda b: b.endswith('bar'), type={'b': basestring}) 254 | def _(a): 255 | return 'b.endswith(\'bar\')' 256 | 257 | @genfunc.when(lambda a, b: len(a) == b, type={'a': [list, tuple], 'b': int}) 258 | def _(a): 259 | return 'len(a) == b' 260 | 261 | assert genfunc(15, 10) == 'a > b' 262 | assert genfunc(15, b=10) == 'a > b' 263 | assert genfunc(a=15, b=10) == 'a > b' 264 | 265 | assert genfunc('aaa', 'blabar') == 'b.endswith(\'bar\')' 266 | assert genfunc('aaa', b='blabar') == 'b.endswith(\'bar\')' 267 | assert genfunc(a='aaa', b='blabar') == 'b.endswith(\'bar\')' 268 | 269 | assert genfunc([1, 2, 3], 3) == 'len(a) == b' 270 | assert genfunc((1, 2, 3), 3) == 'len(a) == b' 271 | assert genfunc((1, 2, 3), b=3) == 'len(a) == b' 272 | assert genfunc(a=(1, 2, 3), b=3) == 'len(a) == b' 273 | 274 | assert genfunc(5, 5) == 'default' 275 | assert genfunc('', b='abc') == 'default' 276 | assert genfunc(a='', b='abc') == 'default' 277 | assert genfunc([], {}) == 'default' 278 | assert genfunc([1, 2, 3], 4) == 'default' 279 | 280 | with pytest.raises(TypeError) as exc_info: 281 | @genfunc.when(lambda a, b: True, type={'a': int, 'b': lambda b: ''}) 282 | def _(a, b): 283 | return '' 284 | assert 'In a dict that maps arguments to expected types, the values must be either types or iterables of types.' \ 285 | in str(exc_info) 286 | 287 | 288 | def test_dict_as_predicate(): 289 | @genericfuncs.generic 290 | def genfunc(a, b): 291 | return 'default' 292 | 293 | @genfunc.when({ 294 | 'a': lambda a: a == 5, 295 | 'b': lambda b: b == 10 296 | }) 297 | def _(a, b): 298 | return 'a == 5 and b == 10' 299 | 300 | @genfunc.when({ 301 | 'b': int 302 | }) 303 | def _(a, b): 304 | return 'b is an int' 305 | 306 | @genfunc.when({ 307 | 'a': [int, lambda a: a % 2 == 0, lambda a: a > 20] 308 | }) 309 | def _(a, b): 310 | return 'a is divisibe by 2 and greater than 20' 311 | 312 | @genfunc.when({ 313 | 'a': lambda a: a.startswith('foo') 314 | }, type=basestring) 315 | def _(a, b): 316 | return 'a starts with foo and all args are strings' 317 | 318 | @genfunc.when({ 319 | 'a': lambda a: a.startswith('foo') 320 | }, type={ 321 | 'a': basestring, 322 | 'b': float 323 | }) 324 | def _(a, b): 325 | return 'a starts with foo, a is a string and b is a float' 326 | 327 | # not currently supported 328 | # @genfunc.when({ 329 | # 'a': lambda a: a.startswith('foo') 330 | # }, type=[basestring, list]) 331 | # def _(a, b): 332 | # return 'a starts with foo and all are either strings or lists' 333 | 334 | assert genfunc(5, 10) == 'a == 5 and b == 10' 335 | assert genfunc('bla', b=12) == 'b is an int' 336 | assert genfunc(a='bla', b=12) == 'b is an int' 337 | assert genfunc(5, 15) == 'b is an int' 338 | assert genfunc(30, 'bla') == 'a is divisibe by 2 and greater than 20' 339 | assert genfunc('foobla', 'bla') == 'a starts with foo and all args are strings' 340 | # not currently supported 341 | # assert genfunc('foobla', []) == 'a starts with foo and all are either strings or lists' 342 | assert genfunc([], []) == 'default' 343 | assert genfunc([], b=[]) == 'default' 344 | assert genfunc(a=[], b=[]) == 'default' 345 | 346 | 347 | -------------------------------------------------------------------------------- /user.py: -------------------------------------------------------------------------------- 1 | import genericfuncs 2 | 3 | 4 | @genericfuncs.generic 5 | def genfunc(a, b): 6 | return 'default' 7 | 8 | 9 | @genfunc.when({ 10 | 'b': basestring 11 | }) 12 | def _(a, b): 13 | return 'b is a basestring' 14 | 15 | 16 | assert genfunc(10, 'abc') == 'b is a basestring' 17 | --------------------------------------------------------------------------------