1import inspect 2import sys 3from functools import partial 4from importlib import import_module 5from operator import attrgetter 6from textwrap import dedent 7from types import MethodType 8from cytoolz.utils import no_default 9import cytoolz._signatures as _sigs 10 11from toolz.functoolz import (InstanceProperty, instanceproperty, is_arity, 12 num_required_args, has_varargs, has_keywords, 13 is_valid_args, is_partial_args) 14 15cimport cython 16from cpython.dict cimport PyDict_Merge, PyDict_New 17from cpython.object cimport (PyCallable_Check, PyObject_Call, PyObject_CallObject, 18 PyObject_RichCompare, Py_EQ, Py_NE) 19from cpython.ref cimport PyObject 20from cpython.sequence cimport PySequence_Concat 21from cpython.set cimport PyFrozenSet_New 22from cpython.tuple cimport PyTuple_Check, PyTuple_GET_SIZE 23 24 25__all__ = ['identity', 'thread_first', 'thread_last', 'memoize', 'compose', 'compose_left', 26 'pipe', 'complement', 'juxt', 'do', 'curry', 'memoize', 'flip', 27 'excepts', 'apply'] 28 29 30cpdef object identity(object x): 31 return x 32 33 34def apply(*func_and_args, **kwargs): 35 """ 36 Applies a function and returns the results 37 38 >>> def double(x): return 2*x 39 >>> def inc(x): return x + 1 40 >>> apply(double, 5) 41 10 42 43 >>> tuple(map(apply, [double, inc, double], [10, 500, 8000])) 44 (20, 501, 16000) 45 """ 46 if not func_and_args: 47 raise TypeError('func argument is required') 48 return func_and_args[0](*func_and_args[1:], **kwargs) 49 50 51cdef object c_thread_first(object val, object forms): 52 cdef object form, func 53 cdef tuple args 54 for form in forms: 55 if PyCallable_Check(form): 56 val = form(val) 57 elif PyTuple_Check(form): 58 func, args = form[0], (val,) + form[1:] 59 val = PyObject_CallObject(func, args) 60 else: 61 val = None 62 return val 63 64 65def thread_first(val, *forms): 66 """ 67 Thread value through a sequence of functions/forms 68 69 >>> def double(x): return 2*x 70 >>> def inc(x): return x + 1 71 >>> thread_first(1, inc, double) 72 4 73 74 If the function expects more than one input you can specify those inputs 75 in a tuple. The value is used as the first input. 76 77 >>> def add(x, y): return x + y 78 >>> def pow(x, y): return x**y 79 >>> thread_first(1, (add, 4), (pow, 2)) # pow(add(1, 4), 2) 80 25 81 82 So in general 83 thread_first(x, f, (g, y, z)) 84 expands to 85 g(f(x), y, z) 86 87 See Also: 88 thread_last 89 """ 90 return c_thread_first(val, forms) 91 92 93cdef object c_thread_last(object val, object forms): 94 cdef object form, func 95 cdef tuple args 96 for form in forms: 97 if PyCallable_Check(form): 98 val = form(val) 99 elif PyTuple_Check(form): 100 func, args = form[0], form[1:] + (val,) 101 val = PyObject_CallObject(func, args) 102 else: 103 val = None 104 return val 105 106 107def thread_last(val, *forms): 108 """ 109 Thread value through a sequence of functions/forms 110 111 >>> def double(x): return 2*x 112 >>> def inc(x): return x + 1 113 >>> thread_last(1, inc, double) 114 4 115 116 If the function expects more than one input you can specify those inputs 117 in a tuple. The value is used as the last input. 118 119 >>> def add(x, y): return x + y 120 >>> def pow(x, y): return x**y 121 >>> thread_last(1, (add, 4), (pow, 2)) # pow(2, add(4, 1)) 122 32 123 124 So in general 125 thread_last(x, f, (g, y, z)) 126 expands to 127 g(y, z, f(x)) 128 129 >>> def iseven(x): 130 ... return x % 2 == 0 131 >>> list(thread_last([1, 2, 3], (map, inc), (filter, iseven))) 132 [2, 4] 133 134 See Also: 135 thread_first 136 """ 137 return c_thread_last(val, forms) 138 139 140cdef struct partialobject: 141 PyObject _ 142 PyObject *fn 143 PyObject *args 144 PyObject *kw 145 PyObject *dict 146 PyObject *weakreflist 147 148 149cdef object _partial = partial(lambda: None) 150 151 152cdef object _empty_kwargs(): 153 if <object> (<partialobject*> _partial).kw is None: 154 return None 155 return PyDict_New() 156 157 158cdef class curry: 159 """ curry(self, *args, **kwargs) 160 161 Curry a callable function 162 163 Enables partial application of arguments through calling a function with an 164 incomplete set of arguments. 165 166 >>> def mul(x, y): 167 ... return x * y 168 >>> mul = curry(mul) 169 170 >>> double = mul(2) 171 >>> double(10) 172 20 173 174 Also supports keyword arguments 175 176 >>> @curry # Can use curry as a decorator 177 ... def f(x, y, a=10): 178 ... return a * (x + y) 179 180 >>> add = f(a=1) 181 >>> add(2, 3) 182 5 183 184 See Also: 185 cytoolz.curried - namespace of curried functions 186 https://toolz.readthedocs.io/en/latest/curry.html 187 """ 188 189 def __cinit__(self, *args, **kwargs): 190 if not args: 191 raise TypeError('__init__() takes at least 2 arguments (1 given)') 192 func, args = args[0], args[1:] 193 if not PyCallable_Check(func): 194 raise TypeError("Input must be callable") 195 196 # curry- or functools.partial-like object? Unpack and merge arguments 197 if (hasattr(func, 'func') 198 and hasattr(func, 'args') 199 and hasattr(func, 'keywords') 200 and isinstance(func.args, tuple)): 201 if func.keywords: 202 PyDict_Merge(kwargs, func.keywords, False) 203 ## Equivalent to: 204 # for key, val in func.keywords.items(): 205 # if key not in kwargs: 206 # kwargs[key] = val 207 args = func.args + args 208 func = func.func 209 210 self.func = func 211 self.args = args 212 self.keywords = kwargs if kwargs else _empty_kwargs() 213 self.__doc__ = getattr(func, '__doc__', None) 214 self.__name__ = getattr(func, '__name__', '<curry>') 215 self.__module__ = getattr(func, '__module__', None) 216 self.__qualname__ = getattr(func, '__qualname__', None) 217 self._sigspec = None 218 self._has_unknown_args = None 219 220 def __str__(self): 221 return str(self.func) 222 223 def __repr__(self): 224 return repr(self.func) 225 226 def __hash__(self): 227 return hash((self.func, self.args, 228 frozenset(self.keywords.items()) if self.keywords 229 else None)) 230 231 def __richcmp__(self, other, int op): 232 is_equal = (isinstance(other, curry) and self.func == other.func and 233 self.args == other.args and self.keywords == other.keywords) 234 if op == Py_EQ: 235 return is_equal 236 if op == Py_NE: 237 return not is_equal 238 return PyObject_RichCompare(id(self), id(other), op) 239 240 def __call__(self, *args, **kwargs): 241 cdef object val 242 243 if PyTuple_GET_SIZE(args) == 0: 244 args = self.args 245 elif PyTuple_GET_SIZE(self.args) != 0: 246 args = PySequence_Concat(self.args, args) 247 if self.keywords is not None: 248 PyDict_Merge(kwargs, self.keywords, False) 249 try: 250 return self.func(*args, **kwargs) 251 except TypeError as val: 252 if self._should_curry_internal(args, kwargs, val): 253 return type(self)(self.func, *args, **kwargs) 254 raise 255 256 def _should_curry(self, args, kwargs, exc=None): 257 if PyTuple_GET_SIZE(args) == 0: 258 args = self.args 259 elif PyTuple_GET_SIZE(self.args) != 0: 260 args = PySequence_Concat(self.args, args) 261 if self.keywords is not None: 262 PyDict_Merge(kwargs, self.keywords, False) 263 return self._should_curry_internal(args, kwargs) 264 265 def _should_curry_internal(self, args, kwargs, exc=None): 266 func = self.func 267 268 # `toolz` has these three lines 269 #args = self.args + args 270 #if self.keywords: 271 # kwargs = dict(self.keywords, **kwargs) 272 273 if self._sigspec is None: 274 sigspec = self._sigspec = _sigs.signature_or_spec(func) 275 self._has_unknown_args = has_varargs(func, sigspec) is not False 276 else: 277 sigspec = self._sigspec 278 279 if is_partial_args(func, args, kwargs, sigspec) is False: 280 # Nothing can make the call valid 281 return False 282 elif self._has_unknown_args: 283 # The call may be valid and raised a TypeError, but we curry 284 # anyway because the function may have `*args`. This is useful 285 # for decorators with signature `func(*args, **kwargs)`. 286 return True 287 elif not is_valid_args(func, args, kwargs, sigspec): 288 # Adding more arguments may make the call valid 289 return True 290 else: 291 # There was a genuine TypeError 292 return False 293 294 def bind(self, *args, **kwargs): 295 return type(self)(self, *args, **kwargs) 296 297 def call(self, *args, **kwargs): 298 cdef object val 299 300 if PyTuple_GET_SIZE(args) == 0: 301 args = self.args 302 elif PyTuple_GET_SIZE(self.args) != 0: 303 args = PySequence_Concat(self.args, args) 304 if self.keywords is not None: 305 PyDict_Merge(kwargs, self.keywords, False) 306 return self.func(*args, **kwargs) 307 308 def __get__(self, instance, owner): 309 if instance is None: 310 return self 311 return type(self)(self, instance) 312 313 property __signature__: 314 def __get__(self): 315 sig = inspect.signature(self.func) 316 args = self.args or () 317 keywords = self.keywords or {} 318 if is_partial_args(self.func, args, keywords, sig) is False: 319 raise TypeError('curry object has incorrect arguments') 320 321 params = list(sig.parameters.values()) 322 skip = 0 323 for param in params[:len(args)]: 324 if param.kind == param.VAR_POSITIONAL: 325 break 326 skip += 1 327 328 kwonly = False 329 newparams = [] 330 for param in params[skip:]: 331 kind = param.kind 332 default = param.default 333 if kind == param.VAR_KEYWORD: 334 pass 335 elif kind == param.VAR_POSITIONAL: 336 if kwonly: 337 continue 338 elif param.name in keywords: 339 default = keywords[param.name] 340 kind = param.KEYWORD_ONLY 341 kwonly = True 342 else: 343 if kwonly: 344 kind = param.KEYWORD_ONLY 345 if default is param.empty: 346 default = no_default 347 newparams.append(param.replace(default=default, kind=kind)) 348 349 return sig.replace(parameters=newparams) 350 351 def __reduce__(self): 352 func = self.func 353 modname = getattr(func, '__module__', None) 354 qualname = getattr(func, '__qualname__', None) 355 if qualname is None: 356 qualname = getattr(func, '__name__', None) 357 is_decorated = None 358 if modname and qualname: 359 attrs = [] 360 obj = import_module(modname) 361 for attr in qualname.split('.'): 362 if isinstance(obj, curry): 363 attrs.append('func') 364 obj = obj.func 365 obj = getattr(obj, attr, None) 366 if obj is None: 367 break 368 attrs.append(attr) 369 if isinstance(obj, curry) and obj.func is func: 370 is_decorated = obj is self 371 qualname = '.'.join(attrs) 372 func = '%s:%s' % (modname, qualname) 373 374 state = (type(self), func, self.args, self.keywords, is_decorated) 375 return (_restore_curry, state) 376 377 378cpdef object _restore_curry(cls, func, args, kwargs, is_decorated): 379 if isinstance(func, str): 380 modname, qualname = func.rsplit(':', 1) 381 obj = import_module(modname) 382 for attr in qualname.split('.'): 383 obj = getattr(obj, attr) 384 if is_decorated: 385 return obj 386 func = obj.func 387 obj = cls(func, *args, **(kwargs or {})) 388 return obj 389 390 391cpdef object memoize(object func, object cache=None, object key=None): 392 """ 393 Cache a function's result for speedy future evaluation 394 395 Considerations: 396 Trades memory for speed. 397 Only use on pure functions. 398 399 >>> def add(x, y): return x + y 400 >>> add = memoize(add) 401 402 Or use as a decorator 403 404 >>> @memoize 405 ... def add(x, y): 406 ... return x + y 407 408 Use the ``cache`` keyword to provide a dict-like object as an initial cache 409 410 >>> @memoize(cache={(1, 2): 3}) 411 ... def add(x, y): 412 ... return x + y 413 414 Note that the above works as a decorator because ``memoize`` is curried. 415 416 It is also possible to provide a ``key(args, kwargs)`` function that 417 calculates keys used for the cache, which receives an ``args`` tuple and 418 ``kwargs`` dict as input, and must return a hashable value. However, 419 the default key function should be sufficient most of the time. 420 421 >>> # Use key function that ignores extraneous keyword arguments 422 >>> @memoize(key=lambda args, kwargs: args) 423 ... def add(x, y, verbose=False): 424 ... if verbose: 425 ... print('Calculating %s + %s' % (x, y)) 426 ... return x + y 427 """ 428 return _memoize(func, cache, key) 429 430 431cdef class _memoize: 432 433 property __doc__: 434 def __get__(self): 435 return self.func.__doc__ 436 437 property __name__: 438 def __get__(self): 439 return self.func.__name__ 440 441 property __wrapped__: 442 def __get__(self): 443 return self.func 444 445 def __cinit__(self, func, cache, key): 446 self.func = func 447 if cache is None: 448 self.cache = PyDict_New() 449 else: 450 self.cache = cache 451 self.key = key 452 453 try: 454 self.may_have_kwargs = has_keywords(func) is not False 455 # Is unary function (single arg, no variadic argument or keywords)? 456 self.is_unary = is_arity(1, func) 457 except TypeError: 458 self.is_unary = False 459 self.may_have_kwargs = True 460 461 def __call__(self, *args, **kwargs): 462 cdef object key 463 if self.key is not None: 464 key = self.key(args, kwargs) 465 elif self.is_unary: 466 key = args[0] 467 elif self.may_have_kwargs: 468 key = (args or None, 469 PyFrozenSet_New(kwargs.items()) if kwargs else None) 470 else: 471 key = args 472 473 if key in self.cache: 474 return self.cache[key] 475 else: 476 result = PyObject_Call(self.func, args, kwargs) 477 self.cache[key] = result 478 return result 479 480 def __get__(self, instance, owner): 481 if instance is None: 482 return self 483 return curry(self, instance) 484 485 486cdef class Compose: 487 """ Compose(self, *funcs) 488 489 A composition of functions 490 491 See Also: 492 compose 493 """ 494 # fix for #103, note: we cannot use __name__ at module-scope in cython 495 __module__ = 'cytooz.functoolz' 496 497 def __cinit__(self, *funcs): 498 self.first = funcs[-1] 499 self.funcs = tuple(reversed(funcs[:-1])) 500 501 def __call__(self, *args, **kwargs): 502 cdef object func, ret 503 ret = PyObject_Call(self.first, args, kwargs) 504 for func in self.funcs: 505 ret = func(ret) 506 return ret 507 508 def __reduce__(self): 509 return (Compose, (self.first,), self.funcs) 510 511 def __setstate__(self, state): 512 self.funcs = state 513 514 def __repr__(self): 515 return '{.__class__.__name__}{!r}'.format( 516 self, tuple(reversed((self.first, ) + self.funcs))) 517 518 def __eq__(self, other): 519 if isinstance(other, Compose): 520 return other.first == self.first and other.funcs == self.funcs 521 return NotImplemented 522 523 def __ne__(self, other): 524 if isinstance(other, Compose): 525 return other.first != self.first or other.funcs != self.funcs 526 return NotImplemented 527 528 def __hash__(self): 529 return hash(self.first) ^ hash(self.funcs) 530 531 def __get__(self, obj, objtype): 532 if obj is None: 533 return self 534 else: 535 return MethodType(self, obj) 536 537 property __wrapped__: 538 def __get__(self): 539 return self.first 540 541 property __signature__: 542 def __get__(self): 543 base = inspect.signature(self.first) 544 last = inspect.signature(self.funcs[-1]) 545 return base.replace(return_annotation=last.return_annotation) 546 547 property __name__: 548 def __get__(self): 549 try: 550 return '_of_'.join( 551 f.__name__ for f in reversed((self.first,) + self.funcs) 552 ) 553 except AttributeError: 554 return type(self).__name__ 555 556 property __doc__: 557 def __get__(self): 558 def composed_doc(*fs): 559 """Generate a docstring for the composition of fs. 560 """ 561 if not fs: 562 # Argument name for the docstring. 563 return '*args, **kwargs' 564 565 return '{f}({g})'.format(f=fs[0].__name__, g=composed_doc(*fs[1:])) 566 567 try: 568 return ( 569 'lambda *args, **kwargs: ' + 570 composed_doc(*reversed((self.first,) + self.funcs)) 571 ) 572 except AttributeError: 573 # One of our callables does not have a `__name__`, whatever. 574 return 'A composition of functions' 575 576 577cdef object c_compose(object funcs): 578 if not funcs: 579 return identity 580 elif len(funcs) == 1: 581 return funcs[0] 582 else: 583 return Compose(*funcs) 584 585 586def compose(*funcs): 587 """ 588 Compose functions to operate in series. 589 590 Returns a function that applies other functions in sequence. 591 592 Functions are applied from right to left so that 593 ``compose(f, g, h)(x, y)`` is the same as ``f(g(h(x, y)))``. 594 595 If no arguments are provided, the identity function (f(x) = x) is returned. 596 597 >>> inc = lambda i: i + 1 598 >>> compose(str, inc)(3) 599 '4' 600 601 See Also: 602 compose_left 603 pipe 604 """ 605 return c_compose(funcs) 606 607 608cdef object c_compose_left(object funcs): 609 if not funcs: 610 return identity 611 elif len(funcs) == 1: 612 return funcs[0] 613 else: 614 return Compose(*reversed(funcs)) 615 616 617def compose_left(*funcs): 618 """ 619 Compose functions to operate in series. 620 621 Returns a function that applies other functions in sequence. 622 623 Functions are applied from left to right so that 624 ``compose_left(f, g, h)(x, y)`` is the same as ``h(g(f(x, y)))``. 625 626 If no arguments are provided, the identity function (f(x) = x) is returned. 627 628 >>> inc = lambda i: i + 1 629 >>> compose_left(inc, str)(3) 630 '4' 631 632 See Also: 633 compose 634 pipe 635 """ 636 return c_compose_left(funcs) 637 638 639cdef object c_pipe(object data, object funcs): 640 cdef object func 641 for func in funcs: 642 data = func(data) 643 return data 644 645 646def pipe(data, *funcs): 647 """ 648 Pipe a value through a sequence of functions 649 650 I.e. ``pipe(data, f, g, h)`` is equivalent to ``h(g(f(data)))`` 651 652 We think of the value as progressing through a pipe of several 653 transformations, much like pipes in UNIX 654 655 ``$ cat data | f | g | h`` 656 657 >>> double = lambda i: 2 * i 658 >>> pipe(3, double, str) 659 '6' 660 661 See Also: 662 compose 663 compose_left 664 thread_first 665 thread_last 666 """ 667 return c_pipe(data, funcs) 668 669 670cdef class complement: 671 """ complement(func) 672 673 Convert a predicate function to its logical complement. 674 675 In other words, return a function that, for inputs that normally 676 yield True, yields False, and vice-versa. 677 678 >>> def iseven(n): return n % 2 == 0 679 >>> isodd = complement(iseven) 680 >>> iseven(2) 681 True 682 >>> isodd(2) 683 False 684 """ 685 def __cinit__(self, func): 686 self.func = func 687 688 def __call__(self, *args, **kwargs): 689 return not PyObject_Call(self.func, args, kwargs) # use PyObject_Not? 690 691 def __reduce__(self): 692 return (complement, (self.func,)) 693 694 695cdef class _juxt_inner: 696 def __cinit__(self, funcs): 697 self.funcs = tuple(funcs) 698 699 def __call__(self, *args, **kwargs): 700 if kwargs: 701 return tuple(PyObject_Call(func, args, kwargs) for func in self.funcs) 702 else: 703 return tuple(PyObject_CallObject(func, args) for func in self.funcs) 704 705 def __reduce__(self): 706 return (_juxt_inner, (self.funcs,)) 707 708 709cdef object c_juxt(object funcs): 710 return _juxt_inner(funcs) 711 712 713def juxt(*funcs): 714 """ 715 Creates a function that calls several functions with the same arguments 716 717 Takes several functions and returns a function that applies its arguments 718 to each of those functions then returns a tuple of the results. 719 720 Name comes from juxtaposition: the fact of two things being seen or placed 721 close together with contrasting effect. 722 723 >>> inc = lambda x: x + 1 724 >>> double = lambda x: x * 2 725 >>> juxt(inc, double)(10) 726 (11, 20) 727 >>> juxt([inc, double])(10) 728 (11, 20) 729 """ 730 if len(funcs) == 1 and not PyCallable_Check(funcs[0]): 731 funcs = funcs[0] 732 return c_juxt(funcs) 733 734 735cpdef object do(object func, object x): 736 """ 737 Runs ``func`` on ``x``, returns ``x`` 738 739 Because the results of ``func`` are not returned, only the side 740 effects of ``func`` are relevant. 741 742 Logging functions can be made by composing ``do`` with a storage function 743 like ``list.append`` or ``file.write`` 744 745 >>> from cytoolz import compose 746 >>> from cytoolz.curried import do 747 748 >>> log = [] 749 >>> inc = lambda x: x + 1 750 >>> inc = compose(inc, do(log.append)) 751 >>> inc(1) 752 2 753 >>> inc(11) 754 12 755 >>> log 756 [1, 11] 757 """ 758 func(x) 759 return x 760 761 762cpdef object flip(object func, object a, object b): 763 """ 764 Call the function call with the arguments flipped 765 766 This function is curried. 767 768 >>> def div(a, b): 769 ... return a // b 770 ... 771 >>> flip(div, 2, 6) 772 3 773 >>> div_by_two = flip(div, 2) 774 >>> div_by_two(4) 775 2 776 777 This is particularly useful for built in functions and functions defined 778 in C extensions that accept positional only arguments. For example: 779 isinstance, issubclass. 780 781 >>> data = [1, 'a', 'b', 2, 1.5, object(), 3] 782 >>> only_ints = list(filter(flip(isinstance, int), data)) 783 >>> only_ints 784 [1, 2, 3] 785 """ 786 return PyObject_CallObject(func, (b, a)) 787 788 789_flip = flip # uncurried 790 791 792cpdef object return_none(object exc): 793 """ 794 Returns None. 795 """ 796 return None 797 798 799cdef class excepts: 800 """ 801 A wrapper around a function to catch exceptions and 802 dispatch to a handler. 803 804 This is like a functional try/except block, in the same way that 805 ifexprs are functional if/else blocks. 806 807 Examples 808 -------- 809 >>> excepting = excepts( 810 ... ValueError, 811 ... lambda a: [1, 2].index(a), 812 ... lambda _: -1, 813 ... ) 814 >>> excepting(1) 815 0 816 >>> excepting(3) 817 -1 818 819 Multiple exceptions and default except clause. 820 >>> excepting = excepts((IndexError, KeyError), lambda a: a[0]) 821 >>> excepting([]) 822 >>> excepting([1]) 823 1 824 >>> excepting({}) 825 >>> excepting({0: 1}) 826 1 827 """ 828 829 def __init__(self, exc, func, handler=return_none): 830 self.exc = exc 831 self.func = func 832 self.handler = handler 833 834 def __call__(self, *args, **kwargs): 835 try: 836 return self.func(*args, **kwargs) 837 except self.exc as e: 838 return self.handler(e) 839 840 property __name__: 841 def __get__(self): 842 exc = self.exc 843 try: 844 if isinstance(exc, tuple): 845 exc_name = '_or_'.join(map(attrgetter('__name__'), exc)) 846 else: 847 exc_name = exc.__name__ 848 return '%s_excepting_%s' % (self.func.__name__, exc_name) 849 except AttributeError: 850 return 'excepting' 851 852 property __doc__: 853 def __get__(self): 854 exc = self.exc 855 try: 856 if isinstance(exc, tuple): 857 exc_name = '(%s)' % ', '.join( 858 map(attrgetter('__name__'), exc), 859 ) 860 else: 861 exc_name = exc.__name__ 862 863 return dedent( 864 """\ 865 A wrapper around {inst.func.__name__!r} that will except: 866 {exc} 867 and handle any exceptions with {inst.handler.__name__!r}. 868 869 Docs for {inst.func.__name__!r}: 870 {inst.func.__doc__} 871 872 Docs for {inst.handler.__name__!r}: 873 {inst.handler.__doc__} 874 """ 875 ).format( 876 inst=self, 877 exc=exc_name, 878 ) 879 except AttributeError: 880 return type(self).__doc__ 881 882