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