Release platform/external/python/cachetools v4.2.4 am: 39c4b449e8

Original change: https://android-review.googlesource.com/c/platform/external/python/cachetools/+/1908894

Change-Id: Ib1e33fb4ccc92032d82483bbb0491c6e0aecc36d
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..1139f90
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,22 @@
+name: CI
+
+on: [push, pull_request, workflow_dispatch]
+
+jobs:
+  main:
+    name: Python ${{ matrix.python }}
+    runs-on: ubuntu-20.04
+    strategy:
+      fail-fast: false
+      matrix:
+        python: ["3.6", "3.7", "3.8", "3.9", "3.10", "pypy-3.6", "pypy-3.7"]
+    steps:
+      - uses: actions/checkout@v2
+      - uses: actions/setup-python@v2
+        with:
+          python-version: ${{ matrix.python }}
+      - run: python -m pip install coverage tox
+      - run: python -m tox
+      - uses: codecov/codecov-action@v1
+        with:
+          name: ${{ matrix.python }}
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..235e4aa
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,11 @@
+*.egg-info
+*.pyc
+*.swp
+.cache/
+.coverage
+.tox/
+MANIFEST
+build/
+dist/
+docs/_build/
+.pytest_cache/
diff --git a/Android.bp b/Android.bp
new file mode 100644
index 0000000..3f92aff
--- /dev/null
+++ b/Android.bp
@@ -0,0 +1,29 @@
+// Copyright 2021 Google Inc. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+python_library {
+    name: "py-cachetools",
+    host_supported: true,
+    srcs: [
+        "src/cachetools/*.py",
+    ],
+    version: {
+        py2: {
+            enabled: true,
+        },
+        py3: {
+            enabled: true,
+        },
+    },
+}
\ No newline at end of file
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
new file mode 100644
index 0000000..0c24a03
--- /dev/null
+++ b/CHANGELOG.rst
@@ -0,0 +1,369 @@
+v4.2.4 (2021-09-30)
+===================
+
+- Add submodule shims for backward compatibility.
+
+
+v4.2.3 (2021-09-29)
+===================
+
+- Add documentation and tests for using ``TTLCache`` with
+  ``datetime``.
+
+- Link to typeshed typing stubs.
+
+- Flatten package file hierarchy.
+
+
+v4.2.2 (2021-04-27)
+===================
+
+- Update build environment.
+
+- Remove Python 2 remnants.
+
+- Format code with Black.
+
+
+v4.2.1 (2021-01-24)
+===================
+
+- Handle ``__missing__()`` not storing cache items.
+
+- Clean up ``__missing__()`` example.
+
+
+v4.2.0 (2020-12-10)
+===================
+
+- Add FIFO cache implementation.
+
+- Add MRU cache implementation.
+
+- Improve behavior of decorators in case of race conditions.
+
+- Improve documentation regarding mutability of caches values and use
+  of key functions with decorators.
+
+- Officially support Python 3.9.
+
+
+v4.1.1 (2020-06-28)
+===================
+
+- Improve ``popitem()`` exception context handling.
+
+- Replace ``float('inf')`` with ``math.inf``.
+
+- Improve "envkey" documentation example.
+
+
+v4.1.0 (2020-04-08)
+===================
+
+- Support ``user_function`` with ``cachetools.func`` decorators
+  (Python 3.8 compatibility).
+
+- Support ``cache_parameters()`` with ``cachetools.func`` decorators
+  (Python 3.9 compatibility).
+
+
+v4.0.0 (2019-12-15)
+===================
+
+- Require Python 3.5 or later.
+
+
+v3.1.1 (2019-05-23)
+===================
+
+- Document how to use shared caches with ``@cachedmethod``.
+
+- Fix pickling/unpickling of cache keys
+
+
+v3.1.0 (2019-01-29)
+===================
+
+- Fix Python 3.8 compatibility issue.
+
+- Use ``time.monotonic`` as default timer if available.
+
+- Improve documentation regarding thread safety.
+
+
+v3.0.0 (2018-11-04)
+===================
+
+- Officially support Python 3.7.
+
+- Drop Python 3.3 support (breaking change).
+
+- Remove ``missing`` cache constructor parameter (breaking change).
+
+- Remove ``self`` from ``@cachedmethod`` key arguments (breaking
+  change).
+
+- Add support for ``maxsize=None`` in ``cachetools.func`` decorators.
+
+
+v2.1.0 (2018-05-12)
+===================
+
+- Deprecate ``missing`` cache constructor parameter.
+
+- Handle overridden ``getsizeof()`` method in subclasses.
+
+- Fix Python 2.7 ``RRCache`` pickling issues.
+
+- Various documentation improvements.
+
+
+v2.0.1 (2017-08-11)
+===================
+
+- Officially support Python 3.6.
+
+- Move documentation to RTD.
+
+- Documentation: Update import paths for key functions (courtesy of
+  slavkoja).
+
+
+v2.0.0 (2016-10-03)
+===================
+
+- Drop Python 3.2 support (breaking change).
+
+- Drop support for deprecated features (breaking change).
+
+- Move key functions to separate package (breaking change).
+
+- Accept non-integer ``maxsize`` in ``Cache.__repr__()``.
+
+
+v1.1.6 (2016-04-01)
+===================
+
+- Reimplement ``LRUCache`` and ``TTLCache`` using
+  ``collections.OrderedDict``.  Note that this will break pickle
+  compatibility with previous versions.
+
+- Fix ``TTLCache`` not calling ``__missing__()`` of derived classes.
+
+- Handle ``ValueError`` in ``Cache.__missing__()`` for consistency
+  with caching decorators.
+
+- Improve how ``TTLCache`` handles expired items.
+
+- Use ``Counter.most_common()`` for ``LFUCache.popitem()``.
+
+
+v1.1.5 (2015-10-25)
+===================
+
+- Refactor ``Cache`` base class.  Note that this will break pickle
+  compatibility with previous versions.
+
+- Clean up ``LRUCache`` and ``TTLCache`` implementations.
+
+
+v1.1.4 (2015-10-24)
+===================
+
+- Refactor ``LRUCache`` and ``TTLCache`` implementations.  Note that
+  this will break pickle compatibility with previous versions.
+
+- Document pending removal of deprecated features.
+
+- Minor documentation improvements.
+
+
+v1.1.3 (2015-09-15)
+===================
+
+- Fix pickle tests.
+
+
+v1.1.2 (2015-09-15)
+===================
+
+- Fix pickling of large ``LRUCache`` and ``TTLCache`` instances.
+
+
+v1.1.1 (2015-09-07)
+===================
+
+- Improve key functions.
+
+- Improve documentation.
+
+- Improve unit test coverage.
+
+
+v1.1.0 (2015-08-28)
+===================
+
+- Add ``@cached`` function decorator.
+
+- Add ``hashkey`` and ``typedkey`` fuctions.
+
+- Add `key` and `lock` arguments to ``@cachedmethod``.
+
+- Set ``__wrapped__`` attributes for Python versions < 3.2.
+
+- Move ``functools`` compatible decorators to ``cachetools.func``.
+
+- Deprecate ``@cachedmethod`` `typed` argument.
+
+- Deprecate `cache` attribute for ``@cachedmethod`` wrappers.
+
+- Deprecate `getsizeof` and `lock` arguments for `cachetools.func`
+  decorator.
+
+
+v1.0.3 (2015-06-26)
+===================
+
+- Clear cache statistics when calling ``clear_cache()``.
+
+
+v1.0.2 (2015-06-18)
+===================
+
+- Allow simple cache instances to be pickled.
+
+- Refactor ``Cache.getsizeof`` and ``Cache.missing`` default
+  implementation.
+
+
+v1.0.1 (2015-06-06)
+===================
+
+- Code cleanup for improved PEP 8 conformance.
+
+- Add documentation and unit tests for using ``@cachedmethod`` with
+  generic mutable mappings.
+
+- Improve documentation.
+
+
+v1.0.0 (2014-12-19)
+===================
+
+- Provide ``RRCache.choice`` property.
+
+- Improve documentation.
+
+
+v0.8.2 (2014-12-15)
+===================
+
+- Use a ``NestedTimer`` for ``TTLCache``.
+
+
+v0.8.1 (2014-12-07)
+===================
+
+- Deprecate ``Cache.getsize()``.
+
+
+v0.8.0 (2014-12-03)
+===================
+
+- Ignore ``ValueError`` raised on cache insertion in decorators.
+
+- Add ``Cache.getsize()``.
+
+- Add ``Cache.__missing__()``.
+
+- Feature freeze for `v1.0`.
+
+
+v0.7.1 (2014-11-22)
+===================
+
+- Fix `MANIFEST.in`.
+
+
+v0.7.0 (2014-11-12)
+===================
+
+- Deprecate ``TTLCache.ExpiredError``.
+
+- Add `choice` argument to ``RRCache`` constructor.
+
+- Refactor ``LFUCache``, ``LRUCache`` and ``TTLCache``.
+
+- Use custom ``NullContext`` implementation for unsynchronized
+  function decorators.
+
+
+v0.6.0 (2014-10-13)
+===================
+
+- Raise ``TTLCache.ExpiredError`` for expired ``TTLCache`` items.
+
+- Support unsynchronized function decorators.
+
+- Allow ``@cachedmethod.cache()`` to return None
+
+
+v0.5.1 (2014-09-25)
+===================
+
+- No formatting of ``KeyError`` arguments.
+
+- Update ``README.rst``.
+
+
+v0.5.0 (2014-09-23)
+===================
+
+- Do not delete expired items in TTLCache.__getitem__().
+
+- Add ``@ttl_cache`` function decorator.
+
+- Fix public ``getsizeof()`` usage.
+
+
+v0.4.0 (2014-06-16)
+===================
+
+- Add ``TTLCache``.
+
+- Add ``Cache`` base class.
+
+- Remove ``@cachedmethod`` `lock` parameter.
+
+
+v0.3.1 (2014-05-07)
+===================
+
+- Add proper locking for ``cache_clear()`` and ``cache_info()``.
+
+- Report `size` in ``cache_info()``.
+
+
+v0.3.0 (2014-05-06)
+===================
+
+- Remove ``@cache`` decorator.
+
+- Add ``size``, ``getsizeof`` members.
+
+- Add ``@cachedmethod`` decorator.
+
+
+v0.2.0 (2014-04-02)
+===================
+
+- Add ``@cache`` decorator.
+
+- Update documentation.
+
+
+v0.1.0 (2014-03-27)
+===================
+
+- Initial release.
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..fc2146e
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,20 @@
+The MIT License (MIT)
+
+Copyright (c) 2014-2021 Thomas Kemmer
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/MANIFEST.in b/MANIFEST.in
new file mode 100644
index 0000000..fd9d6a9
--- /dev/null
+++ b/MANIFEST.in
@@ -0,0 +1,10 @@
+include CHANGELOG.rst
+include LICENSE
+include MANIFEST.in
+include README.rst
+include tox.ini
+
+recursive-include docs *
+prune docs/_build
+
+recursive-include tests *.py
diff --git a/NOTICE b/NOTICE
new file mode 120000
index 0000000..7a694c9
--- /dev/null
+++ b/NOTICE
@@ -0,0 +1 @@
+LICENSE
\ No newline at end of file
diff --git a/OWNERS b/OWNERS
index 4d9907d..ea7403e 100644
--- a/OWNERS
+++ b/OWNERS
@@ -1 +1,2 @@
-yim@google.com
+chojoyce@google.com
+herbertxue@google.com
diff --git a/README.rst b/README.rst
new file mode 100644
index 0000000..869d3cb
--- /dev/null
+++ b/README.rst
@@ -0,0 +1,109 @@
+cachetools
+========================================================================
+
+.. image:: https://img.shields.io/pypi/v/cachetools
+   :target: https://pypi.org/project/cachetools/
+   :alt: Latest PyPI version
+
+.. image:: https://img.shields.io/readthedocs/cachetools
+   :target: https://cachetools.readthedocs.io/
+   :alt: Documentation build status
+
+.. image:: https://img.shields.io/github/workflow/status/tkem/cachetools/CI
+   :target: https://github.com/tkem/cachetools/actions/workflows/ci.yml
+   :alt: CI build status
+
+.. image:: https://img.shields.io/codecov/c/github/tkem/cachetools/master.svg
+   :target: https://codecov.io/gh/tkem/cachetools
+   :alt: Test coverage
+
+.. image:: https://img.shields.io/github/license/tkem/cachetools
+   :target: https://raw.github.com/tkem/cachetools/master/LICENSE
+   :alt: License
+
+.. image:: https://img.shields.io/badge/code%20style-black-000000.svg
+   :target: https://github.com/psf/black
+   :alt: Code style: black
+
+This module provides various memoizing collections and decorators,
+including variants of the Python Standard Library's `@lru_cache`_
+function decorator.
+
+.. code-block:: python
+
+   from cachetools import cached, LRUCache, TTLCache
+
+   # speed up calculating Fibonacci numbers with dynamic programming
+   @cached(cache={})
+   def fib(n):
+       return n if n < 2 else fib(n - 1) + fib(n - 2)
+
+   # cache least recently used Python Enhancement Proposals
+   @cached(cache=LRUCache(maxsize=32))
+   def get_pep(num):
+       url = 'http://www.python.org/dev/peps/pep-%04d/' % num
+       with urllib.request.urlopen(url) as s:
+           return s.read()
+
+   # cache weather data for no longer than ten minutes
+   @cached(cache=TTLCache(maxsize=1024, ttl=600))
+   def get_weather(place):
+       return owm.weather_at_place(place).get_weather()
+
+For the purpose of this module, a *cache* is a mutable_ mapping_ of a
+fixed maximum size.  When the cache is full, i.e. by adding another
+item the cache would exceed its maximum size, the cache must choose
+which item(s) to discard based on a suitable `cache algorithm`_.  In
+general, a cache's size is the total size of its items, and an item's
+size is a property or function of its value, e.g. the result of
+``sys.getsizeof(value)``.  For the trivial but common case that each
+item counts as ``1``, a cache's size is equal to the number of its
+items, or ``len(cache)``.
+
+Multiple cache classes based on different caching algorithms are
+implemented, and decorators for easily memoizing function and method
+calls are provided, too.
+
+
+Installation
+------------------------------------------------------------------------
+
+cachetools is available from PyPI_ and can be installed by running::
+
+  pip install cachetools
+
+Typing stubs for this package are provided by typeshed_ and can be
+installed by running::
+
+  pip install types-cachetools
+
+
+Project Resources
+------------------------------------------------------------------------
+
+- `Documentation`_
+- `Issue tracker`_
+- `Source code`_
+- `Change log`_
+
+
+License
+------------------------------------------------------------------------
+
+Copyright (c) 2014-2021 Thomas Kemmer.
+
+Licensed under the `MIT License`_.
+
+
+.. _@lru_cache: https://docs.python.org/3/library/functools.html#functools.lru_cache
+.. _mutable: https://docs.python.org/dev/glossary.html#term-mutable
+.. _mapping: https://docs.python.org/dev/glossary.html#term-mapping
+.. _cache algorithm: https://en.wikipedia.org/wiki/Cache_algorithms
+
+.. _PyPI: https://pypi.org/project/cachetools/
+.. _typeshed: https://github.com/python/typeshed/
+.. _Documentation: https://cachetools.readthedocs.io/
+.. _Issue tracker: https://github.com/tkem/cachetools/issues/
+.. _Source code: https://github.com/tkem/cachetools/
+.. _Change log: https://github.com/tkem/cachetools/blob/master/CHANGELOG.rst
+.. _MIT License: https://raw.github.com/tkem/cachetools/master/LICENSE
diff --git a/docs/.gitignore b/docs/.gitignore
new file mode 100644
index 0000000..e35d885
--- /dev/null
+++ b/docs/.gitignore
@@ -0,0 +1 @@
+_build
diff --git a/docs/Makefile b/docs/Makefile
new file mode 100644
index 0000000..c88ce22
--- /dev/null
+++ b/docs/Makefile
@@ -0,0 +1,153 @@
+# Makefile for Sphinx documentation
+#
+
+# You can set these variables from the command line.
+SPHINXOPTS    =
+SPHINXBUILD   = sphinx-build
+PAPER         =
+BUILDDIR      = _build
+
+# Internal variables.
+PAPEROPT_a4     = -D latex_paper_size=a4
+PAPEROPT_letter = -D latex_paper_size=letter
+ALLSPHINXOPTS   = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
+# the i18n builder cannot share the environment and doctrees with the others
+I18NSPHINXOPTS  = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
+
+.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext
+
+help:
+	@echo "Please use \`make <target>' where <target> is one of"
+	@echo "  html       to make standalone HTML files"
+	@echo "  dirhtml    to make HTML files named index.html in directories"
+	@echo "  singlehtml to make a single large HTML file"
+	@echo "  pickle     to make pickle files"
+	@echo "  json       to make JSON files"
+	@echo "  htmlhelp   to make HTML files and a HTML help project"
+	@echo "  qthelp     to make HTML files and a qthelp project"
+	@echo "  devhelp    to make HTML files and a Devhelp project"
+	@echo "  epub       to make an epub"
+	@echo "  latex      to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
+	@echo "  latexpdf   to make LaTeX files and run them through pdflatex"
+	@echo "  text       to make text files"
+	@echo "  man        to make manual pages"
+	@echo "  texinfo    to make Texinfo files"
+	@echo "  info       to make Texinfo files and run them through makeinfo"
+	@echo "  gettext    to make PO message catalogs"
+	@echo "  changes    to make an overview of all changed/added/deprecated items"
+	@echo "  linkcheck  to check all external links for integrity"
+	@echo "  doctest    to run all doctests embedded in the documentation (if enabled)"
+
+clean:
+	-rm -rf $(BUILDDIR)/*
+
+html:
+	$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
+	@echo
+	@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
+
+dirhtml:
+	$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
+	@echo
+	@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
+
+singlehtml:
+	$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
+	@echo
+	@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
+
+pickle:
+	$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
+	@echo
+	@echo "Build finished; now you can process the pickle files."
+
+json:
+	$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
+	@echo
+	@echo "Build finished; now you can process the JSON files."
+
+htmlhelp:
+	$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
+	@echo
+	@echo "Build finished; now you can run HTML Help Workshop with the" \
+	      ".hhp project file in $(BUILDDIR)/htmlhelp."
+
+qthelp:
+	$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
+	@echo
+	@echo "Build finished; now you can run "qcollectiongenerator" with the" \
+	      ".qhcp project file in $(BUILDDIR)/qthelp, like this:"
+	@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/cachetools.qhcp"
+	@echo "To view the help file:"
+	@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/cachetools.qhc"
+
+devhelp:
+	$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
+	@echo
+	@echo "Build finished."
+	@echo "To view the help file:"
+	@echo "# mkdir -p $$HOME/.local/share/devhelp/cachetools"
+	@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/cachetools"
+	@echo "# devhelp"
+
+epub:
+	$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
+	@echo
+	@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
+
+latex:
+	$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+	@echo
+	@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
+	@echo "Run \`make' in that directory to run these through (pdf)latex" \
+	      "(use \`make latexpdf' here to do that automatically)."
+
+latexpdf:
+	$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+	@echo "Running LaTeX files through pdflatex..."
+	$(MAKE) -C $(BUILDDIR)/latex all-pdf
+	@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
+
+text:
+	$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
+	@echo
+	@echo "Build finished. The text files are in $(BUILDDIR)/text."
+
+man:
+	$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
+	@echo
+	@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
+
+texinfo:
+	$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
+	@echo
+	@echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
+	@echo "Run \`make' in that directory to run these through makeinfo" \
+	      "(use \`make info' here to do that automatically)."
+
+info:
+	$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
+	@echo "Running Texinfo files through makeinfo..."
+	make -C $(BUILDDIR)/texinfo info
+	@echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
+
+gettext:
+	$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
+	@echo
+	@echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
+
+changes:
+	$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
+	@echo
+	@echo "The overview file is in $(BUILDDIR)/changes."
+
+linkcheck:
+	$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
+	@echo
+	@echo "Link check complete; look for any errors in the above output " \
+	      "or in $(BUILDDIR)/linkcheck/output.txt."
+
+doctest:
+	$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
+	@echo "Testing of doctests in the sources finished, look at the " \
+	      "results in $(BUILDDIR)/doctest/output.txt."
diff --git a/docs/conf.py b/docs/conf.py
new file mode 100644
index 0000000..f55e5a5
--- /dev/null
+++ b/docs/conf.py
@@ -0,0 +1,24 @@
+def get_version():
+    import configparser
+    import pathlib
+
+    cp = configparser.ConfigParser()
+    # Python 3.5 ConfigParser does not accept Path as filename
+    cp.read(str(pathlib.Path(__file__).parent.parent / "setup.cfg"))
+    return cp["metadata"]["version"]
+
+
+project = "cachetools"
+copyright = "2014-2021 Thomas Kemmer"
+version = get_version()
+release = version
+
+extensions = [
+    "sphinx.ext.autodoc",
+    "sphinx.ext.coverage",
+    "sphinx.ext.doctest",
+    "sphinx.ext.todo",
+]
+exclude_patterns = ["_build"]
+master_doc = "index"
+html_theme = "default"
diff --git a/docs/index.rst b/docs/index.rst
new file mode 100644
index 0000000..c9828b3
--- /dev/null
+++ b/docs/index.rst
@@ -0,0 +1,533 @@
+*********************************************************************
+:mod:`cachetools` --- Extensible memoizing collections and decorators
+*********************************************************************
+
+.. module:: cachetools
+
+This module provides various memoizing collections and decorators,
+including variants of the Python Standard Library's `@lru_cache`_
+function decorator.
+
+For the purpose of this module, a *cache* is a mutable_ mapping_ of a
+fixed maximum size.  When the cache is full, i.e. by adding another
+item the cache would exceed its maximum size, the cache must choose
+which item(s) to discard based on a suitable `cache algorithm`_.  In
+general, a cache's size is the total size of its items, and an item's
+size is a property or function of its value, e.g. the result of
+``sys.getsizeof(value)``.  For the trivial but common case that each
+item counts as :const:`1`, a cache's size is equal to the number of
+its items, or ``len(cache)``.
+
+Multiple cache classes based on different caching algorithms are
+implemented, and decorators for easily memoizing function and method
+calls are provided, too.
+
+
+.. testsetup:: *
+
+   import operator
+   from cachetools import cached, cachedmethod, LRUCache, TTLCache
+
+   from unittest import mock
+   urllib = mock.MagicMock()
+
+
+Cache implementations
+=====================
+
+This module provides several classes implementing caches using
+different cache algorithms.  All these classes derive from class
+:class:`Cache`, which in turn derives from
+:class:`collections.MutableMapping`, and provide :attr:`maxsize` and
+:attr:`currsize` properties to retrieve the maximum and current size
+of the cache.  When a cache is full, :meth:`Cache.__setitem__()` calls
+:meth:`self.popitem()` repeatedly until there is enough room for the
+item to be added.
+
+:class:`Cache` also features a :meth:`getsizeof` method, which returns
+the size of a given `value`.  The default implementation of
+:meth:`getsizeof` returns :const:`1` irrespective of its argument,
+making the cache's size equal to the number of its items, or
+``len(cache)``.  For convenience, all cache classes accept an optional
+named constructor parameter `getsizeof`, which may specify a function
+of one argument used to retrieve the size of an item's value.
+
+Note that the values of a :class:`Cache` are mutable by default, as
+are e.g. the values of a :class:`dict`.  It is the user's
+responsibility to take care that cached values are not accidentally
+modified.  This is especially important when using a custom
+`getsizeof` function, since the size of an item's value will only be
+computed when the item is inserted into the cache.
+
+.. note::
+
+   Please be aware that all these classes are *not* thread-safe.
+   Access to a shared cache from multiple threads must be properly
+   synchronized, e.g. by using one of the memoizing decorators with a
+   suitable `lock` object.
+
+.. autoclass:: Cache(maxsize, getsizeof=None)
+   :members: currsize, getsizeof, maxsize
+
+   This class discards arbitrary items using :meth:`popitem` to make
+   space when necessary.  Derived classes may override :meth:`popitem`
+   to implement specific caching strategies.  If a subclass has to
+   keep track of item access, insertion or deletion, it may
+   additionally need to override :meth:`__getitem__`,
+   :meth:`__setitem__` and :meth:`__delitem__`.
+
+.. autoclass:: FIFOCache(maxsize, getsizeof=None)
+   :members: popitem
+
+   This class evicts items in the order they were added to make space
+   when necessary.
+
+.. autoclass:: LFUCache(maxsize, getsizeof=None)
+   :members: popitem
+
+   This class counts how often an item is retrieved, and discards the
+   items used least often to make space when necessary.
+
+.. autoclass:: LRUCache(maxsize, getsizeof=None)
+   :members: popitem
+
+   This class discards the least recently used items first to make
+   space when necessary.
+
+.. autoclass:: MRUCache(maxsize, getsizeof=None)
+   :members: popitem
+
+   This class discards the most recently used items first to make
+   space when necessary.
+
+.. autoclass:: RRCache(maxsize, choice=random.choice, getsizeof=None)
+   :members: choice, popitem
+
+   This class randomly selects candidate items and discards them to
+   make space when necessary.
+
+   By default, items are selected from the list of cache keys using
+   :func:`random.choice`.  The optional argument `choice` may specify
+   an alternative function that returns an arbitrary element from a
+   non-empty sequence.
+
+.. autoclass:: TTLCache(maxsize, ttl, timer=time.monotonic, getsizeof=None)
+   :members: popitem, timer, ttl
+
+   This class associates a time-to-live value with each item.  Items
+   that expire because they have exceeded their time-to-live will be
+   no longer accessible, and will be removed eventually.  If no
+   expired items are there to remove, the least recently used items
+   will be discarded first to make space when necessary.
+
+   By default, the time-to-live is specified in seconds and
+   :func:`time.monotonic` is used to retrieve the current time.  A
+   custom `timer` function can also be supplied:
+
+   .. testcode::
+
+      from datetime import datetime, timedelta
+
+      cache = TTLCache(maxsize=10, ttl=timedelta(hours=12), timer=datetime.now)
+
+   The expression `timer() + ttl` at the time of insertion defines the
+   expiration time of a cache item, and must be comparable against
+   later results of `timer()`.
+
+   .. method:: expire(self, time=None)
+
+      Expired items will be removed from a cache only at the next
+      mutating operation, e.g. :meth:`__setitem__` or
+      :meth:`__delitem__`, and therefore may still claim memory.
+      Calling this method removes all items whose time-to-live would
+      have expired by `time`, so garbage collection is free to reuse
+      their memory.  If `time` is :const:`None`, this removes all
+      items that have expired by the current value returned by
+      :attr:`timer`.
+
+
+Extending cache classes
+-----------------------
+
+Sometimes it may be desirable to notice when and what cache items are
+evicted, i.e. removed from a cache to make room for new items.  Since
+all cache implementations call :meth:`popitem` to evict items from the
+cache, this can be achieved by overriding this method in a subclass:
+
+.. doctest::
+   :pyversion: >= 3
+
+   >>> class MyCache(LRUCache):
+   ...     def popitem(self):
+   ...         key, value = super().popitem()
+   ...         print('Key "%s" evicted with value "%s"' % (key, value))
+   ...         return key, value
+
+   >>> c = MyCache(maxsize=2)
+   >>> c['a'] = 1
+   >>> c['b'] = 2
+   >>> c['c'] = 3
+   Key "a" evicted with value "1"
+
+Similar to the standard library's :class:`collections.defaultdict`,
+subclasses of :class:`Cache` may implement a :meth:`__missing__`
+method which is called by :meth:`Cache.__getitem__` if the requested
+key is not found:
+
+.. doctest::
+   :pyversion: >= 3
+
+   >>> class PepStore(LRUCache):
+   ...     def __missing__(self, key):
+   ...         """Retrieve text of a Python Enhancement Proposal"""
+   ...         url = 'http://www.python.org/dev/peps/pep-%04d/' % key
+   ...         with urllib.request.urlopen(url) as s:
+   ...             pep = s.read()
+   ...             self[key] = pep  # store text in cache
+   ...             return pep
+
+   >>> peps = PepStore(maxsize=4)
+   >>> for n in 8, 9, 290, 308, 320, 8, 218, 320, 279, 289, 320:
+   ...     pep = peps[n]
+   >>> print(sorted(peps.keys()))
+   [218, 279, 289, 320]
+
+Note, though, that such a class does not really behave like a *cache*
+any more, and will lead to surprising results when used with any of
+the memoizing decorators described below.  However, it may be useful
+in its own right.
+
+
+Memoizing decorators
+====================
+
+The :mod:`cachetools` module provides decorators for memoizing
+function and method calls.  This can save time when a function is
+often called with the same arguments:
+
+.. doctest::
+
+   >>> @cached(cache={})
+   ... def fib(n):
+   ...     'Compute the nth number in the Fibonacci sequence'
+   ...     return n if n < 2 else fib(n - 1) + fib(n - 2)
+
+   >>> fib(42)
+   267914296
+
+.. decorator:: cached(cache, key=cachetools.keys.hashkey, lock=None)
+
+   Decorator to wrap a function with a memoizing callable that saves
+   results in a cache.
+
+   The `cache` argument specifies a cache object to store previous
+   function arguments and return values.  Note that `cache` need not
+   be an instance of the cache implementations provided by the
+   :mod:`cachetools` module.  :func:`cached` will work with any
+   mutable mapping type, including plain :class:`dict` and
+   :class:`weakref.WeakValueDictionary`.
+
+   `key` specifies a function that will be called with the same
+   positional and keyword arguments as the wrapped function itself,
+   and which has to return a suitable cache key.  Since caches are
+   mappings, the object returned by `key` must be hashable.  The
+   default is to call :func:`cachetools.keys.hashkey`.
+
+   If `lock` is not :const:`None`, it must specify an object
+   implementing the `context manager`_ protocol.  Any access to the
+   cache will then be nested in a ``with lock:`` statement.  This can
+   be used for synchronizing thread access to the cache by providing a
+   :class:`threading.Lock` instance, for example.
+
+   .. note::
+
+      The `lock` context manager is used only to guard access to the
+      cache object.  The underlying wrapped function will be called
+      outside the `with` statement, and must be thread-safe by itself.
+
+   The original underlying function is accessible through the
+   :attr:`__wrapped__` attribute of the memoizing wrapper function.
+   This can be used for introspection or for bypassing the cache.
+
+   To perform operations on the cache object, for example to clear the
+   cache during runtime, the cache should be assigned to a variable.
+   When a `lock` object is used, any access to the cache from outside
+   the function wrapper should also be performed within an appropriate
+   `with` statement:
+
+   .. testcode::
+
+      from cachetools.keys import hashkey
+      from threading import Lock
+
+      cache = LRUCache(maxsize=32)
+      lock = Lock()
+
+      @cached(cache, key=hashkey, lock=lock)
+      def get_pep(num):
+          'Retrieve text of a Python Enhancement Proposal'
+          url = 'http://www.python.org/dev/peps/pep-%04d/' % num
+          with urllib.request.urlopen(url) as s:
+              return s.read()
+
+      # make sure access to cache is synchronized
+      with lock:
+          cache.clear()
+
+      # always use the key function for accessing cache items
+      with lock:
+          cache.pop(hashkey(42), None)
+
+   It is also possible to use a single shared cache object with
+   multiple functions.  However, care must be taken that different
+   cache keys are generated for each function, even for identical
+   function arguments:
+
+   .. doctest::
+      :options: +ELLIPSIS
+
+      >>> from cachetools.keys import hashkey
+      >>> from functools import partial
+
+      >>> # shared cache for integer sequences
+      >>> numcache = {}
+
+      >>> # compute Fibonacci numbers
+      >>> @cached(numcache, key=partial(hashkey, 'fib'))
+      ... def fib(n):
+      ...    return n if n < 2 else fib(n - 1) + fib(n - 2)
+
+      >>> # compute Lucas numbers
+      >>> @cached(numcache, key=partial(hashkey, 'luc'))
+      ... def luc(n):
+      ...    return 2 - n if n < 2 else luc(n - 1) + luc(n - 2)
+
+      >>> fib(42)
+      267914296
+      >>> luc(42)
+      599074578
+      >>> list(sorted(numcache.items()))
+      [..., (('fib', 42), 267914296), ..., (('luc', 42), 599074578)]
+
+
+.. decorator:: cachedmethod(cache, key=cachetools.keys.hashkey, lock=None)
+
+   Decorator to wrap a class or instance method with a memoizing
+   callable that saves results in a (possibly shared) cache.
+
+   The main difference between this and the :func:`cached` function
+   decorator is that `cache` and `lock` are not passed objects, but
+   functions.  Both will be called with :const:`self` (or :const:`cls`
+   for class methods) as their sole argument to retrieve the cache or
+   lock object for the method's respective instance or class.
+
+   .. note::
+
+      As with :func:`cached`, the context manager obtained by calling
+      ``lock(self)`` will only guard access to the cache itself.  It
+      is the user's responsibility to handle concurrent calls to the
+      underlying wrapped method in a multithreaded environment.
+
+   One advantage of :func:`cachedmethod` over the :func:`cached`
+   function decorator is that cache properties such as `maxsize` can
+   be set at runtime:
+
+   .. testcode::
+
+      class CachedPEPs(object):
+
+          def __init__(self, cachesize):
+              self.cache = LRUCache(maxsize=cachesize)
+
+          @cachedmethod(operator.attrgetter('cache'))
+          def get(self, num):
+              """Retrieve text of a Python Enhancement Proposal"""
+              url = 'http://www.python.org/dev/peps/pep-%04d/' % num
+              with urllib.request.urlopen(url) as s:
+                  return s.read()
+
+      peps = CachedPEPs(cachesize=10)
+      print("PEP #1: %s" % peps.get(1))
+
+   .. testoutput::
+      :hide:
+      :options: +ELLIPSIS
+
+      PEP #1: ...
+
+
+   When using a shared cache for multiple methods, be aware that
+   different cache keys must be created for each method even when
+   function arguments are the same, just as with the `@cached`
+   decorator:
+
+   .. testcode::
+
+      class CachedReferences(object):
+
+          def __init__(self, cachesize):
+              self.cache = LRUCache(maxsize=cachesize)
+
+          @cachedmethod(lambda self: self.cache, key=partial(hashkey, 'pep'))
+          def get_pep(self, num):
+              """Retrieve text of a Python Enhancement Proposal"""
+              url = 'http://www.python.org/dev/peps/pep-%04d/' % num
+              with urllib.request.urlopen(url) as s:
+                  return s.read()
+
+          @cachedmethod(lambda self: self.cache, key=partial(hashkey, 'rfc'))
+          def get_rfc(self, num):
+              """Retrieve text of an IETF Request for Comments"""
+              url = 'https://tools.ietf.org/rfc/rfc%d.txt' % num
+              with urllib.request.urlopen(url) as s:
+                  return s.read()
+
+      docs = CachedReferences(cachesize=100)
+      print("PEP #1: %s" % docs.get_pep(1))
+      print("RFC #1: %s" % docs.get_rfc(1))
+
+   .. testoutput::
+      :hide:
+      :options: +ELLIPSIS
+
+      PEP #1: ...
+      RFC #1: ...
+
+
+*****************************************************************
+:mod:`cachetools.keys` --- Key functions for memoizing decorators
+*****************************************************************
+
+.. module:: cachetools.keys
+
+This module provides several functions that can be used as key
+functions with the :func:`cached` and :func:`cachedmethod` decorators:
+
+.. autofunction:: hashkey
+
+   This function returns a :class:`tuple` instance suitable as a cache
+   key, provided the positional and keywords arguments are hashable.
+
+.. autofunction:: typedkey
+
+   This function is similar to :func:`hashkey`, but arguments of
+   different types will yield distinct cache keys.  For example,
+   ``typedkey(3)`` and ``typedkey(3.0)`` will return different
+   results.
+
+These functions can also be helpful when implementing custom key
+functions for handling some non-hashable arguments.  For example,
+calling the following function with a dictionary as its `env` argument
+will raise a :class:`TypeError`, since :class:`dict` is not hashable::
+
+  @cached(LRUCache(maxsize=128))
+  def foo(x, y, z, env={}):
+      pass
+
+However, if `env` always holds only hashable values itself, a custom
+key function can be written that handles the `env` keyword argument
+specially::
+
+  def envkey(*args, env={}, **kwargs):
+      key = hashkey(*args, **kwargs)
+      key += tuple(sorted(env.items()))
+      return key
+
+The :func:`envkey` function can then be used in decorator declarations
+like this::
+
+  @cached(LRUCache(maxsize=128), key=envkey)
+  def foo(x, y, z, env={}):
+      pass
+
+  foo(1, 2, 3, env=dict(a='a', b='b'))
+
+
+****************************************************************************
+:mod:`cachetools.func` --- :func:`functools.lru_cache` compatible decorators
+****************************************************************************
+
+.. module:: cachetools.func
+
+To ease migration from (or to) Python 3's :func:`functools.lru_cache`,
+this module provides several memoizing function decorators with a
+similar API.  All these decorators wrap a function with a memoizing
+callable that saves up to the `maxsize` most recent calls, using
+different caching strategies.  If `maxsize` is set to :const:`None`,
+the caching strategy is effectively disabled and the cache can grow
+without bound.
+
+If the optional argument `typed` is set to :const:`True`, function
+arguments of different types will be cached separately.  For example,
+``f(3)`` and ``f(3.0)`` will be treated as distinct calls with
+distinct results.
+
+If a `user_function` is specified instead, it must be a callable.
+This allows the decorator to be applied directly to a user function,
+leaving the `maxsize` at its default value of 128::
+
+  @cachetools.func.lru_cache
+  def count_vowels(sentence):
+      sentence = sentence.casefold()
+      return sum(sentence.count(vowel) for vowel in 'aeiou')
+
+The wrapped function is instrumented with a :func:`cache_parameters`
+function that returns a new :class:`dict` showing the values for
+`maxsize` and `typed`.  This is for information purposes only.
+Mutating the values has no effect.
+
+The wrapped function is also instrumented with :func:`cache_info` and
+:func:`cache_clear` functions to provide information about cache
+performance and clear the cache.  Please see the
+:func:`functools.lru_cache` documentation for details.  Also note that
+all the decorators in this module are thread-safe by default.
+
+
+.. decorator:: fifo_cache(user_function)
+               fifo_cache(maxsize=128, typed=False)
+
+   Decorator that wraps a function with a memoizing callable that
+   saves up to `maxsize` results based on a First In First Out
+   (FIFO) algorithm.
+
+.. decorator:: lfu_cache(user_function)
+               lfu_cache(maxsize=128, typed=False)
+
+   Decorator that wraps a function with a memoizing callable that
+   saves up to `maxsize` results based on a Least Frequently Used
+   (LFU) algorithm.
+
+.. decorator:: lru_cache(user_function)
+               lru_cache(maxsize=128, typed=False)
+
+   Decorator that wraps a function with a memoizing callable that
+   saves up to `maxsize` results based on a Least Recently Used (LRU)
+   algorithm.
+
+.. decorator:: mru_cache(user_function)
+               mru_cache(maxsize=128, typed=False)
+
+   Decorator that wraps a function with a memoizing callable that
+   saves up to `maxsize` results based on a Most Recently Used (MRU)
+   algorithm.
+
+.. decorator:: rr_cache(user_function)
+               rr_cache(maxsize=128, choice=random.choice, typed=False)
+
+   Decorator that wraps a function with a memoizing callable that
+   saves up to `maxsize` results based on a Random Replacement (RR)
+   algorithm.
+
+.. decorator:: ttl_cache(user_function)
+               ttl_cache(maxsize=128, ttl=600, timer=time.monotonic, typed=False)
+
+   Decorator to wrap a function with a memoizing callable that saves
+   up to `maxsize` results based on a Least Recently Used (LRU)
+   algorithm with a per-item time-to-live (TTL) value.
+
+
+.. _@lru_cache: http://docs.python.org/3/library/functools.html#functools.lru_cache
+.. _cache algorithm: http://en.wikipedia.org/wiki/Cache_algorithms
+.. _context manager: http://docs.python.org/dev/glossary.html#term-context-manager
+.. _mapping: http://docs.python.org/dev/glossary.html#term-mapping
+.. _mutable: http://docs.python.org/dev/glossary.html#term-mutable
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..f87c03a
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,3 @@
+[build-system]
+requires = ["setuptools >= 46.4.0", "wheel"]
+build-backend = "setuptools.build_meta"
diff --git a/setup.cfg b/setup.cfg
new file mode 100644
index 0000000..74bcbc5
--- /dev/null
+++ b/setup.cfg
@@ -0,0 +1,47 @@
+[metadata]
+name = cachetools
+version = attr: cachetools.__version__
+url = https://github.com/tkem/cachetools/
+author = Thomas Kemmer
+author_email = tkemmer@computer.org
+license = MIT
+license_file = LICENSE
+description = Extensible memoizing collections and decorators
+long_description = file: README.rst
+classifiers =
+    Development Status :: 5 - Production/Stable
+    Environment :: Other Environment
+    Intended Audience :: Developers
+    License :: OSI Approved :: MIT License
+    Operating System :: OS Independent
+    Programming Language :: Python
+    Programming Language :: Python :: 3
+    Programming Language :: Python :: 3.5
+    Programming Language :: Python :: 3.6
+    Programming Language :: Python :: 3.7
+    Programming Language :: Python :: 3.8
+    Programming Language :: Python :: 3.9
+    Programming Language :: Python :: 3.10
+    Topic :: Software Development :: Libraries :: Python Modules
+
+[options]
+package_dir =
+    = src
+packages = find:
+python_requires = ~= 3.5
+
+[options.packages.find]
+where = src
+
+[flake8]
+max-line-length = 80
+exclude = .git, .tox, build
+select = C, E, F, W, B, B950, I, N
+# F401: imported but unused (submodule shims)
+# E501: line too long (black)
+ignore = F401, E501
+
+[build_sphinx]
+source-dir = docs/
+build-dir  = docs/_build
+all_files  = 1
diff --git a/setup.py b/setup.py
new file mode 100644
index 0000000..6068493
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,3 @@
+from setuptools import setup
+
+setup()
diff --git a/src/cachetools/__init__.py b/src/cachetools/__init__.py
new file mode 100644
index 0000000..42822f0
--- /dev/null
+++ b/src/cachetools/__init__.py
@@ -0,0 +1,596 @@
+"""Extensible memoizing collections and decorators."""
+
+__all__ = (
+    "Cache",
+    "FIFOCache",
+    "LFUCache",
+    "LRUCache",
+    "MRUCache",
+    "RRCache",
+    "TTLCache",
+    "cached",
+    "cachedmethod",
+)
+
+__version__ = "4.2.4"
+
+import collections
+import collections.abc
+import functools
+import random
+import time
+
+from .keys import hashkey
+
+
+class _DefaultSize:
+
+    __slots__ = ()
+
+    def __getitem__(self, _):
+        return 1
+
+    def __setitem__(self, _, value):
+        assert value == 1
+
+    def pop(self, _):
+        return 1
+
+
+class Cache(collections.abc.MutableMapping):
+    """Mutable mapping to serve as a simple cache or cache base class."""
+
+    __marker = object()
+
+    __size = _DefaultSize()
+
+    def __init__(self, maxsize, getsizeof=None):
+        if getsizeof:
+            self.getsizeof = getsizeof
+        if self.getsizeof is not Cache.getsizeof:
+            self.__size = dict()
+        self.__data = dict()
+        self.__currsize = 0
+        self.__maxsize = maxsize
+
+    def __repr__(self):
+        return "%s(%r, maxsize=%r, currsize=%r)" % (
+            self.__class__.__name__,
+            list(self.__data.items()),
+            self.__maxsize,
+            self.__currsize,
+        )
+
+    def __getitem__(self, key):
+        try:
+            return self.__data[key]
+        except KeyError:
+            return self.__missing__(key)
+
+    def __setitem__(self, key, value):
+        maxsize = self.__maxsize
+        size = self.getsizeof(value)
+        if size > maxsize:
+            raise ValueError("value too large")
+        if key not in self.__data or self.__size[key] < size:
+            while self.__currsize + size > maxsize:
+                self.popitem()
+        if key in self.__data:
+            diffsize = size - self.__size[key]
+        else:
+            diffsize = size
+        self.__data[key] = value
+        self.__size[key] = size
+        self.__currsize += diffsize
+
+    def __delitem__(self, key):
+        size = self.__size.pop(key)
+        del self.__data[key]
+        self.__currsize -= size
+
+    def __contains__(self, key):
+        return key in self.__data
+
+    def __missing__(self, key):
+        raise KeyError(key)
+
+    def __iter__(self):
+        return iter(self.__data)
+
+    def __len__(self):
+        return len(self.__data)
+
+    def get(self, key, default=None):
+        if key in self:
+            return self[key]
+        else:
+            return default
+
+    def pop(self, key, default=__marker):
+        if key in self:
+            value = self[key]
+            del self[key]
+        elif default is self.__marker:
+            raise KeyError(key)
+        else:
+            value = default
+        return value
+
+    def setdefault(self, key, default=None):
+        if key in self:
+            value = self[key]
+        else:
+            self[key] = value = default
+        return value
+
+    @property
+    def maxsize(self):
+        """The maximum size of the cache."""
+        return self.__maxsize
+
+    @property
+    def currsize(self):
+        """The current size of the cache."""
+        return self.__currsize
+
+    @staticmethod
+    def getsizeof(value):
+        """Return the size of a cache element's value."""
+        return 1
+
+
+class FIFOCache(Cache):
+    """First In First Out (FIFO) cache implementation."""
+
+    def __init__(self, maxsize, getsizeof=None):
+        Cache.__init__(self, maxsize, getsizeof)
+        self.__order = collections.OrderedDict()
+
+    def __setitem__(self, key, value, cache_setitem=Cache.__setitem__):
+        cache_setitem(self, key, value)
+        try:
+            self.__order.move_to_end(key)
+        except KeyError:
+            self.__order[key] = None
+
+    def __delitem__(self, key, cache_delitem=Cache.__delitem__):
+        cache_delitem(self, key)
+        del self.__order[key]
+
+    def popitem(self):
+        """Remove and return the `(key, value)` pair first inserted."""
+        try:
+            key = next(iter(self.__order))
+        except StopIteration:
+            raise KeyError("%s is empty" % type(self).__name__) from None
+        else:
+            return (key, self.pop(key))
+
+
+class LFUCache(Cache):
+    """Least Frequently Used (LFU) cache implementation."""
+
+    def __init__(self, maxsize, getsizeof=None):
+        Cache.__init__(self, maxsize, getsizeof)
+        self.__counter = collections.Counter()
+
+    def __getitem__(self, key, cache_getitem=Cache.__getitem__):
+        value = cache_getitem(self, key)
+        if key in self:  # __missing__ may not store item
+            self.__counter[key] -= 1
+        return value
+
+    def __setitem__(self, key, value, cache_setitem=Cache.__setitem__):
+        cache_setitem(self, key, value)
+        self.__counter[key] -= 1
+
+    def __delitem__(self, key, cache_delitem=Cache.__delitem__):
+        cache_delitem(self, key)
+        del self.__counter[key]
+
+    def popitem(self):
+        """Remove and return the `(key, value)` pair least frequently used."""
+        try:
+            ((key, _),) = self.__counter.most_common(1)
+        except ValueError:
+            raise KeyError("%s is empty" % type(self).__name__) from None
+        else:
+            return (key, self.pop(key))
+
+
+class LRUCache(Cache):
+    """Least Recently Used (LRU) cache implementation."""
+
+    def __init__(self, maxsize, getsizeof=None):
+        Cache.__init__(self, maxsize, getsizeof)
+        self.__order = collections.OrderedDict()
+
+    def __getitem__(self, key, cache_getitem=Cache.__getitem__):
+        value = cache_getitem(self, key)
+        if key in self:  # __missing__ may not store item
+            self.__update(key)
+        return value
+
+    def __setitem__(self, key, value, cache_setitem=Cache.__setitem__):
+        cache_setitem(self, key, value)
+        self.__update(key)
+
+    def __delitem__(self, key, cache_delitem=Cache.__delitem__):
+        cache_delitem(self, key)
+        del self.__order[key]
+
+    def popitem(self):
+        """Remove and return the `(key, value)` pair least recently used."""
+        try:
+            key = next(iter(self.__order))
+        except StopIteration:
+            raise KeyError("%s is empty" % type(self).__name__) from None
+        else:
+            return (key, self.pop(key))
+
+    def __update(self, key):
+        try:
+            self.__order.move_to_end(key)
+        except KeyError:
+            self.__order[key] = None
+
+
+class MRUCache(Cache):
+    """Most Recently Used (MRU) cache implementation."""
+
+    def __init__(self, maxsize, getsizeof=None):
+        Cache.__init__(self, maxsize, getsizeof)
+        self.__order = collections.OrderedDict()
+
+    def __getitem__(self, key, cache_getitem=Cache.__getitem__):
+        value = cache_getitem(self, key)
+        if key in self:  # __missing__ may not store item
+            self.__update(key)
+        return value
+
+    def __setitem__(self, key, value, cache_setitem=Cache.__setitem__):
+        cache_setitem(self, key, value)
+        self.__update(key)
+
+    def __delitem__(self, key, cache_delitem=Cache.__delitem__):
+        cache_delitem(self, key)
+        del self.__order[key]
+
+    def popitem(self):
+        """Remove and return the `(key, value)` pair most recently used."""
+        try:
+            key = next(iter(self.__order))
+        except StopIteration:
+            raise KeyError("%s is empty" % type(self).__name__) from None
+        else:
+            return (key, self.pop(key))
+
+    def __update(self, key):
+        try:
+            self.__order.move_to_end(key, last=False)
+        except KeyError:
+            self.__order[key] = None
+
+
+class RRCache(Cache):
+    """Random Replacement (RR) cache implementation."""
+
+    def __init__(self, maxsize, choice=random.choice, getsizeof=None):
+        Cache.__init__(self, maxsize, getsizeof)
+        self.__choice = choice
+
+    @property
+    def choice(self):
+        """The `choice` function used by the cache."""
+        return self.__choice
+
+    def popitem(self):
+        """Remove and return a random `(key, value)` pair."""
+        try:
+            key = self.__choice(list(self))
+        except IndexError:
+            raise KeyError("%s is empty" % type(self).__name__) from None
+        else:
+            return (key, self.pop(key))
+
+
+class _Timer:
+    def __init__(self, timer):
+        self.__timer = timer
+        self.__nesting = 0
+
+    def __call__(self):
+        if self.__nesting == 0:
+            return self.__timer()
+        else:
+            return self.__time
+
+    def __enter__(self):
+        if self.__nesting == 0:
+            self.__time = time = self.__timer()
+        else:
+            time = self.__time
+        self.__nesting += 1
+        return time
+
+    def __exit__(self, *exc):
+        self.__nesting -= 1
+
+    def __reduce__(self):
+        return _Timer, (self.__timer,)
+
+    def __getattr__(self, name):
+        return getattr(self.__timer, name)
+
+
+class _Link:
+
+    __slots__ = ("key", "expire", "next", "prev")
+
+    def __init__(self, key=None, expire=None):
+        self.key = key
+        self.expire = expire
+
+    def __reduce__(self):
+        return _Link, (self.key, self.expire)
+
+    def unlink(self):
+        next = self.next
+        prev = self.prev
+        prev.next = next
+        next.prev = prev
+
+
+class TTLCache(Cache):
+    """LRU Cache implementation with per-item time-to-live (TTL) value."""
+
+    def __init__(self, maxsize, ttl, timer=time.monotonic, getsizeof=None):
+        Cache.__init__(self, maxsize, getsizeof)
+        self.__root = root = _Link()
+        root.prev = root.next = root
+        self.__links = collections.OrderedDict()
+        self.__timer = _Timer(timer)
+        self.__ttl = ttl
+
+    def __contains__(self, key):
+        try:
+            link = self.__links[key]  # no reordering
+        except KeyError:
+            return False
+        else:
+            return not (link.expire < self.__timer())
+
+    def __getitem__(self, key, cache_getitem=Cache.__getitem__):
+        try:
+            link = self.__getlink(key)
+        except KeyError:
+            expired = False
+        else:
+            expired = link.expire < self.__timer()
+        if expired:
+            return self.__missing__(key)
+        else:
+            return cache_getitem(self, key)
+
+    def __setitem__(self, key, value, cache_setitem=Cache.__setitem__):
+        with self.__timer as time:
+            self.expire(time)
+            cache_setitem(self, key, value)
+        try:
+            link = self.__getlink(key)
+        except KeyError:
+            self.__links[key] = link = _Link(key)
+        else:
+            link.unlink()
+        link.expire = time + self.__ttl
+        link.next = root = self.__root
+        link.prev = prev = root.prev
+        prev.next = root.prev = link
+
+    def __delitem__(self, key, cache_delitem=Cache.__delitem__):
+        cache_delitem(self, key)
+        link = self.__links.pop(key)
+        link.unlink()
+        if link.expire < self.__timer():
+            raise KeyError(key)
+
+    def __iter__(self):
+        root = self.__root
+        curr = root.next
+        while curr is not root:
+            # "freeze" time for iterator access
+            with self.__timer as time:
+                if not (curr.expire < time):
+                    yield curr.key
+            curr = curr.next
+
+    def __len__(self):
+        root = self.__root
+        curr = root.next
+        time = self.__timer()
+        count = len(self.__links)
+        while curr is not root and curr.expire < time:
+            count -= 1
+            curr = curr.next
+        return count
+
+    def __setstate__(self, state):
+        self.__dict__.update(state)
+        root = self.__root
+        root.prev = root.next = root
+        for link in sorted(self.__links.values(), key=lambda obj: obj.expire):
+            link.next = root
+            link.prev = prev = root.prev
+            prev.next = root.prev = link
+        self.expire(self.__timer())
+
+    def __repr__(self, cache_repr=Cache.__repr__):
+        with self.__timer as time:
+            self.expire(time)
+            return cache_repr(self)
+
+    @property
+    def currsize(self):
+        with self.__timer as time:
+            self.expire(time)
+            return super().currsize
+
+    @property
+    def timer(self):
+        """The timer function used by the cache."""
+        return self.__timer
+
+    @property
+    def ttl(self):
+        """The time-to-live value of the cache's items."""
+        return self.__ttl
+
+    def expire(self, time=None):
+        """Remove expired items from the cache."""
+        if time is None:
+            time = self.__timer()
+        root = self.__root
+        curr = root.next
+        links = self.__links
+        cache_delitem = Cache.__delitem__
+        while curr is not root and curr.expire < time:
+            cache_delitem(self, curr.key)
+            del links[curr.key]
+            next = curr.next
+            curr.unlink()
+            curr = next
+
+    def clear(self):
+        with self.__timer as time:
+            self.expire(time)
+            Cache.clear(self)
+
+    def get(self, *args, **kwargs):
+        with self.__timer:
+            return Cache.get(self, *args, **kwargs)
+
+    def pop(self, *args, **kwargs):
+        with self.__timer:
+            return Cache.pop(self, *args, **kwargs)
+
+    def setdefault(self, *args, **kwargs):
+        with self.__timer:
+            return Cache.setdefault(self, *args, **kwargs)
+
+    def popitem(self):
+        """Remove and return the `(key, value)` pair least recently used that
+        has not already expired.
+
+        """
+        with self.__timer as time:
+            self.expire(time)
+            try:
+                key = next(iter(self.__links))
+            except StopIteration:
+                raise KeyError("%s is empty" % type(self).__name__) from None
+            else:
+                return (key, self.pop(key))
+
+    def __getlink(self, key):
+        value = self.__links[key]
+        self.__links.move_to_end(key)
+        return value
+
+
+def cached(cache, key=hashkey, lock=None):
+    """Decorator to wrap a function with a memoizing callable that saves
+    results in a cache.
+
+    """
+
+    def decorator(func):
+        if cache is None:
+
+            def wrapper(*args, **kwargs):
+                return func(*args, **kwargs)
+
+        elif lock is None:
+
+            def wrapper(*args, **kwargs):
+                k = key(*args, **kwargs)
+                try:
+                    return cache[k]
+                except KeyError:
+                    pass  # key not found
+                v = func(*args, **kwargs)
+                try:
+                    cache[k] = v
+                except ValueError:
+                    pass  # value too large
+                return v
+
+        else:
+
+            def wrapper(*args, **kwargs):
+                k = key(*args, **kwargs)
+                try:
+                    with lock:
+                        return cache[k]
+                except KeyError:
+                    pass  # key not found
+                v = func(*args, **kwargs)
+                # in case of a race, prefer the item already in the cache
+                try:
+                    with lock:
+                        return cache.setdefault(k, v)
+                except ValueError:
+                    return v  # value too large
+
+        return functools.update_wrapper(wrapper, func)
+
+    return decorator
+
+
+def cachedmethod(cache, key=hashkey, lock=None):
+    """Decorator to wrap a class or instance method with a memoizing
+    callable that saves results in a cache.
+
+    """
+
+    def decorator(method):
+        if lock is None:
+
+            def wrapper(self, *args, **kwargs):
+                c = cache(self)
+                if c is None:
+                    return method(self, *args, **kwargs)
+                k = key(*args, **kwargs)
+                try:
+                    return c[k]
+                except KeyError:
+                    pass  # key not found
+                v = method(self, *args, **kwargs)
+                try:
+                    c[k] = v
+                except ValueError:
+                    pass  # value too large
+                return v
+
+        else:
+
+            def wrapper(self, *args, **kwargs):
+                c = cache(self)
+                if c is None:
+                    return method(self, *args, **kwargs)
+                k = key(*args, **kwargs)
+                try:
+                    with lock(self):
+                        return c[k]
+                except KeyError:
+                    pass  # key not found
+                v = method(self, *args, **kwargs)
+                # in case of a race, prefer the item already in the cache
+                try:
+                    with lock(self):
+                        return c.setdefault(k, v)
+                except ValueError:
+                    return v  # value too large
+
+        return functools.update_wrapper(wrapper, method)
+
+    return decorator
diff --git a/src/cachetools/cache.py b/src/cachetools/cache.py
new file mode 100644
index 0000000..ee5269d
--- /dev/null
+++ b/src/cachetools/cache.py
@@ -0,0 +1,9 @@
+import warnings
+
+from . import Cache
+
+warnings.warn(
+    "cachetools.cache is deprecated, please use cachetools.Cache",
+    DeprecationWarning,
+    stacklevel=2,
+)
diff --git a/src/cachetools/fifo.py b/src/cachetools/fifo.py
new file mode 100644
index 0000000..a29b789
--- /dev/null
+++ b/src/cachetools/fifo.py
@@ -0,0 +1,9 @@
+import warnings
+
+from . import FIFOCache
+
+warnings.warn(
+    "cachetools.fifo is deprecated, please use cachetools.FIFOCache",
+    DeprecationWarning,
+    stacklevel=2,
+)
diff --git a/src/cachetools/func.py b/src/cachetools/func.py
new file mode 100644
index 0000000..01702c2
--- /dev/null
+++ b/src/cachetools/func.py
@@ -0,0 +1,171 @@
+"""`functools.lru_cache` compatible memoizing function decorators."""
+
+__all__ = ("fifo_cache", "lfu_cache", "lru_cache", "mru_cache", "rr_cache", "ttl_cache")
+
+import collections
+import functools
+import math
+import random
+import time
+
+try:
+    from threading import RLock
+except ImportError:  # pragma: no cover
+    from dummy_threading import RLock
+
+from . import FIFOCache, LFUCache, LRUCache, MRUCache, RRCache, TTLCache
+from . import keys
+
+
+_CacheInfo = collections.namedtuple(
+    "CacheInfo", ["hits", "misses", "maxsize", "currsize"]
+)
+
+
+class _UnboundCache(dict):
+    @property
+    def maxsize(self):
+        return None
+
+    @property
+    def currsize(self):
+        return len(self)
+
+
+class _UnboundTTLCache(TTLCache):
+    def __init__(self, ttl, timer):
+        TTLCache.__init__(self, math.inf, ttl, timer)
+
+    @property
+    def maxsize(self):
+        return None
+
+
+def _cache(cache, typed):
+    maxsize = cache.maxsize
+
+    def decorator(func):
+        key = keys.typedkey if typed else keys.hashkey
+        lock = RLock()
+        stats = [0, 0]
+
+        def wrapper(*args, **kwargs):
+            k = key(*args, **kwargs)
+            with lock:
+                try:
+                    v = cache[k]
+                    stats[0] += 1
+                    return v
+                except KeyError:
+                    stats[1] += 1
+            v = func(*args, **kwargs)
+            # in case of a race, prefer the item already in the cache
+            try:
+                with lock:
+                    return cache.setdefault(k, v)
+            except ValueError:
+                return v  # value too large
+
+        def cache_info():
+            with lock:
+                hits, misses = stats
+                maxsize = cache.maxsize
+                currsize = cache.currsize
+            return _CacheInfo(hits, misses, maxsize, currsize)
+
+        def cache_clear():
+            with lock:
+                try:
+                    cache.clear()
+                finally:
+                    stats[:] = [0, 0]
+
+        wrapper.cache_info = cache_info
+        wrapper.cache_clear = cache_clear
+        wrapper.cache_parameters = lambda: {"maxsize": maxsize, "typed": typed}
+        functools.update_wrapper(wrapper, func)
+        return wrapper
+
+    return decorator
+
+
+def fifo_cache(maxsize=128, typed=False):
+    """Decorator to wrap a function with a memoizing callable that saves
+    up to `maxsize` results based on a First In First Out (FIFO)
+    algorithm.
+
+    """
+    if maxsize is None:
+        return _cache(_UnboundCache(), typed)
+    elif callable(maxsize):
+        return _cache(FIFOCache(128), typed)(maxsize)
+    else:
+        return _cache(FIFOCache(maxsize), typed)
+
+
+def lfu_cache(maxsize=128, typed=False):
+    """Decorator to wrap a function with a memoizing callable that saves
+    up to `maxsize` results based on a Least Frequently Used (LFU)
+    algorithm.
+
+    """
+    if maxsize is None:
+        return _cache(_UnboundCache(), typed)
+    elif callable(maxsize):
+        return _cache(LFUCache(128), typed)(maxsize)
+    else:
+        return _cache(LFUCache(maxsize), typed)
+
+
+def lru_cache(maxsize=128, typed=False):
+    """Decorator to wrap a function with a memoizing callable that saves
+    up to `maxsize` results based on a Least Recently Used (LRU)
+    algorithm.
+
+    """
+    if maxsize is None:
+        return _cache(_UnboundCache(), typed)
+    elif callable(maxsize):
+        return _cache(LRUCache(128), typed)(maxsize)
+    else:
+        return _cache(LRUCache(maxsize), typed)
+
+
+def mru_cache(maxsize=128, typed=False):
+    """Decorator to wrap a function with a memoizing callable that saves
+    up to `maxsize` results based on a Most Recently Used (MRU)
+    algorithm.
+    """
+    if maxsize is None:
+        return _cache(_UnboundCache(), typed)
+    elif callable(maxsize):
+        return _cache(MRUCache(128), typed)(maxsize)
+    else:
+        return _cache(MRUCache(maxsize), typed)
+
+
+def rr_cache(maxsize=128, choice=random.choice, typed=False):
+    """Decorator to wrap a function with a memoizing callable that saves
+    up to `maxsize` results based on a Random Replacement (RR)
+    algorithm.
+
+    """
+    if maxsize is None:
+        return _cache(_UnboundCache(), typed)
+    elif callable(maxsize):
+        return _cache(RRCache(128, choice), typed)(maxsize)
+    else:
+        return _cache(RRCache(maxsize, choice), typed)
+
+
+def ttl_cache(maxsize=128, ttl=600, timer=time.monotonic, typed=False):
+    """Decorator to wrap a function with a memoizing callable that saves
+    up to `maxsize` results based on a Least Recently Used (LRU)
+    algorithm with a per-item time-to-live (TTL) value.
+    """
+    if maxsize is None:
+        return _cache(_UnboundTTLCache(ttl, timer), typed)
+    elif callable(maxsize):
+        return _cache(TTLCache(128, ttl, timer), typed)(maxsize)
+    else:
+        return _cache(TTLCache(maxsize, ttl, timer), typed)
diff --git a/src/cachetools/keys.py b/src/cachetools/keys.py
new file mode 100644
index 0000000..13630a4
--- /dev/null
+++ b/src/cachetools/keys.py
@@ -0,0 +1,52 @@
+"""Key functions for memoizing decorators."""
+
+__all__ = ("hashkey", "typedkey")
+
+
+class _HashedTuple(tuple):
+    """A tuple that ensures that hash() will be called no more than once
+    per element, since cache decorators will hash the key multiple
+    times on a cache miss.  See also _HashedSeq in the standard
+    library functools implementation.
+
+    """
+
+    __hashvalue = None
+
+    def __hash__(self, hash=tuple.__hash__):
+        hashvalue = self.__hashvalue
+        if hashvalue is None:
+            self.__hashvalue = hashvalue = hash(self)
+        return hashvalue
+
+    def __add__(self, other, add=tuple.__add__):
+        return _HashedTuple(add(self, other))
+
+    def __radd__(self, other, add=tuple.__add__):
+        return _HashedTuple(add(other, self))
+
+    def __getstate__(self):
+        return {}
+
+
+# used for separating keyword arguments; we do not use an object
+# instance here so identity is preserved when pickling/unpickling
+_kwmark = (_HashedTuple,)
+
+
+def hashkey(*args, **kwargs):
+    """Return a cache key for the specified hashable arguments."""
+
+    if kwargs:
+        return _HashedTuple(args + sum(sorted(kwargs.items()), _kwmark))
+    else:
+        return _HashedTuple(args)
+
+
+def typedkey(*args, **kwargs):
+    """Return a typed cache key for the specified hashable arguments."""
+
+    key = hashkey(*args, **kwargs)
+    key += tuple(type(v) for v in args)
+    key += tuple(type(v) for _, v in sorted(kwargs.items()))
+    return key
diff --git a/src/cachetools/lfu.py b/src/cachetools/lfu.py
new file mode 100644
index 0000000..5c9acef
--- /dev/null
+++ b/src/cachetools/lfu.py
@@ -0,0 +1,9 @@
+import warnings
+
+from . import LFUCache
+
+warnings.warn(
+    "cachetools.lfu is deprecated, please use cachetools.LFUCache",
+    DeprecationWarning,
+    stacklevel=2,
+)
diff --git a/src/cachetools/lru.py b/src/cachetools/lru.py
new file mode 100644
index 0000000..48ddb36
--- /dev/null
+++ b/src/cachetools/lru.py
@@ -0,0 +1,9 @@
+import warnings
+
+from . import LRUCache
+
+warnings.warn(
+    "cachetools.lru is deprecated, please use cachetools.LRUCache",
+    DeprecationWarning,
+    stacklevel=2,
+)
diff --git a/src/cachetools/mru.py b/src/cachetools/mru.py
new file mode 100644
index 0000000..a486dc4
--- /dev/null
+++ b/src/cachetools/mru.py
@@ -0,0 +1,9 @@
+import warnings
+
+from . import MRUCache
+
+warnings.warn(
+    "cachetools.mru is deprecated, please use cachetools.MRUCache",
+    DeprecationWarning,
+    stacklevel=2,
+)
diff --git a/src/cachetools/rr.py b/src/cachetools/rr.py
new file mode 100644
index 0000000..81331bc
--- /dev/null
+++ b/src/cachetools/rr.py
@@ -0,0 +1,9 @@
+import warnings
+
+from . import RRCache
+
+warnings.warn(
+    "cachetools.rr is deprecated, please use cachetools.RRCache",
+    DeprecationWarning,
+    stacklevel=2,
+)
diff --git a/src/cachetools/ttl.py b/src/cachetools/ttl.py
new file mode 100644
index 0000000..ef343da
--- /dev/null
+++ b/src/cachetools/ttl.py
@@ -0,0 +1,9 @@
+import warnings
+
+from . import TTLCache
+
+warnings.warn(
+    "cachetools.ttl is deprecated, please use cachetools.TTLCache",
+    DeprecationWarning,
+    stacklevel=2,
+)
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 0000000..35ac81a
--- /dev/null
+++ b/tests/__init__.py
@@ -0,0 +1,304 @@
+import sys
+import unittest
+
+
+class CacheTestMixin:
+
+    Cache = None
+
+    def test_defaults(self):
+        cache = self.Cache(maxsize=1)
+        self.assertEqual(0, len(cache))
+        self.assertEqual(1, cache.maxsize)
+        self.assertEqual(0, cache.currsize)
+        self.assertEqual(1, cache.getsizeof(None))
+        self.assertEqual(1, cache.getsizeof(""))
+        self.assertEqual(1, cache.getsizeof(0))
+        self.assertTrue(repr(cache).startswith(cache.__class__.__name__))
+
+    def test_insert(self):
+        cache = self.Cache(maxsize=2)
+
+        cache.update({1: 1, 2: 2})
+        self.assertEqual(2, len(cache))
+        self.assertEqual(1, cache[1])
+        self.assertEqual(2, cache[2])
+
+        cache[3] = 3
+        self.assertEqual(2, len(cache))
+        self.assertEqual(3, cache[3])
+        self.assertTrue(1 in cache or 2 in cache)
+
+        cache[4] = 4
+        self.assertEqual(2, len(cache))
+        self.assertEqual(4, cache[4])
+        self.assertTrue(1 in cache or 2 in cache or 3 in cache)
+
+    def test_update(self):
+        cache = self.Cache(maxsize=2)
+
+        cache.update({1: 1, 2: 2})
+        self.assertEqual(2, len(cache))
+        self.assertEqual(1, cache[1])
+        self.assertEqual(2, cache[2])
+
+        cache.update({1: 1, 2: 2})
+        self.assertEqual(2, len(cache))
+        self.assertEqual(1, cache[1])
+        self.assertEqual(2, cache[2])
+
+        cache.update({1: "a", 2: "b"})
+        self.assertEqual(2, len(cache))
+        self.assertEqual("a", cache[1])
+        self.assertEqual("b", cache[2])
+
+    def test_delete(self):
+        cache = self.Cache(maxsize=2)
+
+        cache.update({1: 1, 2: 2})
+        self.assertEqual(2, len(cache))
+        self.assertEqual(1, cache[1])
+        self.assertEqual(2, cache[2])
+
+        del cache[2]
+        self.assertEqual(1, len(cache))
+        self.assertEqual(1, cache[1])
+        self.assertNotIn(2, cache)
+
+        del cache[1]
+        self.assertEqual(0, len(cache))
+        self.assertNotIn(1, cache)
+        self.assertNotIn(2, cache)
+
+        with self.assertRaises(KeyError):
+            del cache[1]
+        self.assertEqual(0, len(cache))
+        self.assertNotIn(1, cache)
+        self.assertNotIn(2, cache)
+
+    def test_pop(self):
+        cache = self.Cache(maxsize=2)
+
+        cache.update({1: 1, 2: 2})
+        self.assertEqual(2, cache.pop(2))
+        self.assertEqual(1, len(cache))
+        self.assertEqual(1, cache.pop(1))
+        self.assertEqual(0, len(cache))
+
+        with self.assertRaises(KeyError):
+            cache.pop(2)
+        with self.assertRaises(KeyError):
+            cache.pop(1)
+        with self.assertRaises(KeyError):
+            cache.pop(0)
+
+        self.assertEqual(None, cache.pop(2, None))
+        self.assertEqual(None, cache.pop(1, None))
+        self.assertEqual(None, cache.pop(0, None))
+
+    def test_popitem(self):
+        cache = self.Cache(maxsize=2)
+
+        cache.update({1: 1, 2: 2})
+        self.assertIn(cache.pop(1), {1: 1, 2: 2})
+        self.assertEqual(1, len(cache))
+        self.assertIn(cache.pop(2), {1: 1, 2: 2})
+        self.assertEqual(0, len(cache))
+
+        with self.assertRaises(KeyError):
+            cache.popitem()
+
+    @unittest.skipUnless(sys.version_info >= (3, 7), "requires Python 3.7")
+    def test_popitem_exception_context(self):
+        # since Python 3.7, MutableMapping.popitem() suppresses
+        # exception context as implementation detail
+        exception = None
+        try:
+            self.Cache(maxsize=2).popitem()
+        except Exception as e:
+            exception = e
+        self.assertIsNone(exception.__cause__)
+        self.assertTrue(exception.__suppress_context__)
+
+    def test_missing(self):
+        class DefaultCache(self.Cache):
+            def __missing__(self, key):
+                self[key] = key
+                return key
+
+        cache = DefaultCache(maxsize=2)
+
+        self.assertEqual(0, cache.currsize)
+        self.assertEqual(2, cache.maxsize)
+        self.assertEqual(0, len(cache))
+        self.assertEqual(1, cache[1])
+        self.assertEqual(2, cache[2])
+        self.assertEqual(2, len(cache))
+        self.assertTrue(1 in cache and 2 in cache)
+
+        self.assertEqual(3, cache[3])
+        self.assertEqual(2, len(cache))
+        self.assertTrue(3 in cache)
+        self.assertTrue(1 in cache or 2 in cache)
+        self.assertTrue(1 not in cache or 2 not in cache)
+
+        self.assertEqual(4, cache[4])
+        self.assertEqual(2, len(cache))
+        self.assertTrue(4 in cache)
+        self.assertTrue(1 in cache or 2 in cache or 3 in cache)
+
+        # verify __missing__() is *not* called for any operations
+        # besides __getitem__()
+
+        self.assertEqual(4, cache.get(4))
+        self.assertEqual(None, cache.get(5))
+        self.assertEqual(5 * 5, cache.get(5, 5 * 5))
+        self.assertEqual(2, len(cache))
+
+        self.assertEqual(4, cache.pop(4))
+        with self.assertRaises(KeyError):
+            cache.pop(5)
+        self.assertEqual(None, cache.pop(5, None))
+        self.assertEqual(5 * 5, cache.pop(5, 5 * 5))
+        self.assertEqual(1, len(cache))
+
+        cache.clear()
+        cache[1] = 1 + 1
+        self.assertEqual(1 + 1, cache.setdefault(1))
+        self.assertEqual(1 + 1, cache.setdefault(1, 1))
+        self.assertEqual(1 + 1, cache[1])
+        self.assertEqual(2 + 2, cache.setdefault(2, 2 + 2))
+        self.assertEqual(2 + 2, cache.setdefault(2, None))
+        self.assertEqual(2 + 2, cache.setdefault(2))
+        self.assertEqual(2 + 2, cache[2])
+        self.assertEqual(2, len(cache))
+        self.assertTrue(1 in cache and 2 in cache)
+        self.assertEqual(None, cache.setdefault(3))
+        self.assertEqual(2, len(cache))
+        self.assertTrue(3 in cache)
+        self.assertTrue(1 in cache or 2 in cache)
+        self.assertTrue(1 not in cache or 2 not in cache)
+
+    def test_missing_getsizeof(self):
+        class DefaultCache(self.Cache):
+            def __missing__(self, key):
+                try:
+                    self[key] = key
+                except ValueError:
+                    pass  # not stored
+                return key
+
+        cache = DefaultCache(maxsize=2, getsizeof=lambda x: x)
+
+        self.assertEqual(0, cache.currsize)
+        self.assertEqual(2, cache.maxsize)
+
+        self.assertEqual(1, cache[1])
+        self.assertEqual(1, len(cache))
+        self.assertEqual(1, cache.currsize)
+        self.assertIn(1, cache)
+
+        self.assertEqual(2, cache[2])
+        self.assertEqual(1, len(cache))
+        self.assertEqual(2, cache.currsize)
+        self.assertNotIn(1, cache)
+        self.assertIn(2, cache)
+
+        self.assertEqual(3, cache[3])  # not stored
+        self.assertEqual(1, len(cache))
+        self.assertEqual(2, cache.currsize)
+        self.assertEqual(1, cache[1])
+        self.assertEqual(1, len(cache))
+        self.assertEqual(1, cache.currsize)
+        self.assertEqual((1, 1), cache.popitem())
+
+    def _test_getsizeof(self, cache):
+        self.assertEqual(0, cache.currsize)
+        self.assertEqual(3, cache.maxsize)
+        self.assertEqual(1, cache.getsizeof(1))
+        self.assertEqual(2, cache.getsizeof(2))
+        self.assertEqual(3, cache.getsizeof(3))
+
+        cache.update({1: 1, 2: 2})
+        self.assertEqual(2, len(cache))
+        self.assertEqual(3, cache.currsize)
+        self.assertEqual(1, cache[1])
+        self.assertEqual(2, cache[2])
+
+        cache[1] = 2
+        self.assertEqual(1, len(cache))
+        self.assertEqual(2, cache.currsize)
+        self.assertEqual(2, cache[1])
+        self.assertNotIn(2, cache)
+
+        cache.update({1: 1, 2: 2})
+        self.assertEqual(2, len(cache))
+        self.assertEqual(3, cache.currsize)
+        self.assertEqual(1, cache[1])
+        self.assertEqual(2, cache[2])
+
+        cache[3] = 3
+        self.assertEqual(1, len(cache))
+        self.assertEqual(3, cache.currsize)
+        self.assertEqual(3, cache[3])
+        self.assertNotIn(1, cache)
+        self.assertNotIn(2, cache)
+
+        with self.assertRaises(ValueError):
+            cache[3] = 4
+        self.assertEqual(1, len(cache))
+        self.assertEqual(3, cache.currsize)
+        self.assertEqual(3, cache[3])
+
+        with self.assertRaises(ValueError):
+            cache[4] = 4
+        self.assertEqual(1, len(cache))
+        self.assertEqual(3, cache.currsize)
+        self.assertEqual(3, cache[3])
+
+    def test_getsizeof_param(self):
+        self._test_getsizeof(self.Cache(maxsize=3, getsizeof=lambda x: x))
+
+    def test_getsizeof_subclass(self):
+        class Cache(self.Cache):
+            def getsizeof(self, value):
+                return value
+
+        self._test_getsizeof(Cache(maxsize=3))
+
+    def test_pickle(self):
+        import pickle
+
+        source = self.Cache(maxsize=2)
+        source.update({1: 1, 2: 2})
+
+        cache = pickle.loads(pickle.dumps(source))
+        self.assertEqual(source, cache)
+
+        self.assertEqual(2, len(cache))
+        self.assertEqual(1, cache[1])
+        self.assertEqual(2, cache[2])
+
+        cache[3] = 3
+        self.assertEqual(2, len(cache))
+        self.assertEqual(3, cache[3])
+        self.assertTrue(1 in cache or 2 in cache)
+
+        cache[4] = 4
+        self.assertEqual(2, len(cache))
+        self.assertEqual(4, cache[4])
+        self.assertTrue(1 in cache or 2 in cache or 3 in cache)
+
+        self.assertEqual(cache, pickle.loads(pickle.dumps(cache)))
+
+    def test_pickle_maxsize(self):
+        import pickle
+        import sys
+
+        # test empty cache, single element, large cache (recursion limit)
+        for n in [0, 1, sys.getrecursionlimit() * 2]:
+            source = self.Cache(maxsize=n)
+            source.update((i, i) for i in range(n))
+            cache = pickle.loads(pickle.dumps(source))
+            self.assertEqual(n, len(cache))
+            self.assertEqual(source, cache)
diff --git a/tests/test_cache.py b/tests/test_cache.py
new file mode 100644
index 0000000..ef87877
--- /dev/null
+++ b/tests/test_cache.py
@@ -0,0 +1,10 @@
+import unittest
+
+import cachetools
+
+from . import CacheTestMixin
+
+
+class CacheTest(unittest.TestCase, CacheTestMixin):
+
+    Cache = cachetools.Cache
diff --git a/tests/test_deprecated.py b/tests/test_deprecated.py
new file mode 100644
index 0000000..6510c82
--- /dev/null
+++ b/tests/test_deprecated.py
@@ -0,0 +1,67 @@
+import unittest
+import warnings
+
+
+class DeprecatedTest(unittest.TestCase):
+    def test_cache(self):
+        with warnings.catch_warnings(record=True) as w:
+            warnings.simplefilter("always")
+            from cachetools.cache import Cache
+
+            assert len(w) == 1
+            assert issubclass(w[-1].category, DeprecationWarning)
+            assert "cachetools.cache" in str(w[-1].message)
+
+    def test_fifo(self):
+        with warnings.catch_warnings(record=True) as w:
+            warnings.simplefilter("always")
+            from cachetools.fifo import FIFOCache
+
+            assert len(w) == 1
+            assert issubclass(w[-1].category, DeprecationWarning)
+            assert "cachetools.fifo" in str(w[-1].message)
+
+    def test_lfu(self):
+        with warnings.catch_warnings(record=True) as w:
+            warnings.simplefilter("always")
+            from cachetools.lfu import LFUCache
+
+            assert len(w) == 1
+            assert issubclass(w[-1].category, DeprecationWarning)
+            assert "cachetools.lfu" in str(w[-1].message)
+
+    def test_lru(self):
+        with warnings.catch_warnings(record=True) as w:
+            warnings.simplefilter("always")
+            from cachetools.lru import LRUCache
+
+            assert len(w) == 1
+            assert issubclass(w[-1].category, DeprecationWarning)
+            assert "cachetools.lru" in str(w[-1].message)
+
+    def test_mru(self):
+        with warnings.catch_warnings(record=True) as w:
+            warnings.simplefilter("always")
+            from cachetools.mru import MRUCache
+
+            assert len(w) == 1
+            assert issubclass(w[-1].category, DeprecationWarning)
+            assert "cachetools.mru" in str(w[-1].message)
+
+    def test_rr(self):
+        with warnings.catch_warnings(record=True) as w:
+            warnings.simplefilter("always")
+            from cachetools.rr import RRCache
+
+            assert len(w) == 1
+            assert issubclass(w[-1].category, DeprecationWarning)
+            assert "cachetools.rr" in str(w[-1].message)
+
+    def test_ttl(self):
+        with warnings.catch_warnings(record=True) as w:
+            warnings.simplefilter("always")
+            from cachetools.ttl import TTLCache
+
+            assert len(w) == 1
+            assert issubclass(w[-1].category, DeprecationWarning)
+            assert "cachetools.ttl" in str(w[-1].message)
diff --git a/tests/test_fifo.py b/tests/test_fifo.py
new file mode 100644
index 0000000..933af56
--- /dev/null
+++ b/tests/test_fifo.py
@@ -0,0 +1,57 @@
+import unittest
+
+from cachetools import FIFOCache
+
+from . import CacheTestMixin
+
+
+class LRUCacheTest(unittest.TestCase, CacheTestMixin):
+
+    Cache = FIFOCache
+
+    def test_fifo(self):
+        cache = FIFOCache(maxsize=2)
+
+        cache[1] = 1
+        cache[2] = 2
+        cache[3] = 3
+
+        self.assertEqual(len(cache), 2)
+        self.assertEqual(cache[2], 2)
+        self.assertEqual(cache[3], 3)
+        self.assertNotIn(1, cache)
+
+        cache[2]
+        cache[4] = 4
+        self.assertEqual(len(cache), 2)
+        self.assertEqual(cache[3], 3)
+        self.assertEqual(cache[4], 4)
+        self.assertNotIn(2, cache)
+
+        cache[5] = 5
+        self.assertEqual(len(cache), 2)
+        self.assertEqual(cache[4], 4)
+        self.assertEqual(cache[5], 5)
+        self.assertNotIn(3, cache)
+
+    def test_fifo_getsizeof(self):
+        cache = FIFOCache(maxsize=3, getsizeof=lambda x: x)
+
+        cache[1] = 1
+        cache[2] = 2
+
+        self.assertEqual(len(cache), 2)
+        self.assertEqual(cache[1], 1)
+        self.assertEqual(cache[2], 2)
+
+        cache[3] = 3
+
+        self.assertEqual(len(cache), 1)
+        self.assertEqual(cache[3], 3)
+        self.assertNotIn(1, cache)
+        self.assertNotIn(2, cache)
+
+        with self.assertRaises(ValueError):
+            cache[4] = 4
+        self.assertEqual(len(cache), 1)
+        self.assertEqual(cache[3], 3)
diff --git a/tests/test_func.py b/tests/test_func.py
new file mode 100644
index 0000000..72e7589
--- /dev/null
+++ b/tests/test_func.py
@@ -0,0 +1,129 @@
+import unittest
+
+import cachetools.func
+
+
+class DecoratorTestMixin:
+    def decorator(self, maxsize, **kwargs):
+        return self.DECORATOR(maxsize, **kwargs)
+
+    def test_decorator(self):
+        cached = self.decorator(maxsize=2)(lambda n: n)
+        self.assertEqual(cached.cache_parameters(), {"maxsize": 2, "typed": False})
+        self.assertEqual(cached.cache_info(), (0, 0, 2, 0))
+        self.assertEqual(cached(1), 1)
+        self.assertEqual(cached.cache_info(), (0, 1, 2, 1))
+        self.assertEqual(cached(1), 1)
+        self.assertEqual(cached.cache_info(), (1, 1, 2, 1))
+        self.assertEqual(cached(1.0), 1.0)
+        self.assertEqual(cached.cache_info(), (2, 1, 2, 1))
+
+    def test_decorator_clear(self):
+        cached = self.decorator(maxsize=2)(lambda n: n)
+        self.assertEqual(cached.cache_parameters(), {"maxsize": 2, "typed": False})
+        self.assertEqual(cached.cache_info(), (0, 0, 2, 0))
+        self.assertEqual(cached(1), 1)
+        self.assertEqual(cached.cache_info(), (0, 1, 2, 1))
+        cached.cache_clear()
+        self.assertEqual(cached.cache_info(), (0, 0, 2, 0))
+        self.assertEqual(cached(1), 1)
+        self.assertEqual(cached.cache_info(), (0, 1, 2, 1))
+
+    def test_decorator_nocache(self):
+        cached = self.decorator(maxsize=0)(lambda n: n)
+        self.assertEqual(cached.cache_parameters(), {"maxsize": 0, "typed": False})
+        self.assertEqual(cached.cache_info(), (0, 0, 0, 0))
+        self.assertEqual(cached(1), 1)
+        self.assertEqual(cached.cache_info(), (0, 1, 0, 0))
+        self.assertEqual(cached(1), 1)
+        self.assertEqual(cached.cache_info(), (0, 2, 0, 0))
+        self.assertEqual(cached(1.0), 1.0)
+        self.assertEqual(cached.cache_info(), (0, 3, 0, 0))
+
+    def test_decorator_unbound(self):
+        cached = self.decorator(maxsize=None)(lambda n: n)
+        self.assertEqual(cached.cache_parameters(), {"maxsize": None, "typed": False})
+        self.assertEqual(cached.cache_info(), (0, 0, None, 0))
+        self.assertEqual(cached(1), 1)
+        self.assertEqual(cached.cache_info(), (0, 1, None, 1))
+        self.assertEqual(cached(1), 1)
+        self.assertEqual(cached.cache_info(), (1, 1, None, 1))
+        self.assertEqual(cached(1.0), 1.0)
+        self.assertEqual(cached.cache_info(), (2, 1, None, 1))
+
+    def test_decorator_typed(self):
+        cached = self.decorator(maxsize=2, typed=True)(lambda n: n)
+        self.assertEqual(cached.cache_parameters(), {"maxsize": 2, "typed": True})
+        self.assertEqual(cached.cache_info(), (0, 0, 2, 0))
+        self.assertEqual(cached(1), 1)
+        self.assertEqual(cached.cache_info(), (0, 1, 2, 1))
+        self.assertEqual(cached(1), 1)
+        self.assertEqual(cached.cache_info(), (1, 1, 2, 1))
+        self.assertEqual(cached(1.0), 1.0)
+        self.assertEqual(cached.cache_info(), (1, 2, 2, 2))
+        self.assertEqual(cached(1.0), 1.0)
+        self.assertEqual(cached.cache_info(), (2, 2, 2, 2))
+
+    def test_decorator_user_function(self):
+        cached = self.decorator(lambda n: n)
+        self.assertEqual(cached.cache_parameters(), {"maxsize": 128, "typed": False})
+        self.assertEqual(cached.cache_info(), (0, 0, 128, 0))
+        self.assertEqual(cached(1), 1)
+        self.assertEqual(cached.cache_info(), (0, 1, 128, 1))
+        self.assertEqual(cached(1), 1)
+        self.assertEqual(cached.cache_info(), (1, 1, 128, 1))
+        self.assertEqual(cached(1.0), 1.0)
+        self.assertEqual(cached.cache_info(), (2, 1, 128, 1))
+
+    def test_decorator_needs_rlock(self):
+        cached = self.decorator(lambda n: n)
+
+        class RecursiveEquals:
+            def __init__(self, use_cache):
+                self._use_cache = use_cache
+
+            def __hash__(self):
+                return hash(self._use_cache)
+
+            def __eq__(self, other):
+                if self._use_cache:
+                    # This call will happen while the cache-lock is held,
+                    # requiring a reentrant lock to avoid deadlock.
+                    cached(self)
+                return self._use_cache == other._use_cache
+
+        # Prime the cache.
+        cached(RecursiveEquals(False))
+        cached(RecursiveEquals(True))
+        # Then do a call which will cause a deadlock with a non-reentrant lock.
+        self.assertEqual(cached(RecursiveEquals(True)), RecursiveEquals(True))
+
+
+class FIFODecoratorTest(unittest.TestCase, DecoratorTestMixin):
+
+    DECORATOR = staticmethod(cachetools.func.fifo_cache)
+
+
+class LFUDecoratorTest(unittest.TestCase, DecoratorTestMixin):
+
+    DECORATOR = staticmethod(cachetools.func.lfu_cache)
+
+
+class LRUDecoratorTest(unittest.TestCase, DecoratorTestMixin):
+
+    DECORATOR = staticmethod(cachetools.func.lru_cache)
+
+
+class MRUDecoratorTest(unittest.TestCase, DecoratorTestMixin):
+
+    DECORATOR = staticmethod(cachetools.func.mru_cache)
+
+
+class RRDecoratorTest(unittest.TestCase, DecoratorTestMixin):
+
+    DECORATOR = staticmethod(cachetools.func.rr_cache)
+
+
+class TTLDecoratorTest(unittest.TestCase, DecoratorTestMixin):
+
+    DECORATOR = staticmethod(cachetools.func.ttl_cache)
diff --git a/tests/test_keys.py b/tests/test_keys.py
new file mode 100644
index 0000000..892a620
--- /dev/null
+++ b/tests/test_keys.py
@@ -0,0 +1,57 @@
+import unittest
+
+import cachetools.keys
+
+
+class CacheKeysTest(unittest.TestCase):
+    def test_hashkey(self, key=cachetools.keys.hashkey):
+        self.assertEqual(key(), key())
+        self.assertEqual(hash(key()), hash(key()))
+        self.assertEqual(key(1, 2, 3), key(1, 2, 3))
+        self.assertEqual(hash(key(1, 2, 3)), hash(key(1, 2, 3)))
+        self.assertEqual(key(1, 2, 3, x=0), key(1, 2, 3, x=0))
+        self.assertEqual(hash(key(1, 2, 3, x=0)), hash(key(1, 2, 3, x=0)))
+        self.assertNotEqual(key(1, 2, 3), key(3, 2, 1))
+        self.assertNotEqual(key(1, 2, 3), key(1, 2, 3, x=None))
+        self.assertNotEqual(key(1, 2, 3, x=0), key(1, 2, 3, x=None))
+        self.assertNotEqual(key(1, 2, 3, x=0), key(1, 2, 3, y=0))
+        with self.assertRaises(TypeError):
+            hash(key({}))
+        # untyped keys compare equal
+        self.assertEqual(key(1, 2, 3), key(1.0, 2.0, 3.0))
+        self.assertEqual(hash(key(1, 2, 3)), hash(key(1.0, 2.0, 3.0)))
+
+    def test_typedkey(self, key=cachetools.keys.typedkey):
+        self.assertEqual(key(), key())
+        self.assertEqual(hash(key()), hash(key()))
+        self.assertEqual(key(1, 2, 3), key(1, 2, 3))
+        self.assertEqual(hash(key(1, 2, 3)), hash(key(1, 2, 3)))
+        self.assertEqual(key(1, 2, 3, x=0), key(1, 2, 3, x=0))
+        self.assertEqual(hash(key(1, 2, 3, x=0)), hash(key(1, 2, 3, x=0)))
+        self.assertNotEqual(key(1, 2, 3), key(3, 2, 1))
+        self.assertNotEqual(key(1, 2, 3), key(1, 2, 3, x=None))
+        self.assertNotEqual(key(1, 2, 3, x=0), key(1, 2, 3, x=None))
+        self.assertNotEqual(key(1, 2, 3, x=0), key(1, 2, 3, y=0))
+        with self.assertRaises(TypeError):
+            hash(key({}))
+        # typed keys compare unequal
+        self.assertNotEqual(key(1, 2, 3), key(1.0, 2.0, 3.0))
+
+    def test_addkeys(self, key=cachetools.keys.hashkey):
+        self.assertIsInstance(key(), tuple)
+        self.assertIsInstance(key(1, 2, 3) + key(4, 5, 6), type(key()))
+        self.assertIsInstance(key(1, 2, 3) + (4, 5, 6), type(key()))
+        self.assertIsInstance((1, 2, 3) + key(4, 5, 6), type(key()))
+
+    def test_pickle(self, key=cachetools.keys.hashkey):
+        import pickle
+
+        for k in [key(), key("abc"), key("abc", 123), key("abc", q="abc")]:
+            # white-box test: assert cached hash value is not pickled
+            self.assertEqual(len(k.__dict__), 0)
+            h = hash(k)
+            self.assertEqual(len(k.__dict__), 1)
+            pickled = pickle.loads(pickle.dumps(k))
+            self.assertEqual(len(pickled.__dict__), 0)
+            self.assertEqual(k, pickled)
+            self.assertEqual(h, hash(pickled))
diff --git a/tests/test_lfu.py b/tests/test_lfu.py
new file mode 100644
index 0000000..6679a88
--- /dev/null
+++ b/tests/test_lfu.py
@@ -0,0 +1,50 @@
+import unittest
+
+from cachetools import LFUCache
+
+from . import CacheTestMixin
+
+
+class LFUCacheTest(unittest.TestCase, CacheTestMixin):
+
+    Cache = LFUCache
+
+    def test_lfu(self):
+        cache = LFUCache(maxsize=2)
+
+        cache[1] = 1
+        cache[1]
+        cache[2] = 2
+        cache[3] = 3
+
+        self.assertEqual(len(cache), 2)
+        self.assertEqual(cache[1], 1)
+        self.assertTrue(2 in cache or 3 in cache)
+        self.assertTrue(2 not in cache or 3 not in cache)
+
+        cache[4] = 4
+        self.assertEqual(len(cache), 2)
+        self.assertEqual(cache[4], 4)
+        self.assertEqual(cache[1], 1)
+
+    def test_lfu_getsizeof(self):
+        cache = LFUCache(maxsize=3, getsizeof=lambda x: x)
+
+        cache[1] = 1
+        cache[2] = 2
+
+        self.assertEqual(len(cache), 2)
+        self.assertEqual(cache[1], 1)
+        self.assertEqual(cache[2], 2)
+
+        cache[3] = 3
+
+        self.assertEqual(len(cache), 1)
+        self.assertEqual(cache[3], 3)
+        self.assertNotIn(1, cache)
+        self.assertNotIn(2, cache)
+
+        with self.assertRaises(ValueError):
+            cache[4] = 4
+        self.assertEqual(len(cache), 1)
+        self.assertEqual(cache[3], 3)
diff --git a/tests/test_lru.py b/tests/test_lru.py
new file mode 100644
index 0000000..fe97803
--- /dev/null
+++ b/tests/test_lru.py
@@ -0,0 +1,57 @@
+import unittest
+
+from cachetools import LRUCache
+
+from . import CacheTestMixin
+
+
+class LRUCacheTest(unittest.TestCase, CacheTestMixin):
+
+    Cache = LRUCache
+
+    def test_lru(self):
+        cache = LRUCache(maxsize=2)
+
+        cache[1] = 1
+        cache[2] = 2
+        cache[3] = 3
+
+        self.assertEqual(len(cache), 2)
+        self.assertEqual(cache[2], 2)
+        self.assertEqual(cache[3], 3)
+        self.assertNotIn(1, cache)
+
+        cache[2]
+        cache[4] = 4
+        self.assertEqual(len(cache), 2)
+        self.assertEqual(cache[2], 2)
+        self.assertEqual(cache[4], 4)
+        self.assertNotIn(3, cache)
+
+        cache[5] = 5
+        self.assertEqual(len(cache), 2)
+        self.assertEqual(cache[4], 4)
+        self.assertEqual(cache[5], 5)
+        self.assertNotIn(2, cache)
+
+    def test_lru_getsizeof(self):
+        cache = LRUCache(maxsize=3, getsizeof=lambda x: x)
+
+        cache[1] = 1
+        cache[2] = 2
+
+        self.assertEqual(len(cache), 2)
+        self.assertEqual(cache[1], 1)
+        self.assertEqual(cache[2], 2)
+
+        cache[3] = 3
+
+        self.assertEqual(len(cache), 1)
+        self.assertEqual(cache[3], 3)
+        self.assertNotIn(1, cache)
+        self.assertNotIn(2, cache)
+
+        with self.assertRaises(ValueError):
+            cache[4] = 4
+        self.assertEqual(len(cache), 1)
+        self.assertEqual(cache[3], 3)
diff --git a/tests/test_method.py b/tests/test_method.py
new file mode 100644
index 0000000..b41dac0
--- /dev/null
+++ b/tests/test_method.py
@@ -0,0 +1,165 @@
+import operator
+import unittest
+
+from cachetools import LRUCache, cachedmethod, keys
+
+
+class Cached:
+    def __init__(self, cache, count=0):
+        self.cache = cache
+        self.count = count
+
+    @cachedmethod(operator.attrgetter("cache"))
+    def get(self, value):
+        count = self.count
+        self.count += 1
+        return count
+
+    @cachedmethod(operator.attrgetter("cache"), key=keys.typedkey)
+    def get_typed(self, value):
+        count = self.count
+        self.count += 1
+        return count
+
+    # https://github.com/tkem/cachetools/issues/107
+    def __hash__(self):
+        raise TypeError("unhashable type")
+
+
+class Locked:
+    def __init__(self, cache):
+        self.cache = cache
+        self.count = 0
+
+    @cachedmethod(operator.attrgetter("cache"), lock=lambda self: self)
+    def get(self, value):
+        return self.count
+
+    def __enter__(self):
+        self.count += 1
+
+    def __exit__(self, *exc):
+        pass
+
+
+class CachedMethodTest(unittest.TestCase):
+    def test_dict(self):
+        cached = Cached({})
+
+        self.assertEqual(cached.get(0), 0)
+        self.assertEqual(cached.get(1), 1)
+        self.assertEqual(cached.get(1), 1)
+        self.assertEqual(cached.get(1.0), 1)
+        self.assertEqual(cached.get(1.0), 1)
+
+        cached.cache.clear()
+        self.assertEqual(cached.get(1), 2)
+
+    def test_typed_dict(self):
+        cached = Cached(LRUCache(maxsize=2))
+
+        self.assertEqual(cached.get_typed(0), 0)
+        self.assertEqual(cached.get_typed(1), 1)
+        self.assertEqual(cached.get_typed(1), 1)
+        self.assertEqual(cached.get_typed(1.0), 2)
+        self.assertEqual(cached.get_typed(1.0), 2)
+        self.assertEqual(cached.get_typed(0.0), 3)
+        self.assertEqual(cached.get_typed(0), 4)
+
+    def test_lru(self):
+        cached = Cached(LRUCache(maxsize=2))
+
+        self.assertEqual(cached.get(0), 0)
+        self.assertEqual(cached.get(1), 1)
+        self.assertEqual(cached.get(1), 1)
+        self.assertEqual(cached.get(1.0), 1)
+        self.assertEqual(cached.get(1.0), 1)
+
+        cached.cache.clear()
+        self.assertEqual(cached.get(1), 2)
+
+    def test_typed_lru(self):
+        cached = Cached(LRUCache(maxsize=2))
+
+        self.assertEqual(cached.get_typed(0), 0)
+        self.assertEqual(cached.get_typed(1), 1)
+        self.assertEqual(cached.get_typed(1), 1)
+        self.assertEqual(cached.get_typed(1.0), 2)
+        self.assertEqual(cached.get_typed(1.0), 2)
+        self.assertEqual(cached.get_typed(0.0), 3)
+        self.assertEqual(cached.get_typed(0), 4)
+
+    def test_nospace(self):
+        cached = Cached(LRUCache(maxsize=0))
+
+        self.assertEqual(cached.get(0), 0)
+        self.assertEqual(cached.get(1), 1)
+        self.assertEqual(cached.get(1), 2)
+        self.assertEqual(cached.get(1.0), 3)
+        self.assertEqual(cached.get(1.0), 4)
+
+    def test_nocache(self):
+        cached = Cached(None)
+
+        self.assertEqual(cached.get(0), 0)
+        self.assertEqual(cached.get(1), 1)
+        self.assertEqual(cached.get(1), 2)
+        self.assertEqual(cached.get(1.0), 3)
+        self.assertEqual(cached.get(1.0), 4)
+
+    def test_weakref(self):
+        import weakref
+        import fractions
+        import gc
+
+        # in Python 3.4, `int` does not support weak references even
+        # when subclassed, but Fraction apparently does...
+        class Int(fractions.Fraction):
+            def __add__(self, other):
+                return Int(fractions.Fraction.__add__(self, other))
+
+        cached = Cached(weakref.WeakValueDictionary(), count=Int(0))
+
+        self.assertEqual(cached.get(0), 0)
+        gc.collect()
+        self.assertEqual(cached.get(0), 1)
+
+        ref = cached.get(1)
+        self.assertEqual(ref, 2)
+        self.assertEqual(cached.get(1), 2)
+        self.assertEqual(cached.get(1.0), 2)
+
+        ref = cached.get_typed(1)
+        self.assertEqual(ref, 3)
+        self.assertEqual(cached.get_typed(1), 3)
+        self.assertEqual(cached.get_typed(1.0), 4)
+
+        cached.cache.clear()
+        self.assertEqual(cached.get(1), 5)
+
+    def test_locked_dict(self):
+        cached = Locked({})
+
+        self.assertEqual(cached.get(0), 1)
+        self.assertEqual(cached.get(1), 3)
+        self.assertEqual(cached.get(1), 3)
+        self.assertEqual(cached.get(1.0), 3)
+        self.assertEqual(cached.get(2.0), 7)
+
+    def test_locked_nocache(self):
+        cached = Locked(None)
+
+        self.assertEqual(cached.get(0), 0)
+        self.assertEqual(cached.get(1), 0)
+        self.assertEqual(cached.get(1), 0)
+        self.assertEqual(cached.get(1.0), 0)
+        self.assertEqual(cached.get(1.0), 0)
+
+    def test_locked_nospace(self):
+        cached = Locked(LRUCache(maxsize=0))
+
+        self.assertEqual(cached.get(0), 1)
+        self.assertEqual(cached.get(1), 3)
+        self.assertEqual(cached.get(1), 5)
+        self.assertEqual(cached.get(1.0), 7)
+        self.assertEqual(cached.get(1.0), 9)
diff --git a/tests/test_mru.py b/tests/test_mru.py
new file mode 100644
index 0000000..d11dba4
--- /dev/null
+++ b/tests/test_mru.py
@@ -0,0 +1,50 @@
+import unittest
+
+from cachetools import MRUCache
+
+from . import CacheTestMixin
+
+
+class MRUCacheTest(unittest.TestCase, CacheTestMixin):
+
+    Cache = MRUCache
+
+    def test_evict__writes_only(self):
+        cache = MRUCache(maxsize=2)
+
+        cache[1] = 1
+        cache[2] = 2
+        cache[3] = 3  # Evicts 1 because nothing's been used yet
+
+        assert len(cache) == 2
+        assert 1 not in cache, "Wrong key was evicted. Should have been '1'."
+        assert 2 in cache
+        assert 3 in cache
+
+    def test_evict__with_access(self):
+        cache = MRUCache(maxsize=2)
+
+        cache[1] = 1
+        cache[2] = 2
+        cache[1]
+        cache[2]
+        cache[3] = 3  # Evicts 2
+        assert 2 not in cache, "Wrong key was evicted. Should have been '2'."
+        assert 1 in cache
+        assert 3 in cache
+
+    def test_evict__with_delete(self):
+        cache = MRUCache(maxsize=2)
+
+        cache[1] = 1
+        cache[2] = 2
+        del cache[2]
+        cache[3] = 3  # Doesn't evict anything because we just deleted 2
+
+        assert 2 not in cache
+        assert 1 in cache
+
+        cache[4] = 4  # Should evict 1 as we just accessed it with __contains__
+        assert 1 not in cache
+        assert 3 in cache
+        assert 4 in cache
diff --git a/tests/test_rr.py b/tests/test_rr.py
new file mode 100644
index 0000000..008978b
--- /dev/null
+++ b/tests/test_rr.py
@@ -0,0 +1,35 @@
+import unittest
+
+from cachetools import RRCache
+
+from . import CacheTestMixin
+
+
+class RRCacheTest(unittest.TestCase, CacheTestMixin):
+
+    Cache = RRCache
+
+    def test_rr(self):
+        cache = RRCache(maxsize=2, choice=min)
+        self.assertEqual(min, cache.choice)
+
+        cache[1] = 1
+        cache[2] = 2
+        cache[3] = 3
+
+        self.assertEqual(2, len(cache))
+        self.assertEqual(2, cache[2])
+        self.assertEqual(3, cache[3])
+        self.assertNotIn(1, cache)
+
+        cache[0] = 0
+        self.assertEqual(2, len(cache))
+        self.assertEqual(0, cache[0])
+        self.assertEqual(3, cache[3])
+        self.assertNotIn(2, cache)
+
+        cache[4] = 4
+        self.assertEqual(2, len(cache))
+        self.assertEqual(3, cache[3])
+        self.assertEqual(4, cache[4])
+        self.assertNotIn(0, cache)
diff --git a/tests/test_ttl.py b/tests/test_ttl.py
new file mode 100644
index 0000000..6e51a59
--- /dev/null
+++ b/tests/test_ttl.py
@@ -0,0 +1,196 @@
+import unittest
+
+from cachetools import TTLCache
+
+from . import CacheTestMixin
+
+
+class Timer:
+    def __init__(self, auto=False):
+        self.auto = auto
+        self.time = 0
+
+    def __call__(self):
+        if self.auto:
+            self.time += 1
+        return self.time
+
+    def tick(self):
+        self.time += 1
+
+
+class TTLTestCache(TTLCache):
+    def __init__(self, maxsize, ttl=0, **kwargs):
+        TTLCache.__init__(self, maxsize, ttl=ttl, timer=Timer(), **kwargs)
+
+
+class TTLCacheTest(unittest.TestCase, CacheTestMixin):
+
+    Cache = TTLTestCache
+
+    def test_ttl(self):
+        cache = TTLCache(maxsize=2, ttl=1, timer=Timer())
+        self.assertEqual(0, cache.timer())
+        self.assertEqual(1, cache.ttl)
+
+        cache[1] = 1
+        self.assertEqual({1}, set(cache))
+        self.assertEqual(1, len(cache))
+        self.assertEqual(1, cache[1])
+
+        cache.timer.tick()
+        self.assertEqual({1}, set(cache))
+        self.assertEqual(1, len(cache))
+        self.assertEqual(1, cache[1])
+
+        cache[2] = 2
+        self.assertEqual({1, 2}, set(cache))
+        self.assertEqual(2, len(cache))
+        self.assertEqual(1, cache[1])
+        self.assertEqual(2, cache[2])
+
+        cache.timer.tick()
+        self.assertEqual({2}, set(cache))
+        self.assertEqual(1, len(cache))
+        self.assertNotIn(1, cache)
+        self.assertEqual(2, cache[2])
+
+        cache[3] = 3
+        self.assertEqual({2, 3}, set(cache))
+        self.assertEqual(2, len(cache))
+        self.assertNotIn(1, cache)
+        self.assertEqual(2, cache[2])
+        self.assertEqual(3, cache[3])
+
+        cache.timer.tick()
+        self.assertEqual({3}, set(cache))
+        self.assertEqual(1, len(cache))
+        self.assertNotIn(1, cache)
+        self.assertNotIn(2, cache)
+        self.assertEqual(3, cache[3])
+
+        cache.timer.tick()
+        self.assertEqual(set(), set(cache))
+        self.assertEqual(0, len(cache))
+        self.assertNotIn(1, cache)
+        self.assertNotIn(2, cache)
+        self.assertNotIn(3, cache)
+
+        with self.assertRaises(KeyError):
+            del cache[1]
+        with self.assertRaises(KeyError):
+            cache.pop(2)
+        with self.assertRaises(KeyError):
+            del cache[3]
+
+    def test_ttl_lru(self):
+        cache = TTLCache(maxsize=2, ttl=0, timer=Timer())
+
+        cache[1] = 1
+        cache[2] = 2
+        cache[3] = 3
+
+        self.assertEqual(len(cache), 2)
+        self.assertNotIn(1, cache)
+        self.assertEqual(cache[2], 2)
+        self.assertEqual(cache[3], 3)
+
+        cache[2]
+        cache[4] = 4
+        self.assertEqual(len(cache), 2)
+        self.assertNotIn(1, cache)
+        self.assertEqual(cache[2], 2)
+        self.assertNotIn(3, cache)
+        self.assertEqual(cache[4], 4)
+
+        cache[5] = 5
+        self.assertEqual(len(cache), 2)
+        self.assertNotIn(1, cache)
+        self.assertNotIn(2, cache)
+        self.assertNotIn(3, cache)
+        self.assertEqual(cache[4], 4)
+        self.assertEqual(cache[5], 5)
+
+    def test_ttl_expire(self):
+        cache = TTLCache(maxsize=3, ttl=2, timer=Timer())
+        with cache.timer as time:
+            self.assertEqual(time, cache.timer())
+        self.assertEqual(2, cache.ttl)
+
+        cache[1] = 1
+        cache.timer.tick()
+        cache[2] = 2
+        cache.timer.tick()
+        cache[3] = 3
+        self.assertEqual(2, cache.timer())
+
+        self.assertEqual({1, 2, 3}, set(cache))
+        self.assertEqual(3, len(cache))
+        self.assertEqual(1, cache[1])
+        self.assertEqual(2, cache[2])
+        self.assertEqual(3, cache[3])
+
+        cache.expire()
+        self.assertEqual({1, 2, 3}, set(cache))
+        self.assertEqual(3, len(cache))
+        self.assertEqual(1, cache[1])
+        self.assertEqual(2, cache[2])
+        self.assertEqual(3, cache[3])
+
+        cache.expire(3)
+        self.assertEqual({2, 3}, set(cache))
+        self.assertEqual(2, len(cache))
+        self.assertNotIn(1, cache)
+        self.assertEqual(2, cache[2])
+        self.assertEqual(3, cache[3])
+
+        cache.expire(4)
+        self.assertEqual({3}, set(cache))
+        self.assertEqual(1, len(cache))
+        self.assertNotIn(1, cache)
+        self.assertNotIn(2, cache)
+        self.assertEqual(3, cache[3])
+
+        cache.expire(5)
+        self.assertEqual(set(), set(cache))
+        self.assertEqual(0, len(cache))
+        self.assertNotIn(1, cache)
+        self.assertNotIn(2, cache)
+        self.assertNotIn(3, cache)
+
+    def test_ttl_atomic(self):
+        cache = TTLCache(maxsize=1, ttl=1, timer=Timer(auto=True))
+        cache[1] = 1
+        self.assertEqual(1, cache[1])
+        cache[1] = 1
+        self.assertEqual(1, cache.get(1))
+        cache[1] = 1
+        self.assertEqual(1, cache.pop(1))
+        cache[1] = 1
+        self.assertEqual(1, cache.setdefault(1))
+        cache[1] = 1
+        cache.clear()
+        self.assertEqual(0, len(cache))
+
+    def test_ttl_tuple_key(self):
+        cache = TTLCache(maxsize=1, ttl=0, timer=Timer())
+        self.assertEqual(0, cache.ttl)
+
+        cache[(1, 2, 3)] = 42
+        self.assertEqual(42, cache[(1, 2, 3)])
+        cache.timer.tick()
+        with self.assertRaises(KeyError):
+            cache[(1, 2, 3)]
+        self.assertNotIn((1, 2, 3), cache)
+
+    def test_ttl_datetime(self):
+        from datetime import datetime, timedelta
+
+        cache = TTLCache(maxsize=1, ttl=timedelta(days=1), timer=datetime.now)
+
+        cache[1] = 1
+        self.assertEqual(1, len(cache))
+        cache.expire(datetime.now())
+        self.assertEqual(1, len(cache))
+        cache.expire(datetime.now() + timedelta(days=1))
+        self.assertEqual(0, len(cache))
diff --git a/tests/test_wrapper.py b/tests/test_wrapper.py
new file mode 100644
index 0000000..37af16b
--- /dev/null
+++ b/tests/test_wrapper.py
@@ -0,0 +1,153 @@
+import unittest
+
+import cachetools
+import cachetools.keys
+
+
+class DecoratorTestMixin:
+    def cache(self, minsize):
+        raise NotImplementedError
+
+    def func(self, *args, **kwargs):
+        if hasattr(self, "count"):
+            self.count += 1
+        else:
+            self.count = 0
+        return self.count
+
+    def test_decorator(self):
+        cache = self.cache(2)
+        wrapper = cachetools.cached(cache)(self.func)
+
+        self.assertEqual(len(cache), 0)
+        self.assertEqual(wrapper.__wrapped__, self.func)
+
+        self.assertEqual(wrapper(0), 0)
+        self.assertEqual(len(cache), 1)
+        self.assertIn(cachetools.keys.hashkey(0), cache)
+        self.assertNotIn(cachetools.keys.hashkey(1), cache)
+        self.assertNotIn(cachetools.keys.hashkey(1.0), cache)
+
+        self.assertEqual(wrapper(1), 1)
+        self.assertEqual(len(cache), 2)
+        self.assertIn(cachetools.keys.hashkey(0), cache)
+        self.assertIn(cachetools.keys.hashkey(1), cache)
+        self.assertIn(cachetools.keys.hashkey(1.0), cache)
+
+        self.assertEqual(wrapper(1), 1)
+        self.assertEqual(len(cache), 2)
+
+        self.assertEqual(wrapper(1.0), 1)
+        self.assertEqual(len(cache), 2)
+
+        self.assertEqual(wrapper(1.0), 1)
+        self.assertEqual(len(cache), 2)
+
+    def test_decorator_typed(self):
+        cache = self.cache(3)
+        key = cachetools.keys.typedkey
+        wrapper = cachetools.cached(cache, key=key)(self.func)
+
+        self.assertEqual(len(cache), 0)
+        self.assertEqual(wrapper.__wrapped__, self.func)
+
+        self.assertEqual(wrapper(0), 0)
+        self.assertEqual(len(cache), 1)
+        self.assertIn(cachetools.keys.typedkey(0), cache)
+        self.assertNotIn(cachetools.keys.typedkey(1), cache)
+        self.assertNotIn(cachetools.keys.typedkey(1.0), cache)
+
+        self.assertEqual(wrapper(1), 1)
+        self.assertEqual(len(cache), 2)
+        self.assertIn(cachetools.keys.typedkey(0), cache)
+        self.assertIn(cachetools.keys.typedkey(1), cache)
+        self.assertNotIn(cachetools.keys.typedkey(1.0), cache)
+
+        self.assertEqual(wrapper(1), 1)
+        self.assertEqual(len(cache), 2)
+
+        self.assertEqual(wrapper(1.0), 2)
+        self.assertEqual(len(cache), 3)
+        self.assertIn(cachetools.keys.typedkey(0), cache)
+        self.assertIn(cachetools.keys.typedkey(1), cache)
+        self.assertIn(cachetools.keys.typedkey(1.0), cache)
+
+        self.assertEqual(wrapper(1.0), 2)
+        self.assertEqual(len(cache), 3)
+
+    def test_decorator_lock(self):
+        class Lock:
+
+            count = 0
+
+            def __enter__(self):
+                Lock.count += 1
+
+            def __exit__(self, *exc):
+                pass
+
+        cache = self.cache(2)
+        wrapper = cachetools.cached(cache, lock=Lock())(self.func)
+
+        self.assertEqual(len(cache), 0)
+        self.assertEqual(wrapper.__wrapped__, self.func)
+        self.assertEqual(wrapper(0), 0)
+        self.assertEqual(Lock.count, 2)
+        self.assertEqual(wrapper(1), 1)
+        self.assertEqual(Lock.count, 4)
+        self.assertEqual(wrapper(1), 1)
+        self.assertEqual(Lock.count, 5)
+
+
+class CacheWrapperTest(unittest.TestCase, DecoratorTestMixin):
+    def cache(self, minsize):
+        return cachetools.Cache(maxsize=minsize)
+
+    def test_zero_size_cache_decorator(self):
+        cache = self.cache(0)
+        wrapper = cachetools.cached(cache)(self.func)
+
+        self.assertEqual(len(cache), 0)
+        self.assertEqual(wrapper.__wrapped__, self.func)
+
+        self.assertEqual(wrapper(0), 0)
+        self.assertEqual(len(cache), 0)
+
+    def test_zero_size_cache_decorator_lock(self):
+        class Lock:
+
+            count = 0
+
+            def __enter__(self):
+                Lock.count += 1
+
+            def __exit__(self, *exc):
+                pass
+
+        cache = self.cache(0)
+        wrapper = cachetools.cached(cache, lock=Lock())(self.func)
+
+        self.assertEqual(len(cache), 0)
+        self.assertEqual(wrapper.__wrapped__, self.func)
+
+        self.assertEqual(wrapper(0), 0)
+        self.assertEqual(len(cache), 0)
+        self.assertEqual(Lock.count, 2)
+
+
+class DictWrapperTest(unittest.TestCase, DecoratorTestMixin):
+    def cache(self, minsize):
+        return dict()
+
+
+class NoneWrapperTest(unittest.TestCase):
+    def func(self, *args, **kwargs):
+        return args + tuple(kwargs.items())
+
+    def test_decorator(self):
+        wrapper = cachetools.cached(None)(self.func)
+        self.assertEqual(wrapper.__wrapped__, self.func)
+
+        self.assertEqual(wrapper(0), (0,))
+        self.assertEqual(wrapper(1), (1,))
+        self.assertEqual(wrapper(1, foo="bar"), (1, ("foo", "bar")))
diff --git a/tox.ini b/tox.ini
new file mode 100644
index 0000000..b36d00c
--- /dev/null
+++ b/tox.ini
@@ -0,0 +1,39 @@
+[tox]
+envlist = check-manifest,docs,doctest,flake8,py
+
+[testenv]
+deps =
+    pytest
+    pytest-cov
+commands =
+    py.test --basetemp={envtmpdir} --cov=cachetools {posargs}
+
+[testenv:check-manifest]
+deps =
+    check-manifest==0.44; python_version < "3.8"
+    check-manifest; python_version >= "3.8"
+commands =
+    check-manifest
+skip_install = true
+
+[testenv:docs]
+deps =
+     sphinx
+commands =
+     sphinx-build -W -b html -d {envtmpdir}/doctrees docs {envtmpdir}/html
+
+[testenv:doctest]
+deps =
+     sphinx
+commands =
+     sphinx-build -W -b doctest -d {envtmpdir}/doctrees docs {envtmpdir}/doctest
+
+[testenv:flake8]
+deps =
+    flake8
+    flake8-black; implementation_name == "cpython"
+    flake8-bugbear
+    flake8-import-order
+commands =
+    flake8
+skip_install = true