Repository: youknowone/ring

Issue:
In-memory LRU-cache support

Discussion:
codecov[bot]: # [Codecov](https://codecov.io/gh/youknowone/ring/pull/102?src=pr&el=h1) Report
> Merging [#102](https://codecov.io/gh/youknowone/ring/pull/102?src=pr&el=desc) into [master](https://codecov.io/gh/youknowone/ring/commit/7e00f10f73c53b5355c8993ad9921426e84f09f4?src=pr&el=desc) will **decrease** coverage by `6.1%`.
> The diff coverage is `96.69%`.


```diff
@@            Coverage Diff            @@
##           master     #102     +/-   ##
=========================================
- Coverage   97.69%   91.59%   -6.1%     
=========================================
  Files          13       13             
  Lines        1212     1046    -166     
=========================================
- Hits         1184      958    -226     
- Misses         28       88     +60
```





Patch:
diff --git a/docs/coder.rst b/docs/coder.rst
index 19eb05d..6fa4fe5 100644
--- a/docs/coder.rst
+++ b/docs/coder.rst
@@ -86,7 +86,7 @@ Now `FloatCoder` is registered as `float`. Use it in a familiar way.
 
 .. code-block:: python
 
-    @ring.dict({}, coder='float')
+    @ring.lru(coder='float')
     def f():
         return 3.1415
 
@@ -108,7 +108,7 @@ of a ring object.
 
 .. code-block:: python
 
-    @ring.dict({})
+    @ring.lru()
     def f():
         return 3.1415
 
diff --git a/docs/control.rst b/docs/control.rst
index 8ff1b4a..76a7a1e 100644
--- a/docs/control.rst
+++ b/docs/control.rst
@@ -21,7 +21,7 @@ Meta controller
 
     .. code-block:: python
 
-        @ring.dict({})
+        @ring.lru()
         def f(a, b):
             ...
 
diff --git a/docs/factory.rst b/docs/factory.rst
index fc81601..4c053f4 100644
--- a/docs/factory.rst
+++ b/docs/factory.rst
@@ -14,8 +14,8 @@ Built-in factory functions and backends
 ---------------------------------------
 
 **Factory function** means the end user interface of **Ring**, which usually
-looks like ``@ring.dict``, ``@ring.memcache``, ``@ring.django``, etc. These
-factory functions create concrete ring decorators by arguments.
+looks like ``@ring.lru``, ``@ring.dict``, ``@ring.memcache``, ``@ring.django``,
+etc. These factory functions create concrete ring decorators by arguments.
 
 Technically the factory functions are not associated with each backend as
 bijection, but the built-in functions are mostly matching to the backends.
@@ -24,6 +24,7 @@ backend descriptions.
 
 **Ring** includes support for common cache storages:
 
+ - :func:`ring.lru`
  - :func:`ring.dict`
  - :func:`ring.memcache`
  - :func:`ring.redis`
@@ -33,6 +34,7 @@ backend descriptions.
 Which are shortcuts of concrete implementations and tools below:
 
 .. autosummary::
+    ring.func.sync.lru
     ring.func.sync.dict
     ring.func.sync.memcache
     ring.func.sync.redis_py
@@ -68,24 +70,24 @@ Ring decorators can be adapted to any kind of methods and descriptors.
             '''convert self value typed 'A' to ring key component'''
             return v
 
-        @ring.dict({})
+        @ring.lru()
         def method(self):
             '''method support'''
             ...
 
-        @ring.dict({})
+        @ring.lru()
         @classmethod
         def cmethod(self):
             '''classmethod support'''
             ...
 
-        @ring.dict({})
+        @ring.lru()
         @staticmethod
         def smethod(self):
             '''staticmethod support'''
             ...
 
-        @ring.dict({})
+        @ring.lru()
         @property
         def property(self):
             '''property support'''
diff --git a/docs/quickstart.rst b/docs/quickstart.rst
index e4c02ae..ddeb26a 100644
--- a/docs/quickstart.rst
+++ b/docs/quickstart.rst
@@ -30,10 +30,16 @@ included in ring installation due to the following issues:
 Check each backend you use and manually add related packages to `setup.py`
 or `requirements.txt`.
 
-If you are new to **Ring** and cache, let's start with :func:`ring.dict`.
-it doesn't require any dependency. Changing dict to another backend is simple
+If you are new to **Ring** and cache, let's start with :func:`ring.lru`.
+It doesn't require any dependency. Changing lru to another backend is simple
 for later.
 
+.. note::
+
+    If you are new to LRU cache, check
+    `<https://en.wikipedia.org/wiki/Cache_replacement_policies#Least_recently_used_(LRU)>`_
+    for details.
+
 
 First example
 -------------
@@ -45,17 +51,18 @@ Let's start with a simple example: function cache with bytes data.
     import ring
     import requests
 
-    # save in a dict, expire in 60 seconds.
-    @ring.dict({}, expire=60)
+    # save in a new lru storage
+    @ring.lru()
     def get_url(url):
         return requests.get(url).content
 
     # default access - it is cached
     data = get_url('http://example.com')
 
-This flow is what you see in common *smart* cache decorators.
-
+This flow is what you see in common *smart* cache decorators. Actually, this is
+very similar to :func:`functools.lru_cache` in Python standard library.
 
+The differences start here.
 The core feature of **Ring** is explicit controllers.
 
 .. code-block:: python
@@ -72,7 +79,8 @@ The core feature of **Ring** is explicit controllers.
     cached_data = get_url.decode(encoded_data)
 
 
-Ring will have full control for any layer of caching.
+Ring will have full control for any layer of caching. Which doesn't exist
+in :func:`functools.lru_cache`
 
 :see: :doc:`control` for sub-functions details.
 :see: :doc:`why` if this document doesn't explain what **Ring** does.
@@ -98,21 +106,21 @@ method, classmethod, staticmethod, property
         def __ring_key__(self):
             return 'page=' + self.url
 
-        @ring.dict({})
+        @ring.lru()
         def content(self):
             return requests.get(self.url).content
 
-        @ring.dict({})
+        @ring.lru()
         @classmethod
         def class_content(cls):
             return cls.base_content
 
-        @ring.dict({})
+        @ring.lru()
         @staticmethod
         def example_dot_com():
             return requests.get('http://example.com').content
 
-        @ring.dict({})
+        @ring.lru()
         @property
         def url_property(self):
             return self.url_property
@@ -139,7 +147,7 @@ method, classmethod, staticmethod, property
 Choosing backend
 ----------------
 
-Let's consider using actual cache storage instead of :class:`dict`.
+Let's consider using external cache storage instead of :class:`lru`.
 
 **Ring** includes common cache storage supports. `Memcached` is one of the
 popular cache storage. `Memcached` itself is out of the Python world. You must
@@ -171,7 +179,7 @@ Now you are ready to edit the ``get_url`` to use Memcached.
     client = pymemcache.client.Client(('127.0.0.1', 11211))  #2 create a client
 
     # save to memcache client, expire in 60 seconds.
-    @ring.memcache(client, expire=60)  #3 dict -> memcache
+    @ring.memcache(client, expire=60)  #3 lru -> memcache
     def get_url(url):
         return requests.get(url).content
 
@@ -179,7 +187,7 @@ Now you are ready to edit the ``get_url`` to use Memcached.
     data = get_url('http://example.com')
 
 
-Try and compare what's changed from :func:`ring.dict` version.
+Try and compare what's changed from :func:`ring.lru` version.
 
 There are many more included factories for various backends.
 
@@ -200,7 +208,7 @@ They follow similar convention but requiring `await` for IO jobs.
 
     import ring
 
-    @ring.dict({})
+    @ring.lru()
     async def f():
         ...
 
@@ -268,18 +276,23 @@ Low-level access
 
 Do you wonder how your data is encoded? Which keys are mapped to the
 functions? You don't need to be suffered by looking inside of **Ring**.
+At this time, let's use :func:`ring.dict` to look into the storage.
 
 .. code-block:: python
 
     import ring
 
-    @ring.dict({})
+    dict_storage = {}
+
+    @ring.dict(dict_storage)
     def f():
         ...
 
     key = f.key()  # retrieving the key
     raw_data = f.storage.backend.get(key)  # getting raw data from storage
 
+    # look into `dict_storage` by yourself to check how it works.
+
 
 :see: :doc:`control` for more attributes.
 
diff --git a/docs/ring.rst b/docs/ring.rst
index 8064238..3178747 100644
--- a/docs/ring.rst
+++ b/docs/ring.rst
@@ -14,6 +14,15 @@
 
 .. automodule:: ring
 
+    .. function:: lru(...)
+
+        Proxy to select synchronous or :mod:`asyncio` versions of **Ring**
+        factory.
+
+        :see: :func:`ring.func.sync.lru` for synchronous version.
+        :note: :mod:`asyncio` version is based on synchronous version. It is
+            composed using :func:`ring.func.asyncio.convert_storage`.
+
     .. function:: dict(...)
 
         Proxy to select synchronous or :mod:`asyncio` versions of **Ring**
diff --git a/docs/ring/func.rst b/docs/ring/func.rst
index e1a61b8..c8d7939 100644
--- a/docs/ring/func.rst
+++ b/docs/ring/func.rst
@@ -1,5 +1,9 @@
 .. automodule:: ring.func
 
+.. function:: lru(...)
+
+    :see: :func:`ring.lru`
+
 .. function:: dict(...)
 
     :see: :func:`ring.dict`
diff --git a/docs/ring/func_sync.rst b/docs/ring/func_sync.rst
index b685907..36c48f4 100644
--- a/docs/ring/func_sync.rst
+++ b/docs/ring/func_sync.rst
@@ -9,6 +9,9 @@
     :members:
     :undoc-members:
 
+.. autoclass:: ring.func.sync.LruStorage
+    :members:
+    :undoc-members:
 .. autoclass:: ring.func.sync.ExpirableDictStorage
     :members:
     :undoc-members:
@@ -27,3 +30,7 @@
 .. autoclass:: ring.func.sync.RedisStorage
     :members:
     :undoc-members:
+
+.. autoclass:: ring.func.lru_cache.LruCache
+    :members:
+    :undoc-members:
diff --git a/docs/why.rst b/docs/why.rst
index 9af74f1..f2b250c 100644
--- a/docs/why.rst
+++ b/docs/why.rst
@@ -201,7 +201,7 @@ cache`.
 
     import ring
 
-    @ring.dict({})
+    @ring.lru()
     def cached_function():
         ...
 
@@ -229,7 +229,7 @@ Function parameters are also supported in an expected manner:
 
 .. code-block:: python
 
-    @ring.dict({})
+    @ring.lru()
     def cached_function(a, b, c):
         ...
 
@@ -330,24 +330,24 @@ descriptors written in (weak) common convention also work.
             '''convert self value typed 'A' to ring key component'''
             return v
 
-        @ring.dict({})
+        @ring.lru()
         def method(self):
             '''method support'''
             ...
 
-        @ring.dict({})
+        @ring.lru()
         @classmethod
         def cmethod(self):
             '''classmethod support'''
             ...
 
-        @ring.dict({})
+        @ring.lru()
         @staticmethod
         def smethod(self):
             '''staticmethod support'''
             ...
 
-        @ring.dict({})
+        @ring.lru()
         @property
         def property(self):
             '''property support'''
diff --git a/ring/__init__.py b/ring/__init__.py
index 76fe570..e8de637 100644
--- a/ring/__init__.py
+++ b/ring/__init__.py
@@ -6,7 +6,7 @@ Common ring decorators are aliased in this level as shortcuts.
 import ring.coder  # noqa
 from ring.__version__ import __version__  # noqa
 from ring.func import (
-    dict, shelve, disk, memcache, redis, redis_hash)
+    lru, dict, shelve, disk, memcache, redis, redis_hash)
 try:
     import asyncio
     from ring.func.asyncio import aiomcache, aioredis, aioredis_hash
@@ -21,5 +21,5 @@ except ImportError:  # pragma: no cover
 
 
 __all__ = (
-    'dict', 'shelve', 'memcache', 'redis', 'redis_hash', 'disk',
+    'lru', 'dict', 'shelve', 'memcache', 'redis', 'redis_hash', 'disk',
     'aiomcache', 'aioredis', 'aioredis_hash')
diff --git a/ring/func/__init__.py b/ring/func/__init__.py
index 0df4928..da73c22 100644
--- a/ring/func/__init__.py
+++ b/ring/func/__init__.py
@@ -7,26 +7,29 @@ from __future__ import absolute_import
 from ring.func import sync
 
 try:
-    import asyncio as asyncio_mod
+    import asyncio as _has_asyncio
 except ImportError:
-    asyncio_mod = False
+    _has_asyncio = False
 else:
-    from . import asyncio
+    from ring.func import asyncio
 
 
 __all__ = (
-    'dict', 'memcache', 'redis', 'redis_hash', 'shelve', 'disk')
+    'lru', 'dict', 'memcache', 'redis', 'redis_hash', 'shelve', 'disk')
 
 
-if asyncio_mod:
+if _has_asyncio:
+    lru = asyncio.create_asyncio_factory_proxy(
+        (sync.lru, asyncio.create_factory_from(sync.lru, sync.LruStorage)),
+        support_asyncio=False)
     dict = asyncio.create_asyncio_factory_proxy(
         (sync.dict, asyncio.dict),
         support_asyncio=True)
     shelve = asyncio.create_asyncio_factory_proxy(
-        (sync.shelve, asyncio.create_factory_from(sync.ShelveStorage)),
+        (sync.shelve, asyncio.create_factory_from(sync.shelve, sync.ShelveStorage)),
         support_asyncio=False)
     disk = asyncio.create_asyncio_factory_proxy(
-        (sync.diskcache, asyncio.create_factory_from(sync.DiskCacheStorage)),
+        (sync.diskcache, asyncio.create_factory_from(sync.diskcache, sync.DiskCacheStorage)),
         support_asyncio=False)
     memcache = asyncio.create_asyncio_factory_proxy(
         (sync.memcache, asyncio.aiomcache),
@@ -38,4 +41,6 @@ if asyncio_mod:
         (sync.redis_py_hash, asyncio.aioredis_hash),
         support_asyncio=True)
 else:
-    from .sync import dict, shelve, diskcache as disk, memcache, redis_py as redis, redis_py_hash as redis_hash
+    from .sync import (
+        lru, dict, shelve, diskcache as disk, memcache,
+        redis_py as redis, redis_py_hash as redis_hash)
diff --git a/ring/func/asyncio.py b/ring/func/asyncio.py
index 4f45cd0..af2e8d0 100644
--- a/ring/func/asyncio.py
+++ b/ring/func/asyncio.py
@@ -84,24 +84,15 @@ def convert_storage(storage_class):
     return async_storage_class
 
 
-def create_factory_from(_storage_class):
+def create_factory_from(sync_factory, _storage_class):
     """Create :mod:`asyncio` compatible factory from synchronous storage."""
 
-    def factory(
-            obj, key_prefix=None, expire=None, coder=None, ignorable_keys=None,
-            user_interface=CacheUserInterface, storage_class=None,
-            **kwargs):
-
-        if storage_class is None:
-            storage_class = convert_storage(_storage_class)
-
-        return fbase.factory(
-            obj, key_prefix=key_prefix, on_manufactured=factory_doctor,
-            user_interface=user_interface,
-            storage_class=convert_storage(storage_class),
-            miss_value=None, expire_default=expire, coder=coder,
-            ignorable_keys=ignorable_keys,
-            **kwargs)
+    def factory(*args, **kwargs):
+        if 'user_interface' not in kwargs:
+            kwargs['user_interface'] = CacheUserInterface
+        if 'storage_class' not in kwargs:
+            kwargs['storage_class'] = convert_storage(_storage_class)
+        return sync_factory(*args, **kwargs)
 
     return factory
 
diff --git a/ring/func/lru_cache.py b/ring/func/lru_cache.py
new file mode 100644
index 0000000..47e3649
--- /dev/null
+++ b/ring/func/lru_cache.py
@@ -0,0 +1,136 @@
+# Forked from cpython/Lib/functools.py
+# This file follows PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2 as CPython
+# does.
+
+from threading import RLock
+
+try:
+    from functools import _CacheInfo
+except ImportError:
+    from collections import namedtuple
+    _CacheInfo = namedtuple(
+        "CacheInfo", ["hits", "misses", "maxsize", "currsize"])
+
+SENTINEL = object()  # unique object used to signal cache misses
+PREV, NEXT, KEY, RESULT = 0, 1, 2, 3  # names for the link fields
+FULL, HITS, MISSES = 0, 1, 2  # names for stat
+
+
+class LruCache(object):
+    """Created by breaking down functools.lru_cache from CPython 3.7.0."""
+
+    def __init__(self, maxsize):
+        cache = {}
+        cache_get = cache.get  # bound method to lookup a key or return None
+        cache_len = cache.__len__  # get cache size without calling len()
+        lock = RLock()     # because linkedlist updates aren't threadsafe
+        self.root = []   # root of the circular doubly linked list
+        # initialize by pointing to self
+        self.root[:] = [self.root, self.root, None, None]
+        stat = [False, 0, 0]
+
+        def get(key):
+            with lock:
+                link = cache_get(key)
+                if link is not None:
+                    root = self.root
+                    # Move the link to the front of the circular queue
+                    link_prev, link_next, _key, result = link
+                    link_prev[NEXT] = link_next
+                    link_next[PREV] = link_prev
+                    last = root[PREV]
+                    last[NEXT] = root[PREV] = link
+                    link[PREV] = last
+                    link[NEXT] = root
+                    stat[HITS] += 1
+                    return result
+                else:
+                    stat[MISSES] += 1
+            return SENTINEL
+
+        def delete(key):
+            with lock:
+                oldresult = self.root[RESULT]  # noqa
+                del cache[key]
+                stat[FULL] = False
+
+        def set(key, result):
+            with lock:
+                link = cache_get(key)
+                if link is not None:
+                    # Update link to store the new result
+                    link[-1] = result
+                elif stat[FULL]:
+                    # Use the old root to store the new key and result.
+                    oldroot = self.root
+                    oldroot[KEY] = key
+                    oldroot[RESULT] = result
+                    # Empty the oldest link and make it the new root.
+                    # Keep a reference to the old key and old result to
+                    # prevent their ref counts from going to zero during the
+                    # update. That will prevent potentially arbitrary object
+                    # clean-up code (i.e. __del__) from running while we're
+                    # still adjusting the links.
+                    root = self.root = oldroot[NEXT]
+                    oldkey = root[KEY]
+                    oldresult = root[RESULT]  # noqa
+                    root[KEY] = root[RESULT] = None
+                    # Now update the cache dictionary.
+                    del cache[oldkey]
+                    # Save the potentially reentrant cache[key] assignment
+                    # for last, after the root and links have been put in
+                    # a consistent state.
+                    cache[key] = oldroot
+                else:
+                    root = self.root
+                    # Put result in a new link at the front of the queue.
+                    last = root[PREV]
+                    link = [last, root, key, result]
+                    last[NEXT] = root[PREV] = cache[key] = link
+                    # Use the cache_len bound method instead of the len() function
+                    # which could potentially be wrapped in an lru_cache itself.
+                    stat[FULL] = (cache_len() >= maxsize)
+
+        def cache_info():
+            """Report cache statistics"""
+            with lock:
+                return _CacheInfo(stat[HITS], stat[MISSES], maxsize, cache_len())
+
+        def clear():
+            """Clear the cache and cache statistics"""
+            with lock:
+                cache.clear()
+                root = self.root
+                root[:] = [root, root, None, None]
+                stat[:] = False, 0, 0
+
+        self.get = get
+        self.delete = delete
+        self.set = set
+        self.cache_info = cache_info
+        self.clear = clear
+
+        # ring extension
+
+        def has(key):
+            with lock:
+                return key in cache
+
+        def touch(key):
+            with lock:
+                root = self.root
+                link = cache_get(key)
+                if link is None:
+                    raise KeyError
+                # Move the link to the front of the circular queue
+                link_prev, link_next, _key, result = link
+                link_prev[NEXT] = link_next
+                link_next[PREV] = link_prev
+                last = root[PREV]
+                last[NEXT] = root[PREV] = link
+                link[PREV] = last
+                link[NEXT] = root
+                return result
+
+        self.has = has
+        self.touch = touch
diff --git a/ring/func/sync.py b/ring/func/sync.py
index b63a9e6..eb97577 100644
--- a/ring/func/sync.py
+++ b/ring/func/sync.py
@@ -9,9 +9,11 @@ import time
 import re
 import hashlib
 
-from . import base as fbase
+from . import base as fbase, lru_cache as lru_mod
 
-__all__ = ('dict', 'memcache', 'redis_py', 'redis_py_hash', 'shelve', 'diskcache')
+__all__ = (
+    'lru', 'dict', 'memcache', 'redis_py', 'redis_py_hash', 'shelve',
+    'diskcache')
 
 
 class CacheUserInterface(fbase.BaseUserInterface):
@@ -175,6 +177,33 @@ class BulkStorageMixin(object):
         self.touch_many_values(keys, expire)
 
 
+class LruStorage(fbase.CommonMixinStorage, fbase.StorageMixin):
+
+    def get_value(self, key):
+        value = self.backend.get(key)
+        if value is lru_mod.SENTINEL:
+            raise fbase.NotFound
+        return value
+
+    def has_value(self, key):
+        return self.backend.has(key)
+
+    def set_value(self, key, value, expire):
+        self.backend.set(key, value)
+
+    def delete_value(self, key):
+        try:
+            self.backend.delete(key)
+        except KeyError:
+            pass
+
+    def touch_value(self, key, expire):
+        try:
+            self.backend.touch(key)
+        except KeyError:
+            pass
+
+
 class ExpirableDictStorage(fbase.CommonMixinStorage, fbase.StorageMixin):
 
     now = time.time
@@ -360,30 +389,70 @@ class DiskCacheStorage(fbase.CommonMixinStorage, fbase.StorageMixin):
         self.backend.delete(key)
 
 
+def lru(
+        lru=None, key_prefix=None, coder=None, ignorable_keys=None,
+        user_interface=CacheUserInterface, storage_class=LruStorage,
+        maxsize=128, **kwargs):
+    """LRU(Least-Recently-Used) cache interface.
+
+    Because the lru backend is following the basic manner of
+    :func:`functools.lru_cache`, see the document for LRU cache details.
+
+        >>> @ring.lru(maxsize=128)
+        >>> def f(...):
+        ...     ...
+
+    The above is very similar to below:
+
+        >>> @functools.lru_cache(maxsize=128, typed=False)
+        >>> def f(...):
+        ...     ...
+
+    Note that **ring** basically supports key consistency and manual
+    operation features, unlike :func:`functools.lru_cache`.
+
+    :param ring.func.lru_cache.LruCache lru: Cache storage. If the default
+        value :data:`None` is given, a new `LruCache`
+        object will be created. (Recommended)
+    :param int maxsize: The maximum size of the cache storage.
+
+    :see: :func:`functools.lru_cache` for LRU cache basics.
+    :see: :func:`ring.func.sync.CacheUserInterface` for sub-functions.
+    """
+    expire = None
+    if lru is None:
+        lru = lru_mod.LruCache(maxsize)
+        if key_prefix is None:
+            key_prefix = ''
+
+    return fbase.factory(
+        lru, key_prefix=key_prefix, on_manufactured=None,
+        user_interface=user_interface, storage_class=storage_class,
+        miss_value=None, expire_default=expire, coder=coder,
+        ignorable_keys=ignorable_keys,
+        **kwargs)
+
+
 def dict(
         obj, key_prefix=None, expire=None, coder=None, ignorable_keys=None,
         user_interface=CacheUserInterface, storage_class=None,
         **kwargs):
     """Basic Python :class:`dict` based cache.
 
-    This backend is not designed for real products, but useful by
-    keeping below in mind:
+    This backend is not designed for real products. Please carefully read the
+    next details to check it fits your demands.
 
-    - :func:`functools.lru_cache` is the standard library for the most of
-      local cache.
+    - :func:`ring.lru` and :func:`functools.lru_cache` are the standard way
+      for the most of local cache.
     - Expired objects will never be removed from the dict. If the function has
       unlimited input combinations, never use dict.
     - It is designed to "simulate" cache backends, not to provide an actual
       cache backend. If a caching function is a fast job, this backend even
       can drop the performance.
 
-    Still, it doesn't mean you can't use this backend for products. Take
-    advantage of it when your demands fit.
-
     :param dict obj: Cache storage. Any :class:`dict` compatible object.
 
     :see: :func:`ring.func.sync.CacheUserInterface` for sub-functions.
-
     :see: :func:`ring.dict` for :mod:`asyncio` version.
     """
     if storage_class is None:
@@ -432,7 +501,7 @@ def memcache(
         **kwargs):
     """Common Memcached_ interface.
 
-    This backend is common interface for various memcached client libraries
+    This backend is shared interface for various memcached client libraries
     below:
 
     - https://pypi.org/project/python-memcached/


Test patch:
diff --git a/tests/_test_func_asyncio.py b/tests/_test_func_asyncio.py
index 29e055c..65b1de9 100644
--- a/tests/_test_func_asyncio.py
+++ b/tests/_test_func_asyncio.py
@@ -6,6 +6,7 @@ import shelve
 import aiomcache
 import diskcache
 import ring
+from ring.func.lru_cache import LruCache
 
 import pytest
 from tests.test_func_sync import StorageDict
@@ -43,6 +44,11 @@ def storage_and_ring(request):
     return request.param
 
 
+@pytest.fixture()
+def storage_lru():
+    return LruCache(128), ring.lru
+
+
 @pytest.fixture()
 def storage_shelve():
     storage = shelve.open('/tmp/ring-test/shelvea')
@@ -56,6 +62,7 @@ def storage_disk(request):
 
 
 @pytest.fixture(params=[
+    pytest.lazy_fixture('storage_lru'),
     pytest.lazy_fixture('storage_shelve'),
     pytest.lazy_fixture('storage_disk'),
 ])
diff --git a/tests/test_func_sync.py b/tests/test_func_sync.py
index 44f6a71..caaac4d 100644
--- a/tests/test_func_sync.py
+++ b/tests/test_func_sync.py
@@ -6,6 +6,7 @@ import pymemcache.client
 import memcache
 import redis
 import diskcache
+from ring.func.lru_cache import LruCache
 
 import pytest
 
@@ -49,6 +50,17 @@ def storage_shelve():
     return storage
 
 
+@pytest.fixture
+def storage_lru():
+    storage = LruCache(128)
+    storage.ring = ring.lru
+    storage.is_binary = False
+    storage.has_has = True
+    storage.has_touch = True
+    storage.has_expire = False
+    return storage
+
+
 @pytest.fixture(scope='session', params=[
     diskcache.Cache('/tmp/ring-test/diskcache')
 ])
@@ -96,6 +108,7 @@ def redis_client(request):
 @pytest.fixture(params=[
     pytest.lazy_fixture('storage_dict'),
     pytest.lazy_fixture('storage_shelve'),
+    pytest.lazy_fixture('storage_lru'),
     pytest.lazy_fixture('memcache_client'),
     pytest.lazy_fixture('redis_client'),
     pytest.lazy_fixture('storage_diskcache'),
@@ -285,6 +298,20 @@ def test_func_dict_expire():
     assert f.get(1, 2) is None
 
 
+def test_lru(storage_lru):
+    @ring.lru(maxsize=2)
+    def f(a, b):
+        return a * 100 + b
+
+    assert 102 == f(1, 2)
+    assert 205 == f(2, 5)
+    assert 102 == f.get(1, 2)
+    assert 205 == f.get(2, 5)
+
+    assert 503 == f(5, 3)
+    assert None is f.get(1, 2)
+
+
 def test_diskcache(storage_diskcache):
     base = [0]
 
diff --git a/tests/test_lru_cache.py b/tests/test_lru_cache.py
new file mode 100644
index 0000000..54ba45a
--- /dev/null
+++ b/tests/test_lru_cache.py
@@ -0,0 +1,69 @@
+from functools import update_wrapper
+from ring.func.lru_cache import LruCache, SENTINEL
+
+try:
+    from functools import _make_key
+except ImportError:
+    def _make_key(args, kwds, typed):
+        return args, tuple(sorted(kwds.items()))
+
+
+def test_lru_cache_porting():
+    def lru_cache(maxsize=128, typed=False):
+        if maxsize is not None and not isinstance(maxsize, int):
+            raise TypeError('Expected maxsize to be an integer or None')
+
+        def decorating_function(user_function):
+            cache = LruCache(maxsize)
+
+            def wrapper(*args, **kwds):
+                # Size limited caching that tracks accesses by recency
+                key = _make_key(args, kwds, typed)
+                result = cache.get(key)
+                if result is not SENTINEL:
+                    return result
+                result = user_function(*args, **kwds)
+                cache.set(key, result)
+                return result
+
+            return update_wrapper(wrapper, user_function)
+
+        return decorating_function
+
+    global call_count
+    call_count = 0
+
+    @lru_cache(maxsize=128)
+    def fibonacci(n):
+        global call_count
+        call_count += 1
+        if n <= 1:
+            return n
+        return fibonacci(n - 1) + fibonacci(n - 2)
+
+    assert fibonacci(10) == 55
+    assert call_count == 11
+
+
+def test_lru_object():
+    lru = LruCache(3)
+
+    lru.set('a', 10)
+    lru.set('b', 20)
+    lru.set('c', 30)
+    assert 10 == lru.get('a')
+    lru.set('d', 40)
+    assert SENTINEL is lru.get('b')
+    assert 30 == lru.get('c')
+
+    lru.delete('c')
+    assert 10 == lru.get('a')
+    assert SENTINEL is lru.get('b')
+    assert SENTINEL is lru.get('c')
+    assert 40 == lru.get('d')
+
+    lru.clear()
+    for c in 'abcd':
+        assert SENTINEL is lru.get(c)
+
+    lru.cache_info()