├── .gitignore ├── LICENSE ├── MANIFEST.in ├── README.rst ├── function_pattern_matching.py ├── requirements.txt ├── setup.cfg ├── setup.py └── tests ├── py3_defs.py └── test_fpm.py /.gitignore: -------------------------------------------------------------------------------- 1 | .* 2 | !/.gitignore 3 | __pycache__ 4 | *.pyc 5 | *.bak 6 | *.swp 7 | doc/_build 8 | doc/_static 9 | dist 10 | build 11 | *.egg-info 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Adrian Włosiak 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 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include README.rst 3 | include requirements.txt 4 | include setup.cfg 5 | include setup.py 6 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | function-pattern-matching 2 | ************************* 3 | 4 | **function-pattern-matching** (**fpm** for short) is a module which introduces Erlang-style 5 | `multiple clause defined functions `_ and 6 | `guard sequences `_ to Python. 7 | 8 | This module is both Python 2 and 3 compatible. 9 | 10 | .. contents:: Table of contents 11 | 12 | Introduction 13 | ============ 14 | 15 | Two families of decorators are introduced: 16 | 17 | - ``case``: allows multiple function clause definitions and dispatches to correct one. Dispatch happens on the values 18 | of call arguments or, more generally, when call arguments' values match specified guard definitions. 19 | 20 | - ``dispatch``: convenience decorator for dispatching on argument types. Equivalent to using ``case`` and ``guard`` 21 | with type checking. 22 | 23 | - ``guard``: allows arguments' values filtering and raises ``GuardError`` when argument value does not pass through 24 | argument guard. 25 | 26 | - ``rguard``: Wrapper for ``guard`` which converts first positional decorator argument to relguard. See Relguards_. 27 | 28 | - ``raguard``: Like ``rguard``, but converts return annotation. See Relguards_. 29 | 30 | Usage example: 31 | 32 | - All Python versions: 33 | 34 | .. code-block:: python 35 | 36 | import function_pattern_matching as fpm 37 | 38 | @fpm.case 39 | def factorial(n=0): 40 | return 1 41 | 42 | @fpm.case 43 | @fpm.guard(fpm.is_int & fpm.gt(0)) 44 | def factorial(n): 45 | return n * factorial(n - 1) 46 | 47 | - Python 3 only: 48 | 49 | .. code-block:: python 50 | 51 | import function_pattern_matching as fpm 52 | 53 | @fpm.case 54 | def factorial(n=0): 55 | return 1 56 | 57 | @fpm.case 58 | @fpm.guard 59 | def factorial(n: fpm.is_int & fpm.gt(0)): # Guards specified as annotations 60 | return n * factorial(n - 1) 61 | 62 | Of course that's a poor implementation of factorial, but illustrates the idea in a simple way. 63 | 64 | **Note:** This module does not aim to be used on production scale or in a large sensitive application (but I'd be 65 | happy if someone decided to use it in his/her project). I think of it more as a fun project which shows how 66 | flexible Python can be (and as a good training for myself). 67 | 68 | I'm aware that it's somewhat against duck typing and EAFP (easier to ask for forgiveness than for permission) 69 | philosophy employed by the language, but obviously there *are* some cases when preliminary checks are useful and 70 | make code (and life) much simpler. 71 | 72 | Installation 73 | ============ 74 | 75 | function-pattern-matching can be installed with pip:: 76 | 77 | $ pip install function-pattern-matching 78 | 79 | Module will be available as ``function_pattern_matching``. It is recommended to import as ``fpm``. 80 | 81 | Usage 82 | ===== 83 | 84 | Guards 85 | ------ 86 | 87 | With ``guard`` decorator it is possible to filter function arguments upon call. When argument value does not pass 88 | through specified guard, then ``GuardError`` is raised. 89 | 90 | When global setting ``strict_guard_definitions`` is set ``True`` (the default value), then only ``GuardFunc`` 91 | instances can be used in guard definitions. If it's set to ``False``, then any callable is allowed, but it is **not** 92 | recommended, as guard behaviour may be unexpected (``RuntimeWarning`` is emitted), e.g. combining regular callables 93 | will not work. 94 | 95 | ``GuardFunc`` objects can be negated with ``~`` and combined together with ``&``, ``|`` and ``^`` logical operators. 96 | Note however, that *xor* isn't very useful here. 97 | 98 | **Note:** It is not possible to put guards on varying arguments (\*args, \**kwargs). 99 | 100 | List of provided guard functions 101 | ................................ 102 | 103 | Every following function returns/is a callable which takes only one parameter - the call argument that is to be 104 | checked. 105 | 106 | - ``_`` - Catch-all. Returns ``True`` for any input. Actually, this can take any number of arguments. 107 | - ``eq(val)`` - checks if input is equal to *val* 108 | - ``ne(val)`` - checks if input is not equal to *val* 109 | - ``lt(val)`` - checks if input is less than *val* 110 | - ``le(val)`` - checks if input is less or equal to *val* 111 | - ``gt(val)`` - checks if input is greater than *val* 112 | - ``ge(val)`` - checks if input is greater or equal to *val* 113 | - ``Is(val)`` - checks if input is *val* (uses ``is`` operator) 114 | - ``Isnot(val)`` - checks if input is not *val* (uses ``is not`` operator) 115 | - ``isoftype(_type)`` - checks if input is instance of *_type* (uses ``isintance`` function) 116 | - ``isiterable`` - checks if input is iterable 117 | - ``eTrue`` - checks if input evaluates to ``True`` (converts input to ``bool``) 118 | - ``eFalse`` - checks if input evaluates to ``False`` (converts input to ``bool``) 119 | - ``In(val)`` - checks if input is in *val* (uses ``in`` operator) 120 | - ``notIn(val)`` - checks if input is not in *val* (uses ``not in`` operator) 121 | 122 | Custom guards 123 | ............. 124 | 125 | Although it is not advised (at least for simple checks), you can create your own guards: 126 | 127 | - by using ``makeguard`` decorator on your test function. 128 | 129 | - by writing a function that returns a ``GuardFunc`` object initialised with a test function. 130 | 131 | Note that a test function must have only one positional argument. 132 | 133 | Examples: 134 | 135 | .. code-block:: python 136 | 137 | # use decorator 138 | @fpm.makeguard 139 | def is_not_zero_nor_None(inp): 140 | return inp != 0 and inp is not None 141 | 142 | # return GuardFunc object 143 | def is_not_val_nor_specified_thing(val, thing): 144 | return GuardFunc(lambda inp: inp != val and inp is not thing) 145 | 146 | # equivalent to (fpm.ne(0) & fpm.Isnot(None)) | (fpm.ne(1) & fpm.Isnot(some_object)) 147 | @fpm.guard(is_not_zero_nor_None | is_not_val_nor_specified_thing(1, some_object)) 148 | def guarded(argument): 149 | pass 150 | 151 | The above two are very similar, but the second one allows creating function which takes multiple arguments to construct 152 | actual guard. 153 | 154 | **Note:** It is not recommended to create your own guard functions. In most cases combinations of the ones shipped with 155 | fpm should be all you need. 156 | 157 | Define guards for function arguments 158 | .................................... 159 | 160 | There are two ways of defining guards: 161 | 162 | - As decorator arguments 163 | 164 | - positionally: guards order will match decoratee's (the function that is to be decorated) arguments order. 165 | 166 | .. code-block:: python 167 | 168 | @fpm.guard(fpm.isoftype(int) & fpm.ge(0), fpm.isiterable) 169 | def func(number, iterable): 170 | pass 171 | 172 | - as keyword arguments: e.g. guard under name *a* will guard decoratee's argument named *a*. 173 | 174 | .. code-block:: python 175 | 176 | @fpm.guard( 177 | number = fpm.isoftype(int) & fpm.ge(0), 178 | iterable = fpm.isiterable 179 | ) 180 | def func(number, iterable): 181 | pass 182 | 183 | - As annotations (Python 3 only) 184 | 185 | .. code-block:: python 186 | 187 | @fpm.guard 188 | def func( 189 | number: fpm.isoftype(int) & fpm.ge(0), 190 | iterable: fpm.isiterable 191 | ): # this is NOT an emoticon 192 | pass 193 | 194 | If you try to declare guards using both methods at once, then annotations get ignored and are left untouched. 195 | 196 | Relguards 197 | --------- 198 | 199 | Relguard is a kind of guard that checks relations between arguments (and/or external variables). ``fpm`` implements 200 | them as functions (wrapped in ``RelGuard`` object) whose arguments are a subset of decoratee's arguments (no arguments 201 | is fine too). 202 | 203 | Define relguard 204 | ............... 205 | 206 | There are a few ways of defining a relguard. 207 | 208 | - Using ``guard`` with the first (and only) positional non-keyword argument of type ``RelGuard``: 209 | 210 | .. code-block:: python 211 | 212 | @fpm.guard( 213 | fpm.relguard(lambda a, c: a == c), # converts lambda to RelGuard object in-place 214 | a = fpm.isoftype(int) & fpm.eTrue, 215 | b = fpm.Isnot(None) 216 | ) 217 | def func(a, b, c): 218 | pass 219 | 220 | - Using ``guard`` with the return annotation holding a ``RelGuard`` object (Python 3 only): 221 | 222 | .. code-block:: python 223 | 224 | @fpm.guard 225 | def func(a, b, c) -> fpm.relguard(lambda a, b, c: a != b and b < c): 226 | pass 227 | 228 | - Using ``rguard`` with a regular callable as the first (and only) positional non-keyword argument. 229 | 230 | .. code-block:: python 231 | 232 | @fpm.rguard( 233 | lambda a, c: a == c, # rguard will try converting this to RelGuard object 234 | a = fpm.isoftype(int) & fpm.eTrue, 235 | b = fpm.Isnot(None) 236 | ) 237 | def func(a, b, c): 238 | pass 239 | 240 | - Using ``raguard`` with a regular callable as the return annotation. 241 | 242 | .. code-block:: python 243 | 244 | @fpm.raguard 245 | def func(a, b, c) -> lambda a, b, c: a != b and b < c: # raguard will try converting lambda to RelGuard object 246 | pass 247 | 248 | As you can see, when using ``guard`` you have to manually convert functions to ``RelGuard`` objects with ``relguard`` 249 | method. By using ``rguard`` or ``raguard`` decorators you don't need to do it by yourself, and you get a bit cleaner 250 | definition. 251 | 252 | Multiple function clauses 253 | ------------------------- 254 | 255 | With ``case`` decorator you are able to define multiple clauses of the same function. 256 | 257 | When such a function is called with some arguments, then the first matching clause will be executed. Matching clause 258 | will be the one that didn't raise a ``GuardError`` when called with given arguments. 259 | 260 | **Note:** using ``case`` or ``dispatch`` (discussed later) disables default functionality of default argument values. 261 | Functions with varying arguments (\*args, \**kwargs) and keyword-only arguments (py3-only) are not supported. 262 | 263 | Example: 264 | 265 | .. code-block:: python 266 | 267 | @fpm.case 268 | def func(a=0): print("zero!") 269 | 270 | @fpm.case 271 | def func(a=1): print("one!") 272 | 273 | @fpm.case 274 | @fpm.guard(fpm.gt(9000)) 275 | def func(a): print("IT'S OVER 9000!!!") 276 | 277 | @fpm.case 278 | def func(a): print("some var:", a) # catch-all clause 279 | 280 | >>> func(0) 281 | 'zero!' 282 | >>> func(1) 283 | 'one!' 284 | >>> func(9000.1) 285 | "IT'S OVER 9000!!!" 286 | >>> func(1337) 287 | 'some var: 1337' 288 | 289 | If no clause matches, then ``MatchError`` is raised. The example shown above has a catch-all clause, so ``MatchError`` 290 | will never occur. 291 | 292 | Different arities (argument count) are allowed and are dispatched separetely. 293 | 294 | Example: 295 | 296 | .. code-block:: python 297 | 298 | @fpm.case 299 | def func(a=1, b=1, c): 300 | return 1 301 | 302 | @fpm.case 303 | def func(a, b, c): 304 | return 2 305 | 306 | @fpm.case 307 | def func(a=1, b=1, c, d): 308 | return 3 309 | 310 | @fpm.case 311 | def func(a, b, c, d): 312 | return 4 313 | 314 | >>> func(1, 1, 'any') 315 | 1 316 | >>> func(1, 0, 0.5) 317 | 2 318 | >>> func(1, 1, '', '') 319 | 3 320 | >>> func(1, 0, 0, '') 321 | 4 322 | 323 | As you can see, clause order matters only for same-arity clauses. 4-arg catch-all does not affect any 3-arg definition. 324 | 325 | Define multi-claused functions 326 | .............................. 327 | 328 | There are three ways of defining a pattern for a function clause: 329 | 330 | - Specify exact values as decorator arguments (positional and/or keyword) 331 | 332 | .. code-block:: python 333 | 334 | @fpm.case(1, 2, 3) 335 | def func(a, b, c): 336 | pass 337 | 338 | @fpm.case(1, fpm._, 0) 339 | def func(a, b, c): 340 | pass 341 | 342 | @fpm.case(b=10) 343 | def func(a, b, c): 344 | pass 345 | 346 | - Specify exact values as default arguments 347 | 348 | .. code-block:: python 349 | 350 | @fpm.case 351 | def func(a=0): 352 | pass 353 | 354 | @fpm.case 355 | def func(a=10): 356 | pass 357 | 358 | @fpm.case 359 | def func(a=fpm._, b=3): 360 | pass 361 | 362 | - Specify guards for clause to match 363 | 364 | .. code-block:: python 365 | 366 | @fpm.case 367 | @fpm.guard(fpm.eq(0) & ~fpm.isoftype(float)) 368 | def func(a): 369 | pass 370 | 371 | @fpm.case 372 | @fpm.guard(fpm.gt(0)) 373 | def func(a): 374 | pass 375 | 376 | @fpm.case 377 | @fpm.guard(fpm.Is(None)) 378 | def func(a): 379 | pass 380 | 381 | ``dispatch`` decorator 382 | ...................... 383 | 384 | ``dispatch`` decorator is similar to ``case``, but it lets you to define argument types to match against. You can 385 | specify types either as decorator arguments or default values (or as guards, of course, but it makes using ``dispatch`` 386 | pointless). 387 | 388 | Example: 389 | 390 | .. code-block:: python 391 | 392 | @fpm.dispatch(int, int) 393 | def func(a, b): 394 | print("integers") 395 | 396 | @fpm.dispatch 397 | def func(a=float, b=float): 398 | print("floats") 399 | 400 | >>> func(1, 1) 401 | 'integers' 402 | >>> func(1.0, 1.0) 403 | 'floats' 404 | 405 | Examples (the useful ones) 406 | ========================== 407 | 408 | Still working on this section! 409 | 410 | - Ensure that an argument is a list of strings. Prevent feeding string accidentally, which can cause some headache, 411 | since both are iterables. 412 | 413 | - Option 1: do not allow strings 414 | 415 | .. code-block:: python 416 | 417 | # thanks to creshal from HN for suggestion 418 | 419 | lookup = { 420 | "foo": 1, 421 | "bar": 2, 422 | "baz": 3 423 | } 424 | 425 | @fpm.guard 426 | def getSetFromDict( 427 | dict_, # let it throw TypeError if not a dict. Will be more descriptive than a GuardError. 428 | keys: ~fpm.isoftype(str) 429 | ): 430 | "Returns a subset of elements of dict_" 431 | ret_set = set() 432 | for key in keys: 433 | try: 434 | ret_set.add(dict_[key]) 435 | except KeyError: 436 | pass 437 | return ret_set 438 | 439 | getSetFromDict(lookup, ['foo', 'baz', 'not-in-lookup']) # will return two-element set 440 | getSetFromDict(lookup, 'foo') # raises GuardError, but would return empty set without guard! 441 | 442 | Similar solutions 443 | ================= 444 | 445 | - `singledispatch `_ from functools 446 | - `pyfpm `_ 447 | - `patmatch `_ 448 | - http://blog.chadselph.com/adding-functional-style-pattern-matching-to-python.html 449 | - http://www.artima.com/weblogs/viewpost.jsp?thread=101605 (by Guido van Rossum, BDFL) 450 | 451 | License 452 | ======= 453 | 454 | MIT (c) Adrian Włosiak 455 | -------------------------------------------------------------------------------- /function_pattern_matching.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | import six 3 | import types 4 | import warnings 5 | from collections import defaultdict, OrderedDict 6 | try: 7 | from inspect import _ParameterKind as p_kind 8 | except ImportError: 9 | class p_kind(): 10 | POSITIONAL_ONLY = 1 11 | POSITIONAL_OR_KEYWORD = 2 12 | VAR_POSITIONAL = 3 13 | KEYWORD_ONLY = 4 14 | VAR_KEYWORD = 5 15 | try: 16 | from types import SimpleNamespace 17 | except ImportError: 18 | pass 19 | 20 | # GENERAL # 21 | 22 | strict_guard_definitions = True # only instances of GuardFunc and RelGuard can be used as guards. 23 | # if set to False, then any callable is allowed, but may cause unexpected behaviour. 24 | 25 | class GuardError(Exception): 26 | "Guard haven't let argument pass." 27 | pass 28 | 29 | class MatchError(Exception): 30 | "No match for function call." 31 | pass 32 | 33 | class WontMatchError(Exception): 34 | "Case declared after catch-all or identical case." 35 | pass 36 | 37 | class MixedPatternDefinitionError(Exception): 38 | "Raised when pattern is defined both as `case` arguments and function defaults." 39 | pass 40 | 41 | class _(): 42 | """The "don't care" value.""" 43 | def __repr__(self): 44 | return "_" 45 | def __call__(*args, **kwargs): 46 | return True 47 | _ = _() # we need just one and only one instance. 48 | 49 | # GUARDS # 50 | 51 | def _getfullargspec_p(func): 52 | """Gets uniform full arguments specification of func, portably.""" 53 | 54 | try: 55 | sig = inspect.signature(func) 56 | sig_params = sig.parameters 57 | except AttributeError: # signature method not available 58 | # try getfullargspec is present (pre 3.3) 59 | try: 60 | arg_spec = inspect.getfullargspec(func) 61 | except AttributeError: # getfullargspec method not available 62 | arg_spec = inspect.getargspec(func) # py2, trying annotations will fail. 63 | 64 | else: # continue conversion for py >=3.3 65 | arg_spec = SimpleNamespace() # available since 3.3, just like signature, so it's ok to use it here. 66 | 67 | def _arg_spec_helper(kinds=(), defaults=False, kwonlydefaults=False, annotations=False): 68 | for arg_name, param in sig_params.items(): 69 | if not defaults and not kwonlydefaults and not annotations and param.kind in kinds: 70 | yield arg_name 71 | elif sum((defaults, kwonlydefaults, annotations)) > 1: 72 | raise ValueError("Only one of 'defaults', 'kwonlydefaults' or 'annotations' can be True simultaneously") 73 | elif annotations and param.annotation is not inspect._empty: 74 | yield (arg_name, param.annotation) 75 | elif param.default is not inspect._empty: 76 | if defaults and param.kind in (p_kind.POSITIONAL_OR_KEYWORD, p_kind.POSITIONAL_ONLY): 77 | yield param.default 78 | elif kwonlydefaults and param.kind is p_kind.KEYWORD_ONLY: 79 | yield (arg_name, param.default) 80 | 81 | arg_spec.args = tuple(_arg_spec_helper((p_kind.POSITIONAL_OR_KEYWORD, p_kind.POSITIONAL_ONLY))) 82 | arg_spec.varargs = six.next(_arg_spec_helper((p_kind.VAR_POSITIONAL,)), None) 83 | arg_spec.kwonlyargs = tuple(_arg_spec_helper((p_kind.KEYWORD_ONLY,))) 84 | arg_spec.varkw = six.next(_arg_spec_helper((p_kind.VAR_KEYWORD,)), None) 85 | arg_spec.keywords = arg_spec.varkw # getargspec compat 86 | arg_spec.defaults = tuple(_arg_spec_helper(defaults=True)) or None 87 | arg_spec.kwonlydefaults = dict(_arg_spec_helper(kwonlydefaults=True)) 88 | arg_spec.annotations = dict(_arg_spec_helper(annotations=True)) 89 | if sig.return_annotation is not inspect._empty: 90 | arg_spec.annotations['return'] = sig.return_annotation 91 | 92 | return arg_spec 93 | 94 | def _getparams(func): 95 | """Get tuple of arg name and kind pairs.""" 96 | 97 | arg_spec = _getfullargspec_p(func) 98 | 99 | # create tuples for each kind 100 | args = tuple((arg, p_kind.POSITIONAL_OR_KEYWORD) for arg in arg_spec.args) 101 | varargs = ((arg_spec.varargs, p_kind.VAR_POSITIONAL),) if arg_spec.varargs else () 102 | try: 103 | kwonlyargs = tuple((arg, p_kind.KEYWORD_ONLY) for arg in arg_spec.kwonlyargs) 104 | except AttributeError: # python2 105 | kwonlyargs = () 106 | varkw = ((arg_spec.varkw, p_kind.VAR_KEYWORD),) if arg_spec.keywords else () 107 | 108 | return args + varargs + kwonlyargs + varkw 109 | 110 | class GuardFunc(): 111 | """ 112 | Class for guard functions. It checks if function to wrap is defined correctly, returns boolean, doesn't raise 113 | unwanted TypeErrors and allows creating new guards by combining then with logical operators. 114 | """ 115 | 116 | def __init__(self, test): 117 | if not callable(test): 118 | raise ValueError("Guard test has to be callable") 119 | 120 | # check if test signature is ok 121 | test_args = _getparams(test) 122 | 123 | if len(tuple(arg for arg in test_args 124 | if arg[1] in (p_kind.POSITIONAL_ONLY, p_kind.POSITIONAL_OR_KEYWORD))) != 1: 125 | raise ValueError("Guard test has to have only one positional (not varying) argument") 126 | 127 | def apply_try_conv_to_bool(function, arg): 128 | try: 129 | return bool(function(arg)) # test must always return boolean, so convert asap. 130 | except TypeError: # occures when unorderable types are compared, but we don't want TypeError to be raised. 131 | return False # couldn't compare? then guard must say no. 132 | 133 | self.test = lambda inp: apply_try_conv_to_bool(test, inp) 134 | 135 | def _isotherguard(method): 136 | """Checks if `other` is a GuardFunc object.""" 137 | 138 | @six.wraps(method) 139 | def checks(self, other): 140 | if not isinstance(other, GuardFunc): 141 | raise TypeError("The right-hand operand has to be instance of GuardFunc") 142 | return method(self, other) 143 | 144 | return checks 145 | 146 | def __invert__(self): 147 | """ 148 | ~ operator 149 | """ 150 | return GuardFunc(lambda inp: not self.test(inp)) 151 | 152 | @_isotherguard 153 | def __and__(self, other): 154 | """ 155 | & operator 156 | """ 157 | return GuardFunc(lambda inp: (self.test(inp) & other.test(inp))) 158 | 159 | @_isotherguard 160 | def __or__(self, other): 161 | """ 162 | | operator 163 | """ 164 | return GuardFunc(lambda inp: (self.test(inp) | other.test(inp))) 165 | 166 | @_isotherguard 167 | def __xor__(self, other): 168 | """ 169 | ^ operator 170 | """ 171 | return GuardFunc(lambda inp: (self.test(inp) ^ other.test(inp))) 172 | 173 | def __call__(self, inp): 174 | """Forward call to `self.test`""" 175 | return self.test(inp) 176 | 177 | def makeguard(decoratee): 178 | "Decorator which turns decoratee to GuardFunc object." 179 | return GuardFunc(decoratee) 180 | 181 | def eq(val): 182 | "Is inp equal to val." 183 | return GuardFunc(lambda inp: inp == val) 184 | def ne(val): 185 | "Is inp not equal to val." 186 | return GuardFunc(lambda inp: inp != val) 187 | def lt(val): 188 | "Is inp less than val." 189 | return GuardFunc(lambda inp: inp < val) 190 | def le(val): 191 | "Is inp less than or equal to val." 192 | return GuardFunc(lambda inp: inp <= val) 193 | def gt(val): 194 | "Is inp greater than val." 195 | return GuardFunc(lambda inp: inp > val) 196 | def ge(val): 197 | "Is inp greater than or equal to val" 198 | return GuardFunc(lambda inp: inp >= val) 199 | 200 | def Is(val): 201 | "Is inp the same object as val." 202 | return GuardFunc(lambda inp: inp is val) 203 | def Isnot(val): 204 | "Is inp different object than val." 205 | return GuardFunc(lambda inp: inp is not val) 206 | 207 | def isoftype(*types): 208 | "Is inp an instance of any of types." 209 | if len(types) == 1: 210 | types = types[0] 211 | return GuardFunc(lambda inp: isinstance(inp, types)) 212 | 213 | @makeguard 214 | def isiterable(inp): 215 | "Is inp iterable" 216 | try: 217 | iter(inp) 218 | except TypeError: 219 | return False 220 | else: 221 | return True 222 | 223 | @makeguard 224 | def eTrue(inp): 225 | "Does inp evaluate to True." 226 | return bool(inp) 227 | 228 | @makeguard 229 | def eFalse(inp): 230 | "Does inp evaluate to False." 231 | return not bool(inp) 232 | 233 | def In(val): 234 | "Is inp in val" 235 | _ in val # simple test for in support 236 | return GuardFunc(lambda inp: inp in val) 237 | def notIn(val): 238 | "Is inp not in val" 239 | _ in val # simple test for in support 240 | return GuardFunc(lambda inp: inp not in val) 241 | 242 | 243 | class RelGuard(): 244 | """ 245 | Class for relguard functions. It checks if test function is defined correctly, exposes its signature to the `guard` 246 | decorator and passes only the needed arguments to test function when the guarded function is called. 247 | """ 248 | def __init__(self, test): 249 | if not callable(test): 250 | raise ValueError("Relguard test has to be callable") 251 | 252 | # extract simplified signature 253 | test_args = _getparams(test) 254 | 255 | # varying args are not allowed, they make no sense in reguards. 256 | _kinds = {x[1] for x in test_args} 257 | if (p_kind.POSITIONAL_ONLY in _kinds or 258 | p_kind.VAR_POSITIONAL in _kinds or 259 | p_kind.VAR_KEYWORD in _kinds): 260 | raise ValueError("Relguard test must take only named not varying arguments") 261 | 262 | self.test = test 263 | self.__argnames__ = {x[0] for x in test_args} 264 | 265 | def __call__(self, **kwargs): 266 | try: 267 | return bool(self.test(**{arg_name: arg_val for arg_name, arg_val in kwargs.items() if arg_name in self.__argnames__})) 268 | except TypeError: # occures when unorderable types are compared, but we don't want TypeError to be raised. 269 | return False 270 | 271 | def relguard(decoratee): 272 | "Decorator which turns decoratee to RelGuard object." 273 | return RelGuard(decoratee) 274 | 275 | 276 | def guard(*dargs, **dkwargs): 277 | "Checks if arguments meet criteria when the function is called." 278 | 279 | def decorator(decoratee): 280 | "Actual decorator." 281 | 282 | # guard nesting is not allowed 283 | if hasattr(decoratee, '__guarded__'): 284 | raise ValueError("%s() has guards already set" % decoratee.__name__) 285 | # get arg_spec 286 | arg_spec = _getfullargspec_p(decoratee) 287 | 288 | # parse dargs'n'dkwargs 289 | try: 290 | arg_list = arg_spec.args + arg_spec.kwonlyargs 291 | except AttributeError: # py2, no kwonlyargs 292 | arg_list = arg_spec.args 293 | 294 | # check if number of guards makes sense 295 | if len(dargs) > len(arg_spec.args) or len(dkwargs) > len(arg_list): 296 | raise ValueError("Too many guard definitions for '%s()'" % decoratee.__name__) 297 | 298 | if len(dargs) == 1 and isinstance(dargs[0], RelGuard): # check if relguard is specified 299 | rel_guard = dargs[0] 300 | argument_guards = OrderedDict((arg, _) for arg in arg_spec.args) # might raise IndexError if empty 301 | _already_bound = () # UnboundLocalError emerges if this is not defined. 302 | elif len(dargs) == 1 and isinstance(dargs[0], types.FunctionType): # decorate directly (guards in annotations) 303 | rel_guard = _ 304 | argument_guards = OrderedDict((arg, _) for arg in arg_spec.args) # might raise IndexError if empty 305 | _already_bound = () # UnboundLocalError emerges if this is not defined. 306 | else: 307 | rel_guard = _ 308 | # bind positionals to their names 309 | argument_guards = OrderedDict(six.moves.zip_longest(arg_spec.args, dargs, fillvalue=_)) 310 | _already_bound = arg_spec.args[:len(dargs)] # names of arguments matched with dargs 311 | 312 | for arg_name in dkwargs.keys(): 313 | if arg_name in _already_bound: # check if guards in kwargs does not overlap with those bound above 314 | raise ValueError("Multiple definitions of guard for argument '%s'" % arg_name) 315 | if arg_name not in arg_list: # check if arg names are valid 316 | raise ValueError("Unexpected guard definition for not recognised argument '%s'" % arg_name) 317 | # no overlaps, so extend argument_guards: 318 | argument_guards.update(dkwargs) 319 | 320 | # if argument_guards is empty (catch-all, more precisely), try extracting guards from annotations 321 | if all(grd is _ for grd in argument_guards.values()): 322 | try: 323 | arg_annotations = arg_spec.annotations # raises AttributeError in py2 324 | six.next(six.iterkeys(arg_annotations)) 325 | except (AttributeError, # arg_spec was produced by getargspec (py2) thus we need to parse dargs'n'dkwargs 326 | StopIteration): # or annotations are empty (py3) 327 | pass 328 | else: 329 | argument_guards = OrderedDict( 330 | (arg_name, arg_annotations.get(arg_name, _)) # name -> annotation, or _ if no annotation. 331 | for arg_name in arg_list # arg_list dictates right order 332 | ) 333 | if rel_guard is _: 334 | try: 335 | rel_guard = arg_spec.annotations.get("return", _) 336 | except AttributeError: 337 | pass 338 | 339 | if (not argument_guards or all(grd is _ for grd in argument_guards.values())) and rel_guard is _: 340 | raise ValueError("No guards specified for '%s()'" % decoratee.__name__) 341 | 342 | # check if guards are really guards, or at least callable. 343 | if rel_guard is not _ and not isinstance(rel_guard, RelGuard): 344 | if strict_guard_definitions: 345 | raise ValueError("Specified relguard must be an instance of RelGuard, not %s" % type(rel_guard).__name__) 346 | else: 347 | if not callable(rel_guard): 348 | raise ValueError("Specified relguard is not callable") 349 | warnings.warn("Specified relguard is not an instance of RelGuard. Its behaviour may be unexpected.", 350 | RuntimeWarning) 351 | 352 | for arg_name, grd in argument_guards.items(): 353 | if grd is not _ and not isinstance(grd, GuardFunc): 354 | if strict_guard_definitions: 355 | raise ValueError("Guard specified for argument '%s' must be an instance of GuardFunc, not %s" 356 | % (arg_name, type(grd).__name__)) 357 | else: 358 | if not callable(grd): 359 | raise ValueError("Guard specified for argument '%s' is not callable" % arg_name) 360 | warnings.warn("Guard specified for argument '%s' is not an instance of GuardFunc. Its behaviour may be unexpected." 361 | % arg_name, RuntimeWarning) 362 | 363 | # check if relguard argument names fits with decoratee 364 | if isinstance(rel_guard, RelGuard): # only RelGuard objects have __argnames__ attr. 365 | for arg in rel_guard.__argnames__: 366 | if arg not in arg_list: 367 | raise ValueError("Relguard's argument names must be a subset of %s argument names" % decoratee.__name__) 368 | 369 | # get default arg vals 370 | if arg_spec.defaults is not None: 371 | argument_defaults = dict( 372 | zip( 373 | arg_spec.args[-len(arg_spec.defaults):], # slice of last elements. len(slice) == len(defaults) 374 | arg_spec.defaults 375 | ) 376 | ) 377 | else: 378 | argument_defaults = dict() 379 | 380 | try: 381 | argument_defaults.update(dict( 382 | zip( 383 | arg_spec.kwonlyargs[-len(arg_spec.kwonlydefaults):], 384 | arg_spec.kwonlydefaults 385 | ) 386 | )) 387 | except AttributeError: # py2, no kwonlyargs 388 | pass 389 | 390 | @six.wraps(decoratee) 391 | def guarded(*args, **kwargs): 392 | """ 393 | Checks if arguments pass through guards before calling the guarded function. If they're not, then 394 | GuardError is raised. 395 | """ 396 | 397 | # bind defaults, args and kwargs to names 398 | bound_args = argument_defaults.copy() # get defaults first, so they can be overwritten 399 | bound_args.update(zip(arg_spec.args[:len(args)], args)) # bind positional args to names 400 | bound_args.update(kwargs) # add kwargs 401 | 402 | # check relations between args/environment first 403 | if not rel_guard(**bound_args): # argument order in relguard is not guaranteed to match decoratee's 404 | raise GuardError("Arguments did not pass through relguard") 405 | else: 406 | pass # just to explicitly note, that argument passed relguard. 407 | 408 | # check bound_args one by one 409 | for arg_name, arg_value in bound_args.items(): 410 | grd = argument_guards[arg_name] 411 | if not grd(arg_value): 412 | raise GuardError("Wrong value for keyword argument '%s'" % arg_name) 413 | else: 414 | pass # just to explicitly note, that argument passed guard. 415 | 416 | return decoratee(*args, **kwargs) 417 | 418 | ret = guarded 419 | ret._argument_guards = argument_guards 420 | ret._relguard = rel_guard 421 | ret.__guarded__ = decoratee 422 | return ret 423 | 424 | # decide whether initialise decorator 425 | if len(dkwargs) == 0 and len(dargs) == 1 and callable(dargs[0]) and not isinstance(dargs[0], (GuardFunc, RelGuard)): 426 | return decorator(dargs[0]) 427 | else: 428 | return decorator 429 | 430 | def rguard(rel_guard, **kwargs): 431 | """ 432 | Converts first and only positional argument to a RelGuard object. 433 | By using rguard you guarantee that `rel_guard` is a correctly defined relguard function. 434 | """ 435 | return guard(RelGuard(rel_guard), **kwargs) 436 | 437 | def raguard(decoratee): 438 | """ 439 | Converts return annotation to a RelGuard object. 440 | By using raguard you guarantee that return annotation holds a correctly defined relguard function. 441 | """ 442 | try: 443 | rel_guard = decoratee.__annotations__['return'] 444 | except (AttributeError, KeyError): 445 | raise ValueError("raguard requires return annotation") 446 | else: 447 | return guard(RelGuard(rel_guard))(decoratee) 448 | 449 | # CASE # 450 | 451 | class MultiFunc(): 452 | """ 453 | Class capable of function call dispatch based on call arguments. 454 | """ 455 | def __init__(self): 456 | self.clauses = defaultdict(list) # seperate lists for different arities 457 | self.__name__ = None 458 | 459 | def __call__(self, *args, **kwargs): 460 | """ 461 | Dispatch. Note that different arities are dispatched separately and independently. 462 | """ 463 | for clause in self.clauses[len(args) + len(kwargs)]: 464 | try: 465 | return clause(*args, **kwargs) # call first matching function clause 466 | except GuardError: 467 | pass 468 | # no hit, raise 469 | raise MatchError("No match for given argument values") 470 | 471 | def append(self, arity, clause): 472 | """ 473 | Append clause to `self.clauses` under given `arity`. 474 | """ 475 | if self.clauses.get(arity, False) and hasattr(self.clauses[-1], '__catchall__'): 476 | raise WontMatchError("Function clause defined after a catch-all clause") 477 | 478 | self.clauses[arity].append(clause) 479 | if not self.__name__: 480 | self.__name__ = clause.__name__ 481 | 482 | 483 | function_refs = defaultdict(lambda: defaultdict(MultiFunc)) # needed for case to track multi-headed functions. 484 | # module name -> fun (qual)name -> MultiFunc object 485 | 486 | def case(*dargs, **dkwargs): 487 | """ 488 | Creates MultiFunc object or appends function clauses to an existing one. 489 | """ 490 | 491 | def decorator(decoratee): 492 | "Actual decorator." 493 | 494 | # get arg_spec 495 | try: 496 | arg_spec = _getfullargspec_p(decoratee.__guarded__) 497 | except AttributeError: 498 | arg_spec = _getfullargspec_p(decoratee) 499 | 500 | # different arities, seperate dispatch lists, so we need to count args 501 | clause_arity = len(arg_spec.args) 502 | 503 | # what guard should we wrap args with 504 | if hasattr(decoratee, '__dispatch__'): # called by dispatch decorator 505 | grd_wrap = isoftype 506 | else: 507 | grd_wrap = eq 508 | 509 | # only positional and positional_or_kw args allowed 510 | if arg_spec.varargs or arg_spec.keywords: 511 | raise ValueError("Functions with varying arguments unsupported") 512 | if getattr(arg_spec, 'kwonlyargs', False): 513 | raise ValueError("Functions with keyword-only arguments unsupported") 514 | 515 | # read defaults 516 | if arg_spec.defaults is not None: 517 | arg_defaults = dict( 518 | zip( 519 | arg_spec.args[-len(arg_spec.defaults):], 520 | arg_spec.defaults 521 | ) 522 | ) 523 | else: 524 | arg_defaults = dict() 525 | 526 | # parse dargs'n'dkwargs 527 | if (dargs and dargs[0] is not decoratee) or dkwargs: 528 | if arg_defaults: 529 | raise ValueError("Default args are not allowed when pattern is specified as decorator arguments") 530 | 531 | # check if correct (names and count) 532 | for dkey in dkwargs: 533 | if dkey not in arg_spec.args: 534 | raise ValueError("Function %s() has no argument '%s'" % (decoratee.__name__, dkey)) 535 | if len(dargs) > clause_arity: 536 | raise ValueError("Pattern must have at most %i values, found %i" % (clause_arity, len(dargs))) 537 | 538 | match_vals = dict(zip(arg_spec.args[:len(dargs)], dargs)) 539 | match_vals.update(dkwargs) 540 | else: # direct decorator or with empty args: 541 | match_vals = arg_defaults or dict() # will be dict() if arg_defaults is None 542 | 543 | # set guards on decoratee 544 | if not match_vals and not hasattr(decoratee, '__guarded__'): # this is a catch-all! no need for guarding this clause 545 | decoratee.__catchall__ = True 546 | elif hasattr(decoratee, '__guarded__'): # decoratee is already guarded; extend guards 547 | for arg_name, match in match_vals.items(): 548 | if match is not _: 549 | match = grd_wrap(match) # eq or isoftype 550 | try: 551 | decoratee._argument_guards[arg_name] = match & decoratee._argument_guards[arg_name] 552 | except KeyError: 553 | decoratee._argument_guards[arg_name] = match 554 | else: 555 | _guards = match_vals.copy() 556 | _guards.update({k: grd_wrap(v) for k, v in match_vals.items() if v is not _}) 557 | decoratee = guard(**_guards)(decoratee) 558 | 559 | # add to function_refs 560 | try: 561 | decoratee_name = decoratee.__qualname__ # py>=3.3 562 | except AttributeError: 563 | decoratee_name = decoratee.__name__ 564 | 565 | function_refs[decoratee.__module__][decoratee_name].append(clause_arity, decoratee) 566 | 567 | # return MultiFunc object 568 | return function_refs[decoratee.__module__][decoratee_name] 569 | 570 | # decide whether initialise decorator 571 | if len(dkwargs) == 0 and len(dargs) == 1 and callable(dargs[0]): 572 | return decorator(dargs[0]) 573 | else: 574 | return decorator 575 | 576 | def dispatch(*dargs, **dkwargs): 577 | """ 578 | Like `case`, but dispatch happens on type instead of values. 579 | """ 580 | 581 | def direct(decoratee): 582 | decoratee.__dispatch__ = True # this will intruct case to wrap args in isoftype. 583 | return case(decoratee) 584 | 585 | def indirect(decoratee): 586 | decoratee.__dispatch__ = True 587 | return case(*dargs, **dkwargs)(decoratee) 588 | 589 | if len(dkwargs) == 0 and len(dargs) == 1 and callable(dargs[0]): 590 | return direct(dargs[0]) 591 | else: 592 | return indirect 593 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | six 2 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.rst 3 | [bdist_wheel] 4 | universal=1 5 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from __future__ import with_statement 2 | from setuptools import setup 3 | 4 | version = '0.99a2' 5 | 6 | with open("README.rst", mode='r') as fp: 7 | long_desc = fp.read() 8 | 9 | setup( 10 | name = 'function-pattern-matching', 11 | version = version, 12 | description = "Pattern matching and guards for Python functions", 13 | long_description = long_desc, 14 | url = "https://github.com/rasguanabana/function-pattern-matching", 15 | author = "Adrian Włosiak", 16 | author_email = "adwlosiakh@gmail.com", 17 | license = "MIT", 18 | classifiers = [ 19 | "Development Status :: 3 - Alpha", 20 | "Intended Audience :: Developers", 21 | "License :: OSI Approved :: MIT License", 22 | "Operating System :: OS Independent", 23 | "Programming Language :: Python :: 2", 24 | "Programming Language :: Python :: 3", 25 | "Topic :: Software Development :: Libraries :: Python Modules" 26 | ], 27 | keywords = "pattern matching guards", 28 | py_modules = ['function_pattern_matching'], 29 | install_requires = ['six'] 30 | ) 31 | -------------------------------------------------------------------------------- /tests/py3_defs.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module contains syntax which works only with Python 3. 3 | Import will fail when calling test_fpm.py with Python 2 and those tests will be ommited. 4 | """ 5 | import function_pattern_matching as fpm 6 | # annotations all 7 | @fpm.guard 8 | def ka3( 9 | a: fpm.ne(0), 10 | b: fpm.isoftype(str), 11 | c: fpm.eFalse 12 | ): 13 | return (a, b, c) 14 | 15 | # annotations some 16 | @fpm.guard 17 | def ks3( 18 | a: fpm.Isnot(None), 19 | b, # implicit _ 20 | c: fpm.In(range(100)) 21 | ): 22 | return (a, b, c) 23 | 24 | # relguard-only all 25 | @fpm.guard 26 | def roa3(a, b, c) -> fpm.relguard(lambda a, b, c: a == b and b < c): 27 | return (a, b, c) 28 | 29 | # relguard-only some 30 | @fpm.guard 31 | def ros3(a, b, c) -> fpm.relguard(lambda a, c: (a + 1) == c): 32 | return (a, b, c) 33 | 34 | # relguard-only none 35 | some_external_var3 = True 36 | @fpm.guard 37 | def ron3(a, b, c) -> fpm.relguard(lambda: some_external_var3 == True): 38 | return (a, b, c) 39 | 40 | # relguard with all annotations 41 | @fpm.guard 42 | def rwak3( 43 | a: fpm.isoftype(int), 44 | b: fpm.isoftype(str), 45 | c: fpm.isoftype(float) 46 | ) -> fpm.relguard(lambda a, c: a == c): 47 | return (a, b, c) 48 | 49 | # relguard with some annotations 50 | @fpm.guard 51 | def rwsk3( 52 | a: fpm.isoftype(int), 53 | b, 54 | c 55 | ) -> fpm.relguard(lambda a, c: a == c): 56 | return (a, b, c) 57 | 58 | # relguard with all annotations, mixed 59 | @fpm.guard( 60 | fpm.relguard(lambda a, c: a == c) 61 | ) 62 | def rwak3m( 63 | a: fpm.isoftype(int), 64 | b: fpm.isoftype(str), 65 | c: fpm.isoftype(float) 66 | ): 67 | return (a, b, c) 68 | 69 | # relguard with some annotations, mixed 70 | @fpm.guard( 71 | fpm.relguard(lambda a, c: a == c) 72 | ) 73 | def rwsk3m( 74 | a: fpm.isoftype(int), 75 | b, 76 | c 77 | ): 78 | return (a, b, c) 79 | 80 | # relguard with all annotations, rguard mixed 81 | @fpm.rguard(lambda a, c: a == c) 82 | def rwak3rm( 83 | a: fpm.isoftype(int), 84 | b: fpm.isoftype(str), 85 | c: fpm.isoftype(float) 86 | ): 87 | return (a, b, c) 88 | 89 | # relguard with some annotations, rguard mixed 90 | @fpm.rguard(lambda a, c: a == c) 91 | def rwsk3rm( 92 | a: fpm.isoftype(int), 93 | b, 94 | c 95 | ): 96 | return (a, b, c) 97 | 98 | # relguard-only all, raguard 99 | @fpm.raguard 100 | def roa3ra(a, b, c) -> lambda a, b, c: a == b and b < c: 101 | return (a, b, c) 102 | 103 | # relguard-only some, raguard 104 | @fpm.raguard 105 | def ros3ra(a, b, c) -> lambda a, c: (a + 1) == c: 106 | return (a, b, c) 107 | 108 | # relguard-only none, raguard 109 | some_external_var3b = False 110 | @fpm.raguard 111 | def ron3ra(a, b, c) -> lambda: some_external_var3b == False: 112 | return (a, b, c) 113 | 114 | # relguard with all annotations 115 | @fpm.raguard 116 | def rwak3ra( 117 | a: fpm.isoftype(int), 118 | b: fpm.isoftype(str), 119 | c: fpm.isoftype(float) 120 | ) -> lambda a, c: a == c: 121 | return (a, b, c) 122 | 123 | # relguard with some annotations 124 | @fpm.raguard 125 | def rwsk3ra( 126 | a: fpm.isoftype(int), 127 | b, 128 | c 129 | ) -> lambda a, c: a == c: 130 | return (a, b, c) 131 | 132 | def rwak3_bare( 133 | a: fpm.isoftype(int), 134 | b: fpm.isoftype(str), 135 | c: fpm.isoftype(float) 136 | ) -> lambda a, c: a == c: 137 | return (a, b, c) 138 | 139 | @fpm.guard 140 | def ad3( 141 | a: fpm.isoftype(int), 142 | b: fpm.isoftype(bool) =True, 143 | c: fpm.Is(None) | fpm.gt(0) =None 144 | ): 145 | return (a, b, c) 146 | 147 | def kwonly(a, b, *, c): 148 | return (a, b, c) 149 | -------------------------------------------------------------------------------- /tests/test_fpm.py: -------------------------------------------------------------------------------- 1 | #TODO: test guards with default args 2 | #TODO: write test for erroneous 3 | import sys, os 4 | sys.path.insert(1, os.path.join(sys.path[0], '..')) 5 | import function_pattern_matching as fpm 6 | import unittest 7 | try: 8 | from py3_defs import * 9 | py3 = True 10 | except SyntaxError: 11 | py3 = False 12 | 13 | class DoGuardsWork(unittest.TestCase): 14 | def test_simple(self): 15 | "Test each basic guard" 16 | 17 | a1 = fpm.eq(10) 18 | self.assertTrue(a1(10)) 19 | self.assertFalse(a1(0)) 20 | 21 | a2 = fpm.ne(10) 22 | self.assertTrue(a2(11)) 23 | self.assertFalse(a2(10)) 24 | 25 | a3 = fpm.lt(10) 26 | self.assertTrue(a3(-100)) 27 | self.assertFalse(a3(10)) 28 | self.assertFalse(a3(9001)) # IT'S OVER 9000!!! 29 | 30 | a4 = fpm.le(10) 31 | self.assertTrue(a4(-100)) 32 | self.assertTrue(a4(10)) 33 | self.assertFalse(a4(100)) 34 | 35 | a5 = fpm.gt(10) 36 | self.assertTrue(a5(11)) 37 | self.assertFalse(a5(10)) 38 | self.assertFalse(a5(9)) 39 | 40 | a6 = fpm.ge(10) 41 | self.assertTrue(a6(11)) 42 | self.assertTrue(a6(10)) 43 | self.assertFalse(a6(9)) 44 | 45 | a7 = fpm.Is(True) 46 | self.assertTrue(a7(True)) 47 | self.assertFalse(a7(None)) 48 | 49 | x_ = 1 50 | a7a = fpm.Is(x_) 51 | self.assertTrue(a7a(x_)) 52 | self.assertFalse(a7a(2)) 53 | 54 | a7b = fpm.Is(None) 55 | self.assertTrue(a7b(None)) 56 | self.assertFalse(a7b(True)) 57 | 58 | a8 = fpm.Isnot(None) 59 | self.assertTrue(a8(10)) 60 | self.assertFalse(a8(None)) 61 | 62 | a9 = fpm.isoftype(int) 63 | self.assertTrue(a9(10)) 64 | self.assertFalse(a9('qwerty')) 65 | 66 | a9b = fpm.isoftype(int, float) 67 | self.assertTrue(a9b(10)) 68 | self.assertTrue(a9b(10.5)) 69 | self.assertFalse(a9b('x')) 70 | 71 | a9c = fpm.isoftype((int, float)) 72 | self.assertTrue(a9c(10)) 73 | self.assertTrue(a9c(10.5)) 74 | self.assertFalse(a9c('x')) 75 | 76 | a10 = fpm.isiterable 77 | self.assertFalse(a10(10)) 78 | self.assertTrue(a10("asd")) 79 | self.assertTrue(a10([1,2,3,4])) 80 | 81 | a11 = fpm.In(range(10)) 82 | self.assertTrue(a11(5)) 83 | self.assertFalse(a11(10)) 84 | self.assertFalse(a11(15)) 85 | 86 | a12 = fpm.notIn({'a', 2, False}) 87 | self.assertTrue(a12(10)) 88 | self.assertTrue(a12(True)) 89 | self.assertFalse(a12('a')) 90 | self.assertFalse(a12(2)) 91 | self.assertFalse(a12(False)) 92 | 93 | a13 = fpm.eTrue 94 | self.assertTrue(a13(1)) 95 | self.assertTrue(a13([10])) 96 | self.assertFalse(a13(0)) 97 | self.assertFalse(a13([])) 98 | self.assertFalse(a13('')) 99 | 100 | a14 = fpm.eFalse 101 | self.assertFalse(a14(1)) 102 | self.assertFalse(a14([10])) 103 | self.assertTrue(a14(0)) 104 | self.assertTrue(a14([])) 105 | self.assertTrue(a14('')) 106 | 107 | def test_combined(self): 108 | "Test some combined guards" 109 | 110 | # is int and ((>0 and <=5) or =10) 111 | comp2 = fpm.isoftype(int) & ((fpm.gt(0) & fpm.le(5)) | fpm.eq(10)) 112 | self.assertFalse(comp2(-10)) 113 | self.assertFalse(comp2(0)) 114 | self.assertTrue(comp2(1)) 115 | self.assertTrue(comp2(4)) 116 | self.assertTrue(comp2(5)) 117 | self.assertFalse(comp2(6)) 118 | self.assertTrue(comp2(10)) 119 | self.assertFalse(comp2('a')) 120 | self.assertFalse(comp2([])) 121 | self.assertTrue(comp2(True)) # bool is subclass of int, meh 122 | 123 | # (is int and is not bool and >0 and <=5) or is None 124 | comp3 = (fpm.isoftype(int) & ~fpm.isoftype(bool) & fpm.gt(0) & fpm.le(5)) | fpm.Is(None) 125 | self.assertFalse(comp3(-10)) 126 | self.assertFalse(comp3(0)) 127 | self.assertTrue(comp3(1)) 128 | self.assertTrue(comp3(4)) 129 | self.assertTrue(comp3(5)) 130 | self.assertFalse(comp3(6)) 131 | self.assertFalse(comp3(10)) 132 | self.assertFalse(comp3('a')) 133 | self.assertFalse(comp3([])) 134 | self.assertFalse(comp3(True)) 135 | #self.assertTrue(comp3(None)) 136 | 137 | # evals to True or is None 138 | comp4 = fpm.eTrue | fpm.Is(None) 139 | self.assertTrue(comp4(1)) 140 | self.assertTrue(comp4(10)) 141 | self.assertTrue(comp4('asd')) 142 | self.assertTrue(comp4({1,2,3})) 143 | self.assertFalse(comp4(0)) 144 | self.assertFalse(comp4([])) 145 | self.assertFalse(comp4('')) 146 | self.assertFalse(comp4({})) 147 | self.assertFalse(comp4(set())) 148 | self.assertTrue(comp4(None)) 149 | 150 | # is iterable xor evals to True 151 | comp5 = fpm.isiterable ^ fpm.eTrue 152 | self.assertTrue(comp5([])) 153 | self.assertTrue(comp5(1)) 154 | self.assertFalse(comp5(0)) 155 | self.assertFalse(comp5([1,2,3])) 156 | 157 | def test_relation_guards(self): 158 | "Test relguard (relations between arguments)" 159 | 160 | rg1 = fpm.relguard(lambda a, b, c: a + 1 == b and b + 1 == c and a > 0) 161 | self.assertTrue(rg1(a=1, b=2, c=3)) 162 | self.assertFalse(rg1(a=0, b=0, c=0)) 163 | self.assertFalse(rg1(a=[], b=0, c=0)) 164 | self.assertEqual(rg1.__argnames__, {'a', 'b', 'c'}) 165 | 166 | some_external_var = 9 167 | rg2 = fpm.relguard(lambda: some_external_var == 10) 168 | self.assertFalse(rg2()) 169 | self.assertEqual(rg2.__argnames__, set()) 170 | 171 | def test_guarded_correctly_decargs(self): 172 | "Test every possible correct way of defining guards (Py2 & 3, no relguards)" 173 | 174 | # positional all 175 | @fpm.guard(fpm.eq(0), fpm.gt(0), fpm.isoftype(int)) 176 | def pc(a, b, c): 177 | return (a, b, c) 178 | 179 | self.assertEqual(pc(0, 1, 2), (0, 1, 2)) 180 | self.assertRaises(fpm.GuardError, pc, 1, 1, 2) 181 | self.assertRaises(fpm.GuardError, pc, 0, -1, 2) 182 | self.assertRaises(fpm.GuardError, pc, 0, 1, 'a') 183 | self.assertRaises(fpm.GuardError, pc, 1, 1, 'a') 184 | 185 | # positional some (first two args) 186 | @fpm.guard(fpm.eTrue, fpm.eTrue & fpm.isiterable) 187 | def ps(a, b, c): 188 | return (a, b, c) 189 | 190 | self.assertEqual(ps(10, {1, 2}, None), (10, {1, 2}, None)) 191 | self.assertEqual(ps(10, {1, 2}, fpm), (10, {1, 2}, fpm)) 192 | self.assertRaises(fpm.GuardError, ps, None, [1], "x") 193 | self.assertRaises(fpm.GuardError, ps, 1, 1, "x") 194 | self.assertRaises(fpm.GuardError, ps, 1, [], "1") 195 | 196 | # positional first-only (a.k.a does it clash with relguard) 197 | @fpm.guard(~fpm.isiterable) 198 | def pfo(a, b, c): 199 | return (a, b, c) 200 | 201 | self.assertEqual(pfo(1, 0, 0), (1, 0, 0)) 202 | self.assertRaises(fpm.GuardError, pfo, "string", None, None) 203 | 204 | # positional with catch-alls 205 | @fpm.guard(fpm._, fpm._, fpm.isoftype(float)) 206 | def pwca1(a, b, c): 207 | return (a, b, c) 208 | 209 | self.assertEqual(pwca1(0, pwca1, 1.0), (0, pwca1, 1.0)) 210 | self.assertRaises(fpm.GuardError, pwca1, 0, lambda: None, 9001) 211 | 212 | @fpm.guard(fpm.lt(0), fpm._, fpm.isoftype(float)) 213 | def pwca2(a, b, c): 214 | return (a, b, c) 215 | 216 | self.assertEqual(pwca2(-1, (), 9000.1), (-1, (), 9000.1)) # that's over 9000, too. 217 | self.assertRaises(fpm.GuardError, pwca2, 0, 1, 1.0) 218 | self.assertRaises(fpm.GuardError, pwca2, -1, 1, 1) 219 | 220 | # keyword all 221 | @fpm.guard( 222 | a = fpm.ne(0), 223 | b = fpm.isoftype(str), 224 | c = fpm.eFalse 225 | ) 226 | def ka(a, b, c): 227 | return (a, b, c) 228 | 229 | self.assertEqual(ka(1, "", ""), (1, "", "")) 230 | self.assertRaises(fpm.GuardError, ka, 0, "", []) 231 | self.assertRaises(fpm.GuardError, ka, 1, 1, 0) 232 | self.assertRaises(fpm.GuardError, ka, 1, '', True) 233 | 234 | # keyword some 235 | @fpm.guard( 236 | a = fpm.Isnot(None), 237 | c = fpm.In(range(100)) 238 | ) 239 | def ks(a, b, c): 240 | return (a, b, c) 241 | 242 | self.assertEqual(ks(True, 0, 50), (True, 0, 50)) 243 | self.assertRaises(fpm.GuardError, ks, None, 0, 50) 244 | self.assertRaises(fpm.GuardError, ks, 1, '', 1000) 245 | 246 | def test_guarded_correctly_decargs_with_relguards(self): 247 | "Test every possible correct way of defining guards (Py2 & 3, with relguards)" 248 | 249 | # relguard-only all 250 | @fpm.guard( 251 | fpm.relguard(lambda a, b, c: a == b and b < c) 252 | ) 253 | def roa(a, b, c): 254 | return (a, b, c) 255 | 256 | self.assertEqual(roa(10, 10, 15), (10, 10, 15)) 257 | self.assertRaises(fpm.GuardError, roa, 1, 5, 12) 258 | self.assertRaises(fpm.GuardError, roa, 1, 1, -5) 259 | self.assertRaises(fpm.GuardError, roa, [], [], 0) 260 | 261 | # relguard-only some 262 | @fpm.guard( 263 | fpm.relguard(lambda a, c: (a + 1) == c) 264 | ) 265 | def ros(a, b, c): 266 | return (a, b, c) 267 | 268 | self.assertEqual(ros(10, -10, 11), (10, -10, 11)) 269 | self.assertRaises(fpm.GuardError, ros, 0, 0, 0) 270 | 271 | # relguard-only none 272 | some_external_var = True 273 | @fpm.guard( 274 | fpm.relguard(lambda: some_external_var == True) 275 | ) 276 | def ron(a, b, c): 277 | return (a, b, c) 278 | 279 | self.assertEqual(ron(1, 1, 1), (1, 1, 1)) 280 | some_external_var = False 281 | self.assertRaises(fpm.GuardError, ron, 1, 1, 1) 282 | 283 | # relguard with all keywords 284 | @fpm.guard( 285 | fpm.relguard(lambda a, c: a == c), 286 | a = fpm.isoftype(int), 287 | b = fpm.isoftype(str), 288 | c = fpm.isoftype(float) 289 | ) 290 | def rwak(a, b, c): 291 | return (a, b, c) 292 | 293 | self.assertEqual(rwak(1, "asd", 1.0), (1, "asd", 1.0)) 294 | self.assertRaises(fpm.GuardError, rwak, 9000, "x", 9000.1) 295 | self.assertRaises(fpm.GuardError, rwak, 'x', 'y', 'x') 296 | self.assertRaises(fpm.GuardError, rwak, 1, 0, 1.0) 297 | self.assertRaises(fpm.GuardError, rwak, 1, 'x', 1) 298 | 299 | # relguard with some keywords 300 | @fpm.guard( 301 | fpm.relguard(lambda a, c: a == c), 302 | a = fpm.isoftype(int) 303 | ) 304 | def rwsk(a, b, c): 305 | return (a, b, c) 306 | 307 | self.assertEqual(rwsk(1, "asd", 1.0), (1, "asd", 1.0)) 308 | self.assertRaises(fpm.GuardError, rwsk, 9000, "x", 9000.1) 309 | self.assertRaises(fpm.GuardError, rwsk, 'x', 'y', 'x') 310 | 311 | # fancy, with default args 312 | @fpm.guard( 313 | fpm.relguard(lambda a, c: type(a) == type(c)), 314 | a = fpm.eTrue, 315 | b = fpm.Isnot(None) 316 | ) 317 | def fancy(a, b=[], c=1): 318 | return (a, b, c) 319 | 320 | self.assertEqual(fancy(9), (9, [], 1)) 321 | self.assertEqual(fancy(-1, 'x'), (-1, 'x', 1)) 322 | self.assertEqual(fancy('a', b'b', 'c'), ('a', b'b', 'c')) 323 | 324 | def test_rguard_decargs(self): 325 | "Test every possible correct way of defining guards with rguard decorator (Py2 & 3)" 326 | 327 | # relguard-only all 328 | @fpm.rguard(lambda a, b, c: a == b and b < c) 329 | def roaR(a, b, c): 330 | return (a, b, c) 331 | 332 | self.assertEqual(roaR(10, 10, 15), (10, 10, 15)) 333 | self.assertRaises(fpm.GuardError, roaR, 1, 5, 12) 334 | self.assertRaises(fpm.GuardError, roaR, 1, 1, -5) 335 | self.assertRaises(fpm.GuardError, roaR, [], [], 0) 336 | 337 | # relguard-only some 338 | @fpm.rguard(lambda a, c: (a + 1) == c) 339 | def rosR(a, b, c): 340 | return (a, b, c) 341 | 342 | self.assertEqual(rosR(10, -10, 11), (10, -10, 11)) 343 | self.assertRaises(fpm.GuardError, rosR, 0, 0, 0) 344 | 345 | # relguard-only none 346 | some_external_var = True 347 | @fpm.rguard(lambda: some_external_var == True) 348 | def ronR(a, b, c): 349 | return (a, b, c) 350 | 351 | self.assertEqual(ronR(1, 1, 1), (1, 1, 1)) 352 | some_external_var = False 353 | self.assertRaises(fpm.GuardError, ronR, 1, 1, 1) 354 | 355 | # relguard with all keywords 356 | @fpm.rguard( 357 | lambda a, c: a == c, 358 | a = fpm.isoftype(int), 359 | b = fpm.isoftype(str), 360 | c = fpm.isoftype(float) 361 | ) 362 | def rwakR(a, b, c): 363 | return (a, b, c) 364 | 365 | self.assertEqual(rwakR(1, "asd", 1.0), (1, "asd", 1.0)) 366 | self.assertRaises(fpm.GuardError, rwakR, 9000, "x", 9000.1) 367 | self.assertRaises(fpm.GuardError, rwakR, 'x', 'y', 'x') 368 | self.assertRaises(fpm.GuardError, rwakR, 1, 0, 1.0) 369 | self.assertRaises(fpm.GuardError, rwakR, 1, 'x', 1) 370 | 371 | # relguard with some keywords 372 | @fpm.rguard( 373 | lambda a, c: a == c, 374 | a = fpm.isoftype(int) 375 | ) 376 | def rwskR(a, b, c): 377 | return (a, b, c) 378 | 379 | self.assertEqual(rwskR(1, "asd", 1.0), (1, "asd", 1.0)) 380 | self.assertRaises(fpm.GuardError, rwskR, 9000, "x", 9000.1) 381 | self.assertRaises(fpm.GuardError, rwskR, 'x', 'y', 'x') 382 | 383 | def test_guarded_correctly_annotations(self): 384 | "Test every possible correct way of defining guards as annotations (Py3 only, no relguards)" 385 | 386 | if not py3: 387 | return 388 | 389 | # annotations all 390 | self.assertEqual(ka3(1, "", ""), (1, "", "")) 391 | self.assertRaises(fpm.GuardError, ka3, 0, "", []) 392 | self.assertRaises(fpm.GuardError, ka3, 1, 1, 0) 393 | self.assertRaises(fpm.GuardError, ka3, 1, '', True) 394 | 395 | # annotations some 396 | self.assertEqual(ks3(True, 0, 50), (True, 0, 50)) 397 | self.assertRaises(fpm.GuardError, ks3, None, 0, 50) 398 | self.assertRaises(fpm.GuardError, ks3, 1, '', 1000) 399 | 400 | def test_guarded_correctly_annotations_with_relguards(self): 401 | "Test every possible correct way of defining guards as annotations (Py3 only, with relguards)" 402 | 403 | if not py3: 404 | return 405 | 406 | # relguard-only all 407 | self.assertEqual(roa3(10, 10, 15), (10, 10, 15)) 408 | self.assertRaises(fpm.GuardError, roa3, 1, 5, 12) 409 | self.assertRaises(fpm.GuardError, roa3, 1, 1, -5) 410 | self.assertRaises(fpm.GuardError, roa3, [], [], 0) 411 | 412 | # relguard-only some 413 | self.assertEqual(ros3(10, -10, 11), (10, -10, 11)) 414 | self.assertRaises(fpm.GuardError, ros3, 0, 0, 0) 415 | 416 | # relguard-only none 417 | some_external_var3 = True 418 | self.assertEqual(ron3(1, 1, 1), (1, 1, 1)) 419 | #some_external_var3 = False 420 | #self.assertRaises(fpm.GuardError, ron3, 1, 1, 1) 421 | 422 | # relguard with all annotations 423 | self.assertEqual(rwak3(1, "asd", 1.0), (1, "asd", 1.0)) 424 | self.assertRaises(fpm.GuardError, rwak3, 9000, "x", 9000.1) 425 | self.assertRaises(fpm.GuardError, rwak3, 'x', 'y', 'x') 426 | self.assertRaises(fpm.GuardError, rwak3, 1, 0, 1.0) 427 | self.assertRaises(fpm.GuardError, rwak3, 1, 'x', 1) 428 | 429 | # relguard with some annotations 430 | self.assertEqual(rwsk3(1, "asd", 1.0), (1, "asd", 1.0)) 431 | self.assertRaises(fpm.GuardError, rwsk3, 9000, "x", 9000.1) 432 | self.assertRaises(fpm.GuardError, rwak3, 'x', 'y', 'x') 433 | 434 | # relguard with all annotations, mixed 435 | self.assertEqual(rwak3m(1, "asd", 1.0), (1, "asd", 1.0)) 436 | self.assertRaises(fpm.GuardError, rwak3m, 9000, "x", 9000.1) 437 | self.assertRaises(fpm.GuardError, rwak3m, 'x', 'y', 'x') 438 | self.assertRaises(fpm.GuardError, rwak3m, 1, 0, 1.0) 439 | self.assertRaises(fpm.GuardError, rwak3m, 1, 'x', 1) 440 | 441 | # relguard with some annotations, mixed 442 | self.assertEqual(rwsk3m(1, "asd", 1.0), (1, "asd", 1.0)) 443 | self.assertRaises(fpm.GuardError, rwsk3m, 9000, "x", 9000.1) 444 | self.assertRaises(fpm.GuardError, rwak3m, 'x', 'y', 'x') 445 | 446 | # annotations + default values 447 | self.assertEqual(ad3(2), (2, True, None)) 448 | self.assertEqual(ad3(2, False), (2, False, None)) 449 | self.assertEqual(ad3(2, c=10), (2, True, 10)) 450 | self.assertEqual(ad3(2, False, 11), (2, False, 11)) 451 | self.assertRaises(fpm.GuardError, ad3, 2.0) 452 | self.assertRaises(fpm.GuardError, ad3, 1, []) 453 | self.assertRaises(fpm.GuardError, ad3, 1, False, -1) 454 | 455 | def test_rguard_annotations(self): 456 | "Test every possible correct way of defining guards with rguard decorator (Py3 only)" 457 | 458 | if not py3: 459 | return 460 | 461 | # relguard with all annotations, rguard mixed 462 | self.assertEqual(rwak3rm(1, "asd", 1.0), (1, "asd", 1.0)) 463 | self.assertRaises(fpm.GuardError, rwak3rm, 9000, "x", 9000.1) 464 | 465 | # relguard with some annotations, rguard mixed 466 | self.assertEqual(rwsk3rm(1, "asd", 1.0), (1, "asd", 1.0)) 467 | self.assertRaises(fpm.GuardError, rwsk3rm, 9000, "x", 9000.1) 468 | 469 | def test_raguard_annotations(self): 470 | "Test raguard" 471 | 472 | if not py3: 473 | return 474 | 475 | # relguard-only all 476 | self.assertEqual(roa3ra(10, 10, 15), (10, 10, 15)) 477 | self.assertRaises(fpm.GuardError, roa3ra, 1, 5, 12) 478 | self.assertRaises(fpm.GuardError, roa3ra, 1, 1, -5) 479 | self.assertRaises(fpm.GuardError, roa3ra, [], [], 0) 480 | 481 | # relguard-only some 482 | self.assertEqual(ros3ra(10, -10, 11), (10, -10, 11)) 483 | self.assertRaises(fpm.GuardError, ros3ra, 0, 0, 0) 484 | 485 | # relguard-only none 486 | self.assertEqual(ron3ra(1, 1, 1), (1, 1, 1)) 487 | #self.assertRaises(fpm.GuardError, ron3ra, 1, 1, 1) 488 | 489 | # relguard with all annotations 490 | self.assertEqual(rwak3ra(1, "asd", 1.0), (1, "asd", 1.0)) 491 | self.assertRaises(fpm.GuardError, rwak3ra, 9000, "x", 9000.1) 492 | self.assertRaises(fpm.GuardError, rwak3ra, 'x', 'y', 'x') 493 | self.assertRaises(fpm.GuardError, rwak3ra, 1, 0, 1.0) 494 | self.assertRaises(fpm.GuardError, rwak3ra, 1, 'x', 1) 495 | 496 | # relguard with some annotations 497 | self.assertEqual(rwsk3ra(1, "asd", 1.0), (1, "asd", 1.0)) 498 | self.assertRaises(fpm.GuardError, rwsk3ra, 9000, "x", 9000.1) 499 | self.assertRaises(fpm.GuardError, rwak3ra, 'x', 'y', 'x') 500 | 501 | def test_guarded_definition_errors_decargs(self): 502 | "Test syntactically correct but erroneous guard definitions" 503 | 504 | def decoratee_basic(a, b, c): 505 | return (a, b, c) 506 | ## no relguard tests: 507 | 508 | # no guards 509 | self.assertRaises(ValueError, fpm.guard, decoratee_basic) # @guard 510 | self.assertRaises(ValueError, fpm.guard(), decoratee_basic) # @guard() 511 | 512 | # too many positionals 513 | self.assertRaises(ValueError, fpm.guard(fpm.eTrue, fpm.eTrue, fpm.eTrue, fpm.eTrue), decoratee_basic) # @guard(....) 514 | 515 | # not known keyword arg 516 | self.assertRaises(ValueError, fpm.guard(d=fpm.eTrue), decoratee_basic) 517 | 518 | # positional-keyword overlap (same) 519 | self.assertRaises(ValueError, fpm.guard(fpm.eFalse, fpm.eFalse, a=fpm.eFalse), decoratee_basic) 520 | self.assertRaises(ValueError, fpm.guard(fpm.eFalse, a=fpm.eFalse), decoratee_basic) 521 | 522 | # positional-keyword overlap (diff) 523 | self.assertRaises(ValueError, fpm.guard(fpm.eTrue, fpm.eFalse, a=fpm.eFalse), decoratee_basic) 524 | self.assertRaises(ValueError, fpm.guard(fpm.eTrue, a=fpm.eFalse), decoratee_basic) 525 | 526 | # not a guard 527 | self.assertRaises(ValueError, fpm.guard(zip, list, all), decoratee_basic) 528 | 529 | ## with relguard tests: 530 | 531 | # not a relguard 532 | try: 533 | fpm.guard(lambda a, b, c: a and b and c)(decoratee_basic) 534 | except ValueError: 535 | pass 536 | else: 537 | self.fail("Expected ValueError") 538 | 539 | # wrong arg names in relguard 540 | self.assertRaises(ValueError, fpm.guard(fpm.relguard(lambda a, v, x: a and v and x)), decoratee_basic) 541 | 542 | # exceeding arg names in relguard (good mixed with bad) 543 | self.assertRaises(ValueError, fpm.guard(fpm.relguard(lambda a, b, c, x: a and b and c and x)), decoratee_basic) 544 | 545 | # relguard mixed with positionals 546 | self.assertRaises(ValueError, fpm.guard(fpm.relguard(lambda: True), fpm.eq(0), fpm.eFalse, fpm.eFalse), decoratee_basic) 547 | 548 | # relguard with positionals as not 1st arg 549 | self.assertRaises(ValueError, fpm.guard(fpm.eq(0), fpm.relguard(lambda: True), fpm.eFalse, fpm.eFalse), decoratee_basic) 550 | 551 | # defaults not passing through guards and relguard 552 | #def decoratee_defaults(a, b=10, c=0): 553 | # return (a, b, c) 554 | 555 | #assertRaises(ValueError, fpm.guard(fpm._, fpm.gt(100), fpm.eTrue)(decoratee_defaults)) 556 | 557 | # var (kw)args 558 | def decoratee_var1(*args): 559 | return args 560 | self.assertRaises(ValueError, fpm.guard(fpm.eTrue), decoratee_var1) 561 | 562 | def decoratee_var2(a, *args): 563 | return (a, args) 564 | self.assertRaises(ValueError, fpm.guard(args=fpm.eTrue), decoratee_var2) 565 | 566 | def decoratee_var3(**kwargs): 567 | return kwargs 568 | self.assertRaises(ValueError, fpm.guard(kwargs=fpm.eTrue), decoratee_var3) 569 | 570 | def decoratee_var4(a, b, *args, **kwargs): 571 | return (a, b, args, kwargs) 572 | self.assertRaises(ValueError, fpm.guard(fpm.eTrue, fpm.eTrue, fpm.eTrue, fpm.eTrue), decoratee_var4) 573 | 574 | # guarding guarded 575 | @fpm.guard(fpm.ne(0)) 576 | def already_guarded(a): 577 | return a 578 | self.assertRaises(ValueError, fpm.guard(fpm.eTrue), already_guarded) 579 | 580 | if py3: 581 | # raguard, no return annotation 582 | def nra(): pass 583 | self.assertRaises(ValueError, fpm.raguard, nra) 584 | 585 | # not erroneous, but annotations should be ignored if decorator arguments are specified 586 | da_with_ann = fpm.guard(fpm.relguard(lambda a, c: a != c), a=~fpm.isoftype(int), b=~fpm.isoftype(str))(rwak3_bare) 587 | self.assertRaises(fpm.GuardError, da_with_ann, 1, 'x', 1) 588 | self.assertRaises(fpm.GuardError, da_with_ann, 1, 'x', 2) 589 | self.assertRaises(fpm.GuardError, da_with_ann, 'x', 'y', 2) 590 | self.assertEqual(da_with_ann('x', [], 2), ('x', [], 2)) 591 | self.assertIs(da_with_ann._argument_guards['c'], fpm._) 592 | 593 | class IsDispatchCorrect(unittest.TestCase): 594 | def test_with_catchall(self): 595 | "Test function defined with catch all case" 596 | 597 | # all defaults 598 | @fpm.case 599 | def foo(a='bar', b=1337, c=(0, 1, 2)): 600 | return (1, a, b, c) 601 | 602 | @fpm.case 603 | def foo(a='baz', b=1337, c=(0, 1, 2)): 604 | return (2, a, b, c) 605 | 606 | # some defaults 607 | @fpm.case 608 | def foo(a, b=1331, c=(0, 1, 2)): 609 | return (3, a, b, c) 610 | 611 | @fpm.case 612 | def foo(a, b, c='baz'): 613 | return (4, a, b, c) 614 | 615 | # decorator args, some 616 | @fpm.case(1, fpm._, fpm._) 617 | def foo(a, b, c): 618 | return (5, a, b, c) 619 | 620 | # _ in defaults 621 | @fpm.case 622 | def foo(a=fpm._, b=2, c=fpm._): 623 | return (6, a, b, c) 624 | 625 | # any/3 626 | @fpm.case 627 | def foo(a, b, c): 628 | return (7, a, b, c) 629 | 630 | # any/4 631 | @fpm.case 632 | def foo(a, b, c, d): #different arity, but is that an issue? 633 | return (8, a, b, c, d) 634 | 635 | self.assertEqual(foo('bar', 1337, (0, 1, 2)), (1, 'bar', 1337, (0, 1, 2))) 636 | self.assertEqual(foo('baz', 1337, (0, 1, 2)), (2, 'baz', 1337, (0, 1, 2))) 637 | self.assertEqual(foo('xyz', 1331, (0, 1, 2)), (3, 'xyz', 1331, (0, 1, 2))) 638 | self.assertEqual(foo('xyz', 1237, 'baz'), (4, 'xyz', 1237, 'baz')) 639 | self.assertEqual(foo(1, dict(), False), (5, 1, dict(), False)) 640 | self.assertEqual(foo([], 2, b''), (6, [], 2, b'')) 641 | self.assertEqual(foo('xyz', 1337, (0, 1, 2)), (7, 'xyz', 1337, (0, 1, 2))) 642 | self.assertEqual(foo('xyz', 1237, (0, 1, 2)), (7, 'xyz', 1237, (0, 1, 2))) 643 | self.assertEqual(foo('bar', 1337, (0, 1, 2), True), (8, 'bar', 1337, (0, 1, 2), True)) 644 | 645 | def test_factorial(self): 646 | "Erlang-like factorial" 647 | 648 | # no accumulator, because this is just for a testing, not performance. 649 | 650 | @fpm.case 651 | def fac(n=0): 652 | return 1 653 | 654 | @fpm.case 655 | @fpm.guard(fpm.ge(0) & fpm.isoftype(int)) 656 | def fac(n): 657 | return n * fac(n-1) 658 | 659 | self.assertRaises(fpm.MatchError, fac, -1) 660 | self.assertRaises(fpm.MatchError, fac, 'not an int') 661 | 662 | self.assertEqual(fac(0), 1) 663 | self.assertEqual(fac(1), 1) 664 | self.assertEqual(fac(2), 2) 665 | self.assertEqual(fac(3), 6) 666 | 667 | self.assertEqual(fac(7), 5040) 668 | self.assertEqual(fac(8), 40320) 669 | self.assertEqual(fac(9), 362880) 670 | 671 | 672 | def test_fibonacci(self): 673 | "Erlang-like fibonacci" 674 | 675 | # no accumulator, because this is just for a testing, not performance. 676 | 677 | @fpm.case 678 | def fib(n=0): 679 | return 0 680 | 681 | @fpm.case 682 | def fib(n=1): 683 | return 1 684 | 685 | @fpm.case 686 | @fpm.guard(fpm.ge(0)) 687 | def fib(n): 688 | return fib(n-1) + fib(n-2) 689 | 690 | self.assertRaises(fpm.MatchError, fib, -1) 691 | 692 | self.assertEqual(fib(0), 0) 693 | self.assertEqual(fib(1), 1) 694 | self.assertEqual(fib(2), 1) 695 | 696 | self.assertEqual(fib(8), 21) 697 | self.assertEqual(fib(9), 34) 698 | self.assertEqual(fib(10), 55) 699 | self.assertEqual(fib(11), 89) 700 | 701 | def test_bad_case_defs(self): 702 | "Syntactically correct, but erroneous multi-clause functions definitions." 703 | 704 | # varargs 705 | def varargs(a, b, *c): 706 | return (1, a, b, c) 707 | 708 | self.assertRaises(ValueError, fpm.case(1, 2, ()), varargs) 709 | 710 | def varargs(a, b, **c): 711 | return (2, a, b, c) 712 | 713 | self.assertRaises(ValueError, fpm.case(1, 2, {}), varargs) 714 | 715 | # kwonly 716 | if py3: 717 | self.assertRaises(ValueError, fpm.case(a=1, b=2, c=3), kwonly) 718 | 719 | # decorator args and defaults 720 | def decdef(a=1, b=2, c=3): 721 | return (3, a, b, c) 722 | 723 | self.assertRaises(ValueError, fpm.case(1, 2, 3), decdef) 724 | 725 | # exceeding positionals 726 | def regular (a, b, c): 727 | return (4, a, b, c) 728 | 729 | self.assertRaises(ValueError, fpm.case(1, 2, 3, 4), regular) 730 | 731 | # arg name mismatch 732 | self.assertRaises(ValueError, fpm.case(c=3, d=0), regular) 733 | 734 | def test_dispatch(self): 735 | "Test dispatch decorator" # TODO: improve test 736 | 737 | @fpm.dispatch(int, float, str) 738 | def dis(a, b, c): 739 | return (1, a, b, c) 740 | 741 | @fpm.dispatch(int, fpm._, int) 742 | def dis(a, b, c): 743 | return (2, a, b, c) 744 | 745 | @fpm.dispatch 746 | def dis(a=str, b=str, c=str): 747 | return(3, a, b, c) 748 | 749 | self.assertEqual(dis(1, 2.0, '3'), (1, 1, 2.0, '3')) 750 | self.assertEqual(dis(1, b'', 3), (2, 1, b'', 3)) 751 | self.assertEqual(dis('1', '2', '3'), (3, '1', '2', '3')) 752 | self.assertRaises(fpm.MatchError, dis, '1', [], 0) 753 | 754 | if __name__ == '__main__': 755 | unittest.main() 756 | --------------------------------------------------------------------------------