Source code for unstdlib.standard.functools_

from functools import wraps, partial
import warnings

from .list_ import iterate_items


__all__ = [
    'memoized', 'memoized_property', 'assert_hashable', 'deprecated',
]


def assert_hashable(*args, **kw):
    """ Verify that each argument is hashable.

    Passes silently if successful. Raises descriptive TypeError otherwise.

    Example::

        >>> assert_hashable(1, 'foo', bar='baz')
        >>> assert_hashable(1, [], baz='baz')
        Traceback (most recent call last):
          ...
        TypeError: Argument in position 1 is not hashable: []
        >>> assert_hashable(1, 'foo', bar=[])
        Traceback (most recent call last):
          ...
        TypeError: Keyword argument 'bar' is not hashable: []
    """
    try:
        for i, arg in enumerate(args):
            hash(arg)
    except TypeError:
        raise TypeError('Argument in position %d is not hashable: %r' % (i, arg))
    try:
        for key, val in iterate_items(kw):
            hash(val)
    except TypeError:
        raise TypeError('Keyword argument %r is not hashable: %r' % (key, val))


def _memoized_call(fn, cache, *args, **kw):
    key = (args, tuple(sorted(kw.items())))

    try:
        is_cached = key in cache
    except TypeError as e:
        # Re-raise a more descriptive error if it's a hashing problem.
        assert_hashable(*args, **kw)
        # If it hasn't raised by now, then something else is going on,
        # raise it. (This shouldn't happen.)
        raise e

    if not is_cached:
        cache[key] = fn(*args, **kw)
    return cache[key]


def memoized(fn=None, cache=None):
    """ Memoize a function into an optionally-specificed cache container.

    If the `cache` container is not specified, then the instance container is
    accessible from the wrapped function's `memoize_cache` property.

    Example::

        >>> @memoized
        ... def foo(bar):
        ...   print("Not cached.")
        >>> foo(1)
        Not cached.
        >>> foo(1)
        >>> foo(2)
        Not cached.

    Example with a specific cache container (in this case, the
    ``RecentlyUsedContainer``, which will only store the ``maxsize`` most
    recently accessed items)::

        >>> from unstdlib.standard.collections_ import RecentlyUsedContainer
        >>> lru_container = RecentlyUsedContainer(maxsize=2)
        >>> @memoized(cache=lru_container)
        ... def baz(x):
        ...   print("Not cached.")
        >>> baz(1)
        Not cached.
        >>> baz(1)
        >>> baz(2)
        Not cached.
        >>> baz(3)
        Not cached.
        >>> baz(2)
        >>> baz(1)
        Not cached.
        >>> # Notice that the '2' key remains, but the '1' key was evicted from
        >>> # the cache.
    """
    if fn:
        # This is a hack to support both @memoize and @memoize(...)
        return memoized(cache=cache)(fn)

    if cache is None:
        cache = {}

    def decorator(fn):
        wrapped = wraps(fn)(partial(_memoized_call, fn, cache))
        wrapped.memoize_cache = cache
        return wrapped

    return decorator


# `memoized_property` is lovingly borrowed from @zzzeek, with permission:
#   https://twitter.com/zzzeek/status/310503354268790784
class memoized_property(object):
    """ A read-only @property that is only evaluated once. """
    def __init__(self, fget, doc=None, name=None):
        self.fget = fget
        self.__doc__ = doc or fget.__doc__
        self.__name__ = name or fget.__name__

    def __get__(self, obj, cls):
        if obj is None:
            return self
        obj.__dict__[self.__name__] = result = self.fget(obj)
        return result


def memoized_method(method=None, cache_factory=None):
    """ Memoize a class's method.

    Arguments are similar to to `memoized`, except that the cache container is
    specified with `cache_factory`: a function called with no arguments to
    create the caching container for the instance.

    Note that, unlike `memoized`, the result cache will be stored on the
    instance, so cached results will be deallocated along with the instance.

    Example::

        >>> class Person(object):
        ...     def __init__(self, name):
        ...         self._name = name
        ...     @memoized_method
        ...     def get_name(self):
        ...         print("Calling get_name on %r" %(self._name, ))
        ...         return self._name
        >>> shazow = Person("shazow")
        >>> shazow.get_name()
        Calling get_name on 'shazow'
        'shazow'
        >>> shazow.get_name()
        'shazow'
        >>> shazow._get_name_cache
        {((), ()): 'shazow'}

    Example with a specific cache container::

        >>> from unstdlib.standard.collections_ import RecentlyUsedContainer
        >>> class Foo(object):
        ...     @memoized_method(cache_factory=lambda: RecentlyUsedContainer(maxsize=2))
        ...     def add(self, a, b):
        ...         print("Calling add with %r and %r" %(a, b))
        ...         return a + b
        >>> foo = Foo()
        >>> foo.add(1, 1)
        Calling add with 1 and 1
        2
        >>> foo.add(1, 1)
        2
        >>> foo.add(2, 2)
        Calling add with 2 and 2
        4
        >>> foo.add(3, 3)
        Calling add with 3 and 3
        6
        >>> foo.add(1, 1)
        Calling add with 1 and 1
        2
    """

    if method is None:
        return lambda f: memoized_method(f, cache_factory=cache_factory)

    cache_factory = cache_factory or dict

    @wraps(method)
    def memoized_method_property(self):
        cache = cache_factory()
        cache_attr = "_%s_cache" %(method.__name__, )
        setattr(self, cache_attr, cache)
        result = partial(
            _memoized_call,
            partial(method, self),
            cache
        )
        result.memoize_cache = cache
        return result
    return memoized_property(memoized_method_property)


def deprecated(message, exception=PendingDeprecationWarning):
    """Throw a warning when a function/method will be soon deprecated

    Supports passing a ``message`` and an ``exception`` class
    (uses ``PendingDeprecationWarning`` by default). This is useful if you
    want to alternatively pass a ``DeprecationWarning`` exception for already
    deprecated functions/methods.

    Example::

        >>> import warnings
        >>> from functools import wraps
        >>> message = "this function will be deprecated in the near future"
        >>> @deprecated(message)
        ... def foo(n):
        ...     return n+n
        >>> with warnings.catch_warnings(record=True) as w:
        ...     warnings.simplefilter("always")
        ...     foo(4)
        ...     assert len(w) == 1
        ...     assert issubclass(w[-1].category, PendingDeprecationWarning)
        ...     assert message == str(w[-1].message)
        8
    """
    def decorator(func):
        wraps(func)
        def wrapper(*args, **kwargs):
            warnings.warn(message, exception, stacklevel=2)
            return func(*args, **kwargs)
        return wrapper
    return decorator


if __name__ == "__main__":
    import doctest
    doctest.testmod(optionflags=doctest.ELLIPSIS)