repoze.lru-0.6/0000775000175000017500000000000011777616005014644 5ustar tseavertseaver00000000000000repoze.lru-0.6/CHANGES.txt0000664000175000017500000000312011777615462016457 0ustar tseavertseaver00000000000000Changelog ========= 0.6 (2012-07-12) ---------------- - Added a 'CacheMaker' helper class: a maker keeps references (by name) to the caches it creates, to permit them to be cleared. - Added statistics to each cache, tracking lookups, hits, misses, and evictions. - Automated building Sphinx docs and testing example snippets under ``tox``. - Added Sphinx documentation. - Dropped support for Python 2.5. - Added support for PyPy. - Added ``setup.py docs`` alias (installs ``Sphinx`` and dependencies). - Added ``setup.py dev`` alias (runs ``develop`` plus installs ``nose`` and ``coverage``). - Added support for CI under supported Pythons using ``tox``. - Bug: Remove potential race condition on lock in face of interrupts (Issue #10). 0.5 (2012-03-24) ---------------- - Feature: added a new "invalidate()" method to allow removal of items from the cache (issue #8). - Bug: LRUCache.put() could take multiple seconds on large caches (Issue #7). - Bug: LRUCache was not thread safe (Issue #6). - Bug: LRUCache.clock would waste RAM (Issue #4). - Bug: repeated pushing of an entry would remove other cache entries (Issue #3). - Bug: LRUCache would evict entries even when not full (Issue #2). 0.4 (2011-09-04) ---------------- - Moved to GitHub (https://github.com/repoze/repoze.lru). - Added Python 3.2 support. - Python 2.4 no longer supported. - Added tox.ini for easier testing. 0.3 (2009/06/16) ---------------- - Add a thread lock around ``clear`` logic. 0.2 (2009/06/15) ---------------- - Add a ``clear`` method. 0.1 (2009/06/14) ---------------- - Initial release. repoze.lru-0.6/.gitignore0000644000175000017500000000020211765456402016624 0ustar tseavertseaver00000000000000env*/ *.egg *.egg-info *.pyc *$py.class .coverage dist/ *~ .tox/ nosetests.xml coverage.xml repoze/lru/coverage.xml docs/_build/* repoze.lru-0.6/setup.py0000644000175000017500000000423311777615475016370 0ustar tseavertseaver00000000000000############################################################################## # # Copyright (c) 2009 Agendaless Consulting and Contributors. # All Rights Reserved. # # This software is subject to the provisions of the BSD-like license at # http://www.repoze.org/LICENSE.txt. A copy of the license should accompany # this distribution. THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL # EXPRESS OR IMPLIED WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, # THE IMPLIED WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND # FITNESS FOR A PARTICULAR PURPOSE # ############################################################################## import os from setuptools import setup, find_packages here = os.path.abspath(os.path.dirname(__file__)) try: README = open(os.path.join(here, 'README.txt')).read() CHANGES = open(os.path.join(here, 'CHANGES.txt')).read() except: README = '' CHANGES = '' testing_extras = ['nose', 'coverage'] setup(name='repoze.lru', version='0.6', description='A tiny LRU cache implementation and decorator', long_description=README + '\n\n' + CHANGES, classifiers=[ "Intended Audience :: Developers", "Programming Language :: Python", "Programming Language :: Python :: 2.6", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.2", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "License :: Repoze Public License", ], keywords='repoze lru cache', author="Agendaless Consulting", author_email="repoze-dev@lists.repoze.org", url="http://www.repoze.org", license="BSD-derived (http://www.repoze.org/LICENSE.txt)", packages=find_packages(), include_package_data=True, namespace_packages=['repoze'], zip_safe=False, tests_require = [], install_requires = [], test_suite="repoze.lru", entry_points = """\ """, extras_require = { 'testing': testing_extras, 'docs': ['Sphinx',], } ) repoze.lru-0.6/tox.ini0000644000175000017500000000136711765457207016170 0ustar tseavertseaver00000000000000[tox] envlist = py26,py27,py32,pypy,cover,docs [testenv] commands = python setup.py test -q deps = setuptools-git virtualenv [testenv:cover] basepython = python2.6 commands = nosetests --with-xunit --with-xcoverage deps = setuptools-git virtualenv nose coverage nosexcover # we separate coverage into its own testenv because a) "last run wins" wrt # cobertura jenkins reporting and b) pypy and jython can't handle any # combination of versions of coverage and nosexcover that i can find. [testenv:docs] basepython = python2.6 commands = sphinx-build -b html -d docs/_build/doctrees docs docs/_build/html sphinx-build -b doctest -d docs/_build/doctrees docs docs/_build/doctest deps = Sphinx repoze.lru-0.6/README.txt0000664000175000017500000000053511765457677016364 0ustar tseavertseaver00000000000000repoze.lru ========== ``repoze.lru`` is a LRU (least recently used) cache implementation. Keys and values that are not used frequently will be evicted from the cache faster than keys and values that are used frequently. It works under Python 2.5, Python 2.6, Python 2.7, and Python 3.2. Please see ``docs/index.rst`` for detailed documentation. repoze.lru-0.6/setup.cfg0000644000175000017500000000045011777616005016462 0ustar tseavertseaver00000000000000[easy_install] zip_ok = false [nosetests] match = ^test where = repoze/lru nocapture = 1 cover-package = repoze.lru cover-erase = 1 [aliases] dev = develop easy_install repoze.lru[testing] docs = develop easy_install repoze.lru[docs] [egg_info] tag_build = tag_date = 0 tag_svn_revision = 0 repoze.lru-0.6/repoze.lru.egg-info/0000775000175000017500000000000011777616005020443 5ustar tseavertseaver00000000000000repoze.lru-0.6/repoze.lru.egg-info/top_level.txt0000644000175000017500000000000711777616005023170 0ustar tseavertseaver00000000000000repoze repoze.lru-0.6/repoze.lru.egg-info/entry_points.txt0000644000175000017500000000000611777616005023733 0ustar tseavertseaver00000000000000 repoze.lru-0.6/repoze.lru.egg-info/dependency_links.txt0000644000175000017500000000000111777616005024507 0ustar tseavertseaver00000000000000 repoze.lru-0.6/repoze.lru.egg-info/SOURCES.txt0000644000175000017500000000111711777616005022325 0ustar tseavertseaver00000000000000.gitignore CHANGES.txt CONTRIBUTORS.txt COPYRIGHT.txt LICENSE.txt README.txt setup.cfg setup.py tox.ini docs/Makefile docs/api.rst docs/conf.py docs/index.rst docs/make.bat docs/narr.rst docs/_static/placeholder.txt docs/_templates/placeholder.txt repoze/__init__.py repoze.lru.egg-info/PKG-INFO repoze.lru.egg-info/SOURCES.txt repoze.lru.egg-info/dependency_links.txt repoze.lru.egg-info/entry_points.txt repoze.lru.egg-info/namespace_packages.txt repoze.lru.egg-info/not-zip-safe repoze.lru.egg-info/requires.txt repoze.lru.egg-info/top_level.txt repoze/lru/__init__.py repoze/lru/tests.pyrepoze.lru-0.6/repoze.lru.egg-info/not-zip-safe0000644000175000017500000000000111733161761022663 0ustar tseavertseaver00000000000000 repoze.lru-0.6/repoze.lru.egg-info/requires.txt0000644000175000017500000000005011777616005023034 0ustar tseavertseaver00000000000000 [docs] Sphinx [testing] nose coveragerepoze.lru-0.6/repoze.lru.egg-info/PKG-INFO0000644000175000017500000000654611777616005021551 0ustar tseavertseaver00000000000000Metadata-Version: 1.1 Name: repoze.lru Version: 0.6 Summary: A tiny LRU cache implementation and decorator Home-page: http://www.repoze.org Author: Agendaless Consulting Author-email: repoze-dev@lists.repoze.org License: BSD-derived (http://www.repoze.org/LICENSE.txt) Description: repoze.lru ========== ``repoze.lru`` is a LRU (least recently used) cache implementation. Keys and values that are not used frequently will be evicted from the cache faster than keys and values that are used frequently. It works under Python 2.5, Python 2.6, Python 2.7, and Python 3.2. Please see ``docs/index.rst`` for detailed documentation. Changelog ========= 0.6 (2012-07-12) ---------------- - Added a 'CacheMaker' helper class: a maker keeps references (by name) to the caches it creates, to permit them to be cleared. - Added statistics to each cache, tracking lookups, hits, misses, and evictions. - Automated building Sphinx docs and testing example snippets under ``tox``. - Added Sphinx documentation. - Dropped support for Python 2.5. - Added support for PyPy. - Added ``setup.py docs`` alias (installs ``Sphinx`` and dependencies). - Added ``setup.py dev`` alias (runs ``develop`` plus installs ``nose`` and ``coverage``). - Added support for CI under supported Pythons using ``tox``. - Bug: Remove potential race condition on lock in face of interrupts (Issue #10). 0.5 (2012-03-24) ---------------- - Feature: added a new "invalidate()" method to allow removal of items from the cache (issue #8). - Bug: LRUCache.put() could take multiple seconds on large caches (Issue #7). - Bug: LRUCache was not thread safe (Issue #6). - Bug: LRUCache.clock would waste RAM (Issue #4). - Bug: repeated pushing of an entry would remove other cache entries (Issue #3). - Bug: LRUCache would evict entries even when not full (Issue #2). 0.4 (2011-09-04) ---------------- - Moved to GitHub (https://github.com/repoze/repoze.lru). - Added Python 3.2 support. - Python 2.4 no longer supported. - Added tox.ini for easier testing. 0.3 (2009/06/16) ---------------- - Add a thread lock around ``clear`` logic. 0.2 (2009/06/15) ---------------- - Add a ``clear`` method. 0.1 (2009/06/14) ---------------- - Initial release. Keywords: repoze lru cache Platform: UNKNOWN Classifier: Intended Audience :: Developers Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2.6 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.2 Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Programming Language :: Python :: Implementation :: PyPy Classifier: License :: Repoze Public License repoze.lru-0.6/repoze.lru.egg-info/namespace_packages.txt0000644000175000017500000000000711777616005024771 0ustar tseavertseaver00000000000000repoze repoze.lru-0.6/docs/0000775000175000017500000000000011777616005015574 5ustar tseavertseaver00000000000000repoze.lru-0.6/docs/Makefile0000664000175000017500000001271011765455054017237 0ustar tseavertseaver00000000000000# 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 ' where 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/repozelru.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/repozelru.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/repozelru" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/repozelru" @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." repoze.lru-0.6/docs/_templates/0000775000175000017500000000000011777616005017731 5ustar tseavertseaver00000000000000repoze.lru-0.6/docs/_templates/placeholder.txt0000664000175000017500000000000011765456517022751 0ustar tseavertseaver00000000000000repoze.lru-0.6/docs/_static/0000775000175000017500000000000011777616005017222 5ustar tseavertseaver00000000000000repoze.lru-0.6/docs/_static/placeholder.txt0000664000175000017500000000000011765456513022236 0ustar tseavertseaver00000000000000repoze.lru-0.6/docs/narr.rst0000664000175000017500000000476711775314136017304 0ustar tseavertseaver00000000000000Using :mod:`repoze.lru` ====================== ``repoze.lru`` is a LRU (least recently used) cache implementation. Keys and values that are not used frequently will be evicted from the cache faster than keys and values that are used frequently. It works under Python 2.5, Python 2.6, Python 2.7, and Python 3.2. Using the API programmatically ------------------------------ Creating an LRUCache object: .. doctest:: >>> from repoze.lru import LRUCache >>> cache = LRUCache(100) # 100 max length Retrieving from an LRUCache object: .. doctest:: >>> cache.get('nonexisting', 'foo') # return 'foo' 'foo' >>> cache.get('nonexisting') is None True Adding to an LRUCache object: .. doctest:: >>> cache.put('existing', 'value') # add the key 'key' with the value 'value' >>> cache.get('existing') # return the value for existing 'value' Clearing an LRUCache: .. doctest:: >>> cache.clear() Each LRU cache tracks some basic statistics via attributes: cache.lookups # number of calls to the get method cache.hits # number of times a call to get found an object cache.misses # number of times a call to get did not find an object cahce.evictions # number of times a object was evicted from cache Decorating an "expensive" function call --------------------------------------- :mod:`repoze.lru` provides a class :class:`~repoze.lru.lru_cache`, which wrapps another callable, caching the results. All values passed to the decorated function must be hashable. It does not support keyword arguments: .. doctest:: >>> from repoze.lru import lru_cache >>> @lru_cache(500) ... def expensive_function(*arg): #* ... pass Each function decorated with the lru_cache decorator uses its own cache related to that function. Cleaning cache of decorated function ------------------------------------ :mod:`repoze.lru` provides a :class:`~repoze.lru.CacheMaker`, which generates decorators. This way, you can later clear your cache if needed. .. doctest:: >>> from repoze.lru import CacheMaker >>> cache_maker=CacheMaker() >>> @cache_maker.lrucache(maxsize=300, name="adder") ... def yet_another_exepensive_function(*arg):#* ... pass >>> @cache_maker.expiring_lrucache(maxsize=300,timeout=30) ... def another_exepensive_function(*arg):#* ... pass This way, when you need it you can choose to either clear all cache: .. doctest:: >>> cache_maker.clear() or clear a specific cache .. doctest:: >>> cache_maker.clear("adder") repoze.lru-0.6/docs/api.rst0000664000175000017500000000066211775314136017101 0ustar tseavertseaver00000000000000:mod:`repoze.lru` API ===================== Module: :mod:`repoze.lru` -------------------------- .. automodule:: repoze.lru .. autoclass:: LRUCache :members: :member-order: bysource .. autoclass:: ExpiringLRUCache :members: :member-order: bysource .. autoclass:: lru_cache :members: :member-order: bysource .. autoclass:: CacheMaker :members: :member-order: bysource repoze.lru-0.6/docs/make.bat0000664000175000017500000001175611765455054017215 0ustar tseavertseaver00000000000000@ECHO OFF REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set BUILDDIR=_build set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . set I18NSPHINXOPTS=%SPHINXOPTS% . if NOT "%PAPER%" == "" ( set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% ) if "%1" == "" goto help if "%1" == "help" ( :help echo.Please use `make ^` where ^ 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. text to make text files echo. man to make manual pages echo. texinfo to make Texinfo files echo. gettext to make PO message catalogs echo. changes to make an overview over 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 goto end ) if "%1" == "clean" ( for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i del /q /s %BUILDDIR%\* goto end ) if "%1" == "html" ( %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/html. goto end ) if "%1" == "dirhtml" ( %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. goto end ) if "%1" == "singlehtml" ( %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. goto end ) if "%1" == "pickle" ( %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the pickle files. goto end ) if "%1" == "json" ( %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the JSON files. goto end ) if "%1" == "htmlhelp" ( %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run HTML Help Workshop with the ^ .hhp project file in %BUILDDIR%/htmlhelp. goto end ) if "%1" == "qthelp" ( %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run "qcollectiongenerator" with the ^ .qhcp project file in %BUILDDIR%/qthelp, like this: echo.^> qcollectiongenerator %BUILDDIR%\qthelp\repozelru.qhcp echo.To view the help file: echo.^> assistant -collectionFile %BUILDDIR%\qthelp\repozelru.ghc goto end ) if "%1" == "devhelp" ( %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp if errorlevel 1 exit /b 1 echo. echo.Build finished. goto end ) if "%1" == "epub" ( %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub if errorlevel 1 exit /b 1 echo. echo.Build finished. The epub file is in %BUILDDIR%/epub. goto end ) if "%1" == "latex" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex if errorlevel 1 exit /b 1 echo. echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. goto end ) if "%1" == "text" ( %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text if errorlevel 1 exit /b 1 echo. echo.Build finished. The text files are in %BUILDDIR%/text. goto end ) if "%1" == "man" ( %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man if errorlevel 1 exit /b 1 echo. echo.Build finished. The manual pages are in %BUILDDIR%/man. goto end ) if "%1" == "texinfo" ( %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo if errorlevel 1 exit /b 1 echo. echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. goto end ) if "%1" == "gettext" ( %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale if errorlevel 1 exit /b 1 echo. echo.Build finished. The message catalogs are in %BUILDDIR%/locale. goto end ) if "%1" == "changes" ( %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes if errorlevel 1 exit /b 1 echo. echo.The overview file is in %BUILDDIR%/changes. goto end ) if "%1" == "linkcheck" ( %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck if errorlevel 1 exit /b 1 echo. echo.Link check complete; look for any errors in the above output ^ or in %BUILDDIR%/linkcheck/output.txt. goto end ) if "%1" == "doctest" ( %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest if errorlevel 1 exit /b 1 echo. echo.Testing of doctests in the sources finished, look at the ^ results in %BUILDDIR%/doctest/output.txt. goto end ) :end repoze.lru-0.6/docs/conf.py0000664000175000017500000001731211765455054017101 0ustar tseavertseaver00000000000000# -*- coding: utf-8 -*- # # repoze.lru documentation build configuration file, created by # sphinx-quickstart on Mon Jun 11 16:50:52 2012. # # This file is execfile()d with the current directory set to its containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. import sys, os # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. #sys.path.insert(0, os.path.abspath('.')) # -- General configuration ----------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. #needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = ['sphinx.ext.autodoc', 'sphinx.ext.doctest', 'sphinx.ext.todo', 'sphinx.ext.ifconfig', 'sphinx.ext.viewcode'] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix of source filenames. source_suffix = '.rst' # The encoding of source files. #source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' # General information about the project. project = u'repoze.lru' copyright = u'2012, Repoze contributors' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. version = '0.6' # The full version, including alpha/beta/rc tags. release = '0.6' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. #language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: #today = '' # Else, today_fmt is used as the format for a strftime call. #today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = ['_build'] # The reST default role (used for this markup: `text`) to use for all documents. #default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. #add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). #add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. #show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. #modindex_common_prefix = [] # -- Options for HTML output --------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. html_theme = 'default' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. #html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. #html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". #html_title = None # A shorter title for the navigation bar. Default is the same as html_title. #html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. #html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. #html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. #html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. #html_use_smartypants = True # Custom sidebar templates, maps document names to template names. #html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. #html_additional_pages = {} # If false, no module index is generated. #html_domain_indices = True # If false, no index is generated. #html_use_index = True # If true, the index is split into individual pages for each letter. #html_split_index = False # If true, links to the reST sources are added to the pages. #html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. #html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. #html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. #html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). #html_file_suffix = None # Output file base name for HTML help builder. htmlhelp_basename = 'repozelrudoc' # -- Options for LaTeX output -------------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). #'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). #'pointsize': '10pt', # Additional stuff for the LaTeX preamble. #'preamble': '', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ ('index', 'repozelru.tex', u'repoze.lru Documentation', u'Repoze contributors', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. #latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. #latex_use_parts = False # If true, show page references after internal links. #latex_show_pagerefs = False # If true, show URL addresses after external links. #latex_show_urls = False # Documents to append as an appendix to all manuals. #latex_appendices = [] # If false, no module index is generated. #latex_domain_indices = True # -- Options for manual page output -------------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ ('index', 'repozelru', u'repoze.lru Documentation', [u'Repoze contributors'], 1) ] # If true, show URL addresses after external links. #man_show_urls = False # -- Options for Texinfo output ------------------------------------------------ # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ ('index', 'repozelru', u'repoze.lru Documentation', u'Repoze contributors', 'repozelru', 'One line description of project.', 'Miscellaneous'), ] # Documents to append as an appendix to all manuals. #texinfo_appendices = [] # If false, no module index is generated. #texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. #texinfo_show_urls = 'footnote' repoze.lru-0.6/docs/index.rst0000664000175000017500000000027311765455161017440 0ustar tseavertseaver00000000000000:mod:`repoze.lru` ================= Contents: .. toctree:: :maxdepth: 2 narr api Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` repoze.lru-0.6/repoze/0000775000175000017500000000000011777616005016150 5ustar tseavertseaver00000000000000repoze.lru-0.6/repoze/__init__.py0000644000175000017500000000007011531744601020244 0ustar tseavertseaver00000000000000__import__('pkg_resources').declare_namespace(__name__) repoze.lru-0.6/repoze/lru/0000775000175000017500000000000011777616005016752 5ustar tseavertseaver00000000000000repoze.lru-0.6/repoze/lru/__init__.py0000664000175000017500000003140111775314136021060 0ustar tseavertseaver00000000000000""" LRU caching class and decorator """ from __future__ import with_statement import threading import time import uuid _MARKER = object() # By default, expire items after 2**60 seconds. This fits into 64 bit # integers and is close enough to "never" for practical purposes. _DEFAULT_TIMEOUT = 2 ** 60 class LRUCache(object): """ Implements a pseudo-LRU algorithm (CLOCK) The Clock algorithm is not kept strictly to improve performance, e.g. to allow get() and invalidate() to work without acquiring the lock. """ def __init__(self, size): size = int(size) if size < 1: raise ValueError('size must be >0') self.size = size self.lock = threading.Lock() self.hand = 0 self.maxpos = size - 1 self.clock_keys = None self.clock_refs = None self.data = None self.evictions = 0 self.hits = 0 self.misses = 0 self.lookups = 0 self.clear() def clear(self): """Remove all entries from the cache""" with self.lock: # If really clear()ing a full cache, clean up self.data first to # give garbage collection a chance to reduce memorey usage. # Instantiating "[_MARKER] * size" will temporarily have 2 lists # in memory -> high peak memory usage for tiny amount of time. # With self.data already clear, that peak should not exceed what # we normally use. self.data = {} size = self.size self.clock_keys = [_MARKER] * size self.clock_refs = [False] * size self.hand = 0 self.evictions = 0 self.hits = 0 self.misses = 0 self.lookups = 0 def get(self, key, default=None): """Return value for key. If not in cache, return default""" self.lookups += 1 try: pos, val = self.data[key] self.hits += 1 except KeyError: self.misses += 1 return default self.clock_refs[pos] = True return val def put(self, key, val): """Add key to the cache with value val""" # These do not change or they are just references, no need for locking. maxpos = self.maxpos clock_refs = self.clock_refs clock_keys = self.clock_keys data = self.data with self.lock: entry = data.get(key) if entry is not None: # We already have key. Only make sure data is up to date and # to remember that it was used. pos, old_val = entry if old_val is not val: data[key] = (pos, val) self.clock_refs[pos] = True return # else: key is not yet in cache. Search place to insert it. hand = self.hand count = 0 max_count = 107 while 1: ref = clock_refs[hand] if ref == True: clock_refs[hand] = False hand += 1 if hand > maxpos: hand = 0 count += 1 if count >= max_count: # We have been searching long enough. Force eviction of # next entry, no matter what its status is. clock_refs[hand] = False else: oldkey = clock_keys[hand] # Maybe oldkey was not in self.data to begin with. If it # was, self.invalidate() in another thread might have # already removed it. del() would raise KeyError, so pop(). oldentry = data.pop(oldkey, _MARKER) if oldentry is not _MARKER: self.evictions += 1 clock_keys[hand] = key clock_refs[hand] = True data[key] = (hand, val) hand += 1 if hand > maxpos: hand = 0 self.hand = hand break def invalidate(self, key): """Remove key from the cache""" # pop with default arg will not raise KeyError entry = self.data.pop(key, _MARKER) if entry is not _MARKER: # We have no lock, but worst thing that can happen is that we # set another key's entry to False. self.clock_refs[entry[0]] = False # else: key was not in cache. Nothing to do. class ExpiringLRUCache(object): """ Implements a pseudo-LRU algorithm (CLOCK) with expiration times The Clock algorithm is not kept strictly to improve performance, e.g. to allow get() and invalidate() to work without acquiring the lock. """ def __init__(self, size, default_timeout=_DEFAULT_TIMEOUT): self.default_timeout = default_timeout size = int(size) if size < 1: raise ValueError('size must be >0') self.size = size self.lock = threading.Lock() self.hand = 0 self.maxpos = size - 1 self.clock_keys = None self.clock_refs = None self.data = None self.evictions = 0 self.hits = 0 self.misses = 0 self.lookups = 0 self.clear() def clear(self): """Remove all entries from the cache""" with self.lock: # If really clear()ing a full cache, clean up self.data first to # give garbage collection a chance to reduce memorey usage. # Instantiating "[_MARKER] * size" will temporarily have 2 lists # in memory -> high peak memory usage for tiny amount of time. # With self.data already clear, that peak should not exceed what # we normally use. # self.data contains (pos, val, expires) triplets self.data = {} size = self.size self.clock_keys = [_MARKER] * size self.clock_refs = [False] * size self.hand = 0 self.evictions = 0 self.hits = 0 self.misses = 0 self.lookups = 0 def get(self, key, default=None): """Return value for key. If not in cache or expired, return default""" self.lookups += 1 try: pos, val, expires = self.data[key] except KeyError: self.misses += 1 return default if expires > time.time(): # cache entry still valid self.hits += 1 self.clock_refs[pos] = True return val else: # cache entry has expired. Make sure the space in the cache can # be recycled soon. self.misses += 1 self.clock_refs[pos] = False return default def put(self, key, val, timeout=None): """Add key to the cache with value val key will expire in $timeout seconds. If key is already in cache, val and timeout will be updated. """ # These do not change or they are just references, no need for locking. maxpos = self.maxpos clock_refs = self.clock_refs clock_keys = self.clock_keys data = self.data lock = self.lock if timeout is None: timeout = self.default_timeout with self.lock: entry = data.get(key) if entry is not None: # We already have key. Only make sure data is up to date and # to remember that it was used. pos = entry[0] data[key] = (pos, val, time.time() + timeout) clock_refs[pos] = True return # else: key is not yet in cache. Search place to insert it. hand = self.hand count = 0 max_count = 107 while 1: ref = clock_refs[hand] if ref == True: clock_refs[hand] = False hand += 1 if hand > maxpos: hand = 0 count += 1 if count >= max_count: # We have been searching long enough. Force eviction of # next entry, no matter what its status is. clock_refs[hand] = False else: oldkey = clock_keys[hand] # Maybe oldkey was not in self.data to begin with. If it # was, self.invalidate() in another thread might have # already removed it. del() would raise KeyError, so pop(). oldentry = data.pop(oldkey, _MARKER) if oldentry is not _MARKER: self.evictions += 1 clock_keys[hand] = key clock_refs[hand] = True data[key] = (hand, val, time.time() + timeout) hand += 1 if hand > maxpos: hand = 0 self.hand = hand break def invalidate(self, key): """Remove key from the cache""" # pop with default arg will not raise KeyError entry = self.data.pop(key, _MARKER) if entry is not _MARKER: # We have no lock, but worst thing that can happen is that we # set another key's entry to False. self.clock_refs[entry[0]] = False # else: key was not in cache. Nothing to do. class lru_cache(object): """ Decorator for LRU-cached function timeout parameter specifies after how many seconds a cached entry should be considered invalid. """ def __init__(self, maxsize, cache=None, timeout=None): # cache is an arg to serve tests if cache is None: if timeout is None: cache = LRUCache(maxsize) else: cache = ExpiringLRUCache(maxsize, default_timeout=timeout) self.cache = cache def __call__(self, f): cache = self.cache marker = _MARKER def lru_cached(*arg): val = cache.get(arg, marker) if val is marker: val = f(*arg) cache.put(arg, val) return val lru_cached.__module__ = f.__module__ lru_cached.__name__ = f.__name__ lru_cached.__doc__ = f.__doc__ return lru_cached class CacheMaker(object): """Generates decorators that can be cleared later """ def __init__(self, maxsize=None, timeout=_DEFAULT_TIMEOUT): """Create cache decorator factory. - maxsize : the default size for created caches. - timeout : the defaut expiraiton time for created caches. """ self._maxsize = maxsize self._timeout = timeout self._cache = {} def _resolve_setting(self, name=None, maxsize=None, timeout=None): if name is None: while True: name = str(uuid.uuid4()) ## the probability of collision is so low .... if name not in self._cache: break if name in self._cache: raise KeyError("cache %s already in use" % name) if maxsize is None: maxsize = self._maxsize if maxsize is None: raise ValueError("Cache must have a maxsize set") if timeout is None: timeout = self._timeout return name, maxsize, timeout def lrucache(self, name=None, maxsize=None): """Named arguments: - name (optional) is a string, and should be unique amongst all caches - maxsize (optional) is an int, overriding any default value set by the constructor """ name, maxsize, _ = self._resolve_setting(name, maxsize) cache = self._cache[name] = LRUCache(maxsize) return lru_cache(maxsize, cache) def expiring_lrucache(self, name=None, maxsize=None, timeout=None): """Named arguments: - name (optional) is a string, and should be unique amongst all caches - maxsize (optional) is an int, overriding any default value set by the constructor - timeout (optional) is an int, overriding any default value set by the constructor or the default value (%d seconds) """ % _DEFAULT_TIMEOUT name, maxsize, timeout = self._resolve_setting(name, maxsize, timeout) cache = self._cache[name] = ExpiringLRUCache(maxsize, timeout) return lru_cache(maxsize, cache, timeout) def clear(self, *names): """Clear the given cache(s). If no 'names' are passed, clear all caches. """ if len(names) == 0: names = self._cache.keys() for name in names: self._cache[name].clear() repoze.lru-0.6/repoze/lru/tests.py0000664000175000017500000005613711775314136020500 0ustar tseavertseaver00000000000000import random import time import unittest try: range = xrange except NameError: # pragma: NO COVER (Python3) pass class LRUCacheTests(unittest.TestCase): def _getTargetClass(self): from repoze.lru import LRUCache return LRUCache def check_cache_is_consistent(self, cache): """Return if cache is consistent, else raise fail test case.""" # cache.hand/maxpos/size self.assertTrue(cache.hand < len(cache.clock_keys)) self.assertTrue(cache.hand >= 0) self.assertEqual(cache.maxpos, cache.size - 1) self.assertEqual(len(cache.clock_keys), cache.size) # lengths of data structures self.assertEqual(len(cache.clock_keys), len(cache.clock_refs)) self.assertTrue(len(cache.data) <= len(cache.clock_refs)) # For each item in cache.data # 1. pos must be a valid index # 2. clock_keys must point back to the entry for key, value in cache.data.items(): pos, val = value self.assertTrue( type(pos) == type(42) or type(pos) == type(2 ** 128)) self.assertTrue(pos >= 0) self.assertTrue(pos <= cache.maxpos) clock_key = cache.clock_keys[pos] self.assertTrue(clock_key is key) clock_ref = cache.clock_refs[pos] # All clock_refs must be True or False, nothing else. for clock_ref in cache.clock_refs: self.assertTrue(clock_ref is True or clock_ref is False) def _makeOne(self, size): return self._getTargetClass()(size) def test_size_lessthan_1(self): self.assertRaises(ValueError, self._makeOne, 0) def test_get(self): cache = self._makeOne(1) # Must support different types of keys self.assertEqual(cache.get("foo"), None) self.assertEqual(cache.get(42), None) self.assertEqual(cache.get(("foo", 42)), None) self.assertEqual(cache.get(None), None) self.assertEqual(cache.get(""), None) self.assertEqual(cache.get(object()), None) # Check if default value is used self.assertEqual(cache.get("foo", "bar"), "bar") self.assertEqual(cache.get("foo", default="bar"), "bar") self.check_cache_is_consistent(cache) def test_put(self): cache = self._makeOne(8) self.check_cache_is_consistent(cache) # Must support different types of keys cache.put("foo", "FOO") cache.put(42, "fortytwo") cache.put( ("foo", 42), "tuple_as_key") cache.put(None, "None_as_key") cache.put("", "empty_string_as_key") cache.put(3.141, "float_as_key") my_object = object() cache.put(my_object, "object_as_key") self.check_cache_is_consistent(cache) self.assertEqual(cache.get("foo"), "FOO") self.assertEqual(cache.get(42), "fortytwo") self.assertEqual(cache.get(("foo", 42), "fortytwo"), "tuple_as_key") self.assertEqual(cache.get(None), "None_as_key") self.assertEqual(cache.get(""), "empty_string_as_key") self.assertEqual(cache.get(3.141), "float_as_key") self.assertEqual(cache.get(my_object), "object_as_key") # put()ing again must overwrite cache.put(42, "fortytwo again") self.assertEqual(cache.get(42), "fortytwo again") self.check_cache_is_consistent(cache) def test_invalidate(self): cache = self._makeOne(3) cache.put("foo", "bar") cache.put("FOO", "BAR") cache.invalidate("foo") self.assertEqual(cache.get("foo"), None) self.assertEqual(cache.get("FOO"), "BAR") self.check_cache_is_consistent(cache) cache.invalidate("FOO") self.assertEqual(cache.get("foo"), None) self.assertEqual(cache.get("FOO"), None) self.assertEqual(cache.data, {}) self.check_cache_is_consistent(cache) cache.put("foo", "bar") cache.invalidate("nonexistingkey") self.assertEqual(cache.get("foo"), "bar") self.assertEqual(cache.get("FOO"), None) self.check_cache_is_consistent(cache) def test_small_cache(self): """Cache of size 1 must work""" cache = self._makeOne(1) cache.put("foo", "bar") self.assertEqual(cache.get("foo"), "bar") self.check_cache_is_consistent(cache) cache.put("FOO", "BAR") self.assertEqual(cache.get("FOO"), "BAR") self.assertEqual(cache.get("foo"), None) self.check_cache_is_consistent(cache) # put() again cache.put("FOO", "BAR") self.assertEqual(cache.get("FOO"), "BAR") self.assertEqual(cache.get("foo"), None) self.check_cache_is_consistent(cache) # invalidate() cache.invalidate("FOO") self.check_cache_is_consistent(cache) self.assertEqual(cache.get("FOO"), None) self.assertEqual(cache.get("foo"), None) # clear() cache.put("foo", "bar") self.assertEqual(cache.get("foo"), "bar") cache.clear() self.check_cache_is_consistent(cache) self.assertEqual(cache.get("FOO"), None) self.assertEqual(cache.get("foo"), None) def test_equal_but_not_identical(self): """equal but not identical keys must be treated the same""" cache = self._makeOne(1) tuple_one = (1, 1) tuple_two = (1, 1) cache.put(tuple_one, 42) self.assertEqual(cache.get(tuple_one), 42) self.assertEqual(cache.get(tuple_two), 42) self.check_cache_is_consistent(cache) cache = self._makeOne(1) cache.put(tuple_one, 42) cache.invalidate(tuple_two) self.assertEqual(cache.get(tuple_one), None) self.assertEqual(cache.get(tuple_two), None) def test_perfect_hitrate(self): """If cache size equals number of items, expect 100% cache hits""" size = 1000 cache = self._makeOne(size) for count in range(size): cache.put(count, "item%s" % count) for cache_op in range(10000): item = random.randrange(0, size - 1) if random.getrandbits(1): self.assertEqual(cache.get(item), "item%s" % item) else: cache.put(item, "item%s" % item) self.assertEqual(cache.misses, 0) self.assertEqual(cache.evictions, 0) self.check_cache_is_consistent(cache) def test_imperfect_hitrate(self): """If cache size == half the number of items -> hit rate ~50%""" size = 1000 cache = self._makeOne(size / 2) for count in range(size): cache.put(count, "item%s" % count) hits = 0 misses = 0 total_gets = 0 for cache_op in range(10000): item = random.randrange(0, size - 1) if random.getrandbits(1): entry = cache.get(item) total_gets += 1 self.assertTrue( (entry == "item%s" % item) or entry is None) if entry is None: misses += 1 else: hits += 1 else: cache.put(item, "item%s" % item) # Cache hit rate should be roughly 50% hit_ratio = hits / float(total_gets) * 100 self.assertTrue(hit_ratio > 45) self.assertTrue(hit_ratio < 55) # The internal cache counters should have the same information internal_hit_ratio = 100 * cache.hits / cache.lookups self.assertTrue(internal_hit_ratio > 45) self.assertTrue(internal_hit_ratio < 55) # The internal miss counters should also be around 50% internal_miss_ratio = 100 * cache.misses / cache.lookups self.assertTrue(internal_miss_ratio > 45) self.assertTrue(internal_miss_ratio < 55) self.check_cache_is_consistent(cache) def test_eviction_counter(self): cache = self._makeOne(2) cache.put(1, 1) cache.put(2, 1) self.assertEqual(cache.evictions, 0) cache.put(3, 1) cache.put(4, 1) self.assertEqual(cache.evictions, 2) cache.put(3, 1) cache.put(4, 1) self.assertEqual(cache.evictions, 2) cache.clear() self.assertEqual(cache.evictions, 0) def test_it(self): cache = self._makeOne(3) self.assertEqual(cache.get('a'), None) cache.put('a', '1') pos, value = cache.data.get('a') self.assertEqual(cache.clock_refs[pos], True) self.assertEqual(cache.clock_keys[pos], 'a') self.assertEqual(value, '1') self.assertEqual(cache.get('a'), '1') self.assertEqual(cache.hand, pos + 1) pos, value = cache.data.get('a') self.assertEqual(cache.clock_refs[pos], True) self.assertEqual(cache.hand, pos + 1) self.assertEqual(len(cache.data), 1) cache.put('b', '2') pos, value = cache.data.get('b') self.assertEqual(cache.clock_refs[pos], True) self.assertEqual(cache.clock_keys[pos], 'b') self.assertEqual(len(cache.data), 2) cache.put('c', '3') pos, value = cache.data.get('c') self.assertEqual(cache.clock_refs[pos], True) self.assertEqual(cache.clock_keys[pos], 'c') self.assertEqual(len(cache.data), 3) pos, value = cache.data.get('a') self.assertEqual(cache.clock_refs[pos], True) cache.get('a') # All items have ref==True. cache.hand points to "a". Putting # "d" will set ref=False on all items and then replace "a", # because "a" is the first item with ref==False that is found. cache.put('d', '4') self.assertEqual(len(cache.data), 3) self.assertEqual(cache.data.get('a'), None) # Only item "d" has ref==True. cache.hand points at "b", so "b" # will be evicted when "e" is inserted. "c" will be left alone. cache.put('e', '5') self.assertEqual(len(cache.data), 3) self.assertEqual(cache.data.get('b'), None) self.assertEqual(cache.get('d'), '4') self.assertEqual(cache.get('e'), '5') self.assertEqual(cache.get('a'), None) self.assertEqual(cache.get('b'), None) self.assertEqual(cache.get('c'), '3') self.check_cache_is_consistent(cache) class ExpiringLRUCacheTests(LRUCacheTests): def _getTargetClass(self): from repoze.lru import ExpiringLRUCache return ExpiringLRUCache def _makeOne(self, size, default_timeout=None): if default_timeout is None: return self._getTargetClass()(size) else: return self._getTargetClass()(size, default_timeout=default_timeout) def check_cache_is_consistent(self, cache): """Return if cache is consistent, else raise fail test case. This is slightly different for ExpiringLRUCache since self.data contains 3-tuples instead of 2-tuples. """ # cache.hand/maxpos/size self.assertTrue(cache.hand < len(cache.clock_keys)) self.assertTrue(cache.hand >= 0) self.assertEqual(cache.maxpos, cache.size - 1) self.assertEqual(len(cache.clock_keys), cache.size) # lengths of data structures self.assertEqual(len(cache.clock_keys), len(cache.clock_refs)) self.assertTrue(len(cache.data) <= len(cache.clock_refs)) # For each item in cache.data # 1. pos must be a valid index # 2. clock_keys must point back to the entry for key, value in cache.data.items(): pos, val, timeout = value self.assertTrue( type(pos) == type(42) or type(pos) == type(2 ** 128)) self.assertTrue(pos >= 0) self.assertTrue(pos <= cache.maxpos) clock_key = cache.clock_keys[pos] self.assertTrue(clock_key is key) clock_ref = cache.clock_refs[pos] self.assertTrue(type(timeout) == type(3.141)) # All clock_refs must be True or False, nothing else. for clock_ref in cache.clock_refs: self.assertTrue(clock_ref is True or clock_ref is False) def test_it(self): """Test a sequence of operations Looks at internal data, which is different for ExpiringLRUCache. """ cache = self._makeOne(3) self.assertEqual(cache.get('a'), None) cache.put('a', '1') pos, value, expires = cache.data.get('a') self.assertEqual(cache.clock_refs[pos], True) self.assertEqual(cache.clock_keys[pos], 'a') self.assertEqual(value, '1') self.assertEqual(cache.get('a'), '1') self.assertEqual(cache.hand, pos + 1) pos, value, expires = cache.data.get('a') self.assertEqual(cache.clock_refs[pos], True) self.assertEqual(cache.hand, pos + 1) self.assertEqual(len(cache.data), 1) cache.put('b', '2') pos, value, expires = cache.data.get('b') self.assertEqual(cache.clock_refs[pos], True) self.assertEqual(cache.clock_keys[pos], 'b') self.assertEqual(len(cache.data), 2) cache.put('c', '3') pos, value, expires = cache.data.get('c') self.assertEqual(cache.clock_refs[pos], True) self.assertEqual(cache.clock_keys[pos], 'c') self.assertEqual(len(cache.data), 3) pos, value, expires = cache.data.get('a') self.assertEqual(cache.clock_refs[pos], True) cache.get('a') # All items have ref==True. cache.hand points to "a". Putting # "d" will set ref=False on all items and then replace "a", # because "a" is the first item with ref==False that is found. cache.put('d', '4') self.assertEqual(len(cache.data), 3) self.assertEqual(cache.data.get('a'), None) # Only item "d" has ref==True. cache.hand points at "b", so "b" # will be evicted when "e" is inserted. "c" will be left alone. cache.put('e', '5') self.assertEqual(len(cache.data), 3) self.assertEqual(cache.data.get('b'), None) self.assertEqual(cache.get('d'), '4') self.assertEqual(cache.get('e'), '5') self.assertEqual(cache.get('a'), None) self.assertEqual(cache.get('b'), None) self.assertEqual(cache.get('c'), '3') self.check_cache_is_consistent(cache) def test_default_timeout(self): """Default timeout provided at init time must be applied""" # Provide no default timeout -> entries must remain valid cache = self._makeOne(3) cache.put("foo", "bar") time.sleep(0.1) cache.put("FOO", "BAR") self.assertEqual(cache.get("foo"), "bar") self.assertEqual(cache.get("FOO"), "BAR") self.check_cache_is_consistent(cache) # Provide short default timeout -> entries must become invalid cache = self._makeOne(3, default_timeout=0.1) cache.put("foo", "bar") time.sleep(0.1) cache.put("FOO", "BAR") self.assertEqual(cache.get("foo"), None) self.assertEqual(cache.get("FOO"), "BAR") self.check_cache_is_consistent(cache) def test_different_timeouts(self): """Timeouts must be per entry, default applied when none provided""" cache = self._makeOne(3, default_timeout=0.1) cache.put("one", 1) cache.put("two", 2, timeout=0.2) cache.put("three", 3, timeout=0.3) # All entries still here self.assertEqual(cache.get("one"), 1) self.assertEqual(cache.get("two"), 2) self.assertEqual(cache.get("three"), 3) # Entry "one" must expire, "two"/"three" remain valid time.sleep(0.1) self.assertEqual(cache.get("one"), None) self.assertEqual(cache.get("two"), 2) self.assertEqual(cache.get("three"), 3) # Only "three" remains valid time.sleep(0.1) self.assertEqual(cache.get("one"), None) self.assertEqual(cache.get("two"), None) self.assertEqual(cache.get("three"), 3) # All have expired time.sleep(0.1) self.assertEqual(cache.get("one"), None) self.assertEqual(cache.get("two"), None) self.assertEqual(cache.get("three"), None) self.check_cache_is_consistent(cache) def test_renew_timeout(self): """Re-putting an entry must update timeout""" cache = self._makeOne(3, default_timeout=0.2) cache.put("foo", "bar") cache.put("foo2", "bar2", timeout=10) cache.put("foo3", "bar3", timeout=10) time.sleep(0.1) # All must still be here self.assertEqual(cache.get("foo"), "bar") self.assertEqual(cache.get("foo2"), "bar2") self.assertEqual(cache.get("foo3"), "bar3") self.check_cache_is_consistent(cache) # Set new timeouts by re-put()ing the entries cache.put("foo", "bar") cache.put("foo2", "bar2", timeout=0.1) cache.put("foo3", "bar3") time.sleep(0.1) # "foo2" must have expired self.assertEqual(cache.get("foo"), "bar") self.assertEqual(cache.get("foo2"), None) self.assertEqual(cache.get("foo3"), "bar3") self.check_cache_is_consistent(cache) class DecoratorTests(unittest.TestCase): def _getTargetClass(self): from repoze.lru import lru_cache return lru_cache def _makeOne(self, maxsize, cache, timeout=None): return self._getTargetClass()(maxsize, timeout=timeout, cache=cache) def test_ctor_nocache(self): decorator = self._makeOne(10, None) self.assertEqual(decorator.cache.size, 10) def test_singlearg(self): cache = DummyLRUCache() decorator = self._makeOne(0, cache) def wrapped(key): return key decorated = decorator(wrapped) result = decorated(1) self.assertEqual(cache[(1,)], 1) self.assertEqual(result, 1) self.assertEqual(len(cache), 1) result = decorated(2) self.assertEqual(cache[(2,)], 2) self.assertEqual(result, 2) self.assertEqual(len(cache), 2) result = decorated(2) self.assertEqual(cache[(2,)], 2) self.assertEqual(result, 2) self.assertEqual(len(cache), 2) def test_multiargs(self): cache = DummyLRUCache() decorator = self._makeOne(0, cache) def moreargs(*args): return args decorated = decorator(moreargs) result = decorated(3, 4, 5) self.assertEqual(cache[(3, 4, 5)], (3, 4, 5)) self.assertEqual(result, (3, 4, 5)) self.assertEqual(len(cache), 1) def test_expiry(self): """When timeout is given, decorator must eventually forget entries""" @self._makeOne(1, None, timeout=0.1) def sleep_a_bit(param): time.sleep(0.1) return 2 * param # First call must take at least 0.1 seconds start = time.time() result1 = sleep_a_bit("hello") stop = time.time() self.assertEqual(result1, 2 * "hello") self.assertTrue(stop - start > 0.1) # Second call must take less than 0.1 seconds. start = time.time() result2 = sleep_a_bit("hello") stop = time.time() self.assertEqual(result2, 2 * "hello") self.assertTrue(stop - start < 0.1) time.sleep(0.1) # This one must calculate again and take at least 0.1 seconds start = time.time() result3 = sleep_a_bit("hello") stop = time.time() self.assertEqual(result3, 2 * "hello") self.assertTrue(stop - start > 0.1) class DummyLRUCache(dict): def put(self, k, v): return self.__setitem__(k, v) class CacherMaker(unittest.TestCase): def _getTargetClass(self): from repoze.lru import CacheMaker return CacheMaker def _makeOne(self, *args, **kw): return self._getTargetClass()(*args, **kw) def test_named_cache(self): maker = self._makeOne() size = 10 name = "name" decorated = maker.lrucache(maxsize=size, name=name)(_adder) self.assertEqual(list(maker._cache.keys()), [name]) self.assertEqual(maker._cache[name].size, size) decorated(10) decorated(11) self.assertEqual(len(maker._cache[name].data),2) def test_exception(self): maker = self._makeOne() size = 10 name = "name" decorated = maker.lrucache(maxsize=size, name=name)(_adder) self.assertRaises(KeyError, maker.lrucache, maxsize=size, name=name) self.assertRaises(ValueError, maker.lrucache) def test_defaultvalue_and_clear(self): size = 10 maker = self._makeOne(maxsize=size) for i in range(100): decorated = maker.lrucache()(_adder) decorated(10) self.assertEqual(len(maker._cache) , 100) for _cache in maker._cache.values(): self.assertEqual( _cache.size,size) self.assertEqual(len(_cache.data),1) ## and test clear cache maker.clear() for _cache in maker._cache.values(): self.assertEqual( _cache.size,size) self.assertEqual(len(_cache.data),0) def test_clear_with_single_name(self): maker = self._makeOne(maxsize=10) one = maker.lrucache(name='one')(_adder) two = maker.lrucache(name='two')(_adder) for i in range(100): _ = one(i) _ = two(i) self.assertEqual(len(maker._cache['one'].data), 10) self.assertEqual(len(maker._cache['two'].data), 10) maker.clear('one') self.assertEqual(len(maker._cache['one'].data), 0) self.assertEqual(len(maker._cache['two'].data), 10) def test_clear_with_multiple_names(self): maker = self._makeOne(maxsize=10) one = maker.lrucache(name='one')(_adder) two = maker.lrucache(name='two')(_adder) three = maker.lrucache(name='three')(_adder) for i in range(100): _ = one(i) _ = two(i) _ = three(i) self.assertEqual(len(maker._cache['one'].data), 10) self.assertEqual(len(maker._cache['two'].data), 10) self.assertEqual(len(maker._cache['three'].data), 10) maker.clear('one', 'three') self.assertEqual(len(maker._cache['one'].data), 0) self.assertEqual(len(maker._cache['two'].data), 10) self.assertEqual(len(maker._cache['three'].data), 0) def test_expiring(self): size = 10 timeout = 10 name = "name" cache = self._makeOne(maxsize=size, timeout=timeout) for i in range(100): if not i: decorated = cache.expiring_lrucache(name=name)(_adder) self.assertEqual( cache._cache[name].size,size) else: decorated = cache.expiring_lrucache()(_adder) decorated(10) self.assertEqual( len(cache._cache) , 100) for _cache in cache._cache.values(): self.assertEqual( _cache.size,size) self.assertEqual( _cache.default_timeout,timeout) self.assertEqual(len(_cache.data),1) ## and test clear cache cache.clear() for _cache in cache._cache.values(): self.assertEqual( _cache.size,size) self.assertEqual(len(_cache.data),0) def _adder(x): return x + 10 repoze.lru-0.6/COPYRIGHT.txt0000644000175000017500000000015511531744601016744 0ustar tseavertseaver00000000000000Copyright (c) 2009 Agendaless Consulting and Contributors. (http://www.agendaless.com), All Rights Reserved repoze.lru-0.6/PKG-INFO0000664000175000017500000000654611777616005015754 0ustar tseavertseaver00000000000000Metadata-Version: 1.1 Name: repoze.lru Version: 0.6 Summary: A tiny LRU cache implementation and decorator Home-page: http://www.repoze.org Author: Agendaless Consulting Author-email: repoze-dev@lists.repoze.org License: BSD-derived (http://www.repoze.org/LICENSE.txt) Description: repoze.lru ========== ``repoze.lru`` is a LRU (least recently used) cache implementation. Keys and values that are not used frequently will be evicted from the cache faster than keys and values that are used frequently. It works under Python 2.5, Python 2.6, Python 2.7, and Python 3.2. Please see ``docs/index.rst`` for detailed documentation. Changelog ========= 0.6 (2012-07-12) ---------------- - Added a 'CacheMaker' helper class: a maker keeps references (by name) to the caches it creates, to permit them to be cleared. - Added statistics to each cache, tracking lookups, hits, misses, and evictions. - Automated building Sphinx docs and testing example snippets under ``tox``. - Added Sphinx documentation. - Dropped support for Python 2.5. - Added support for PyPy. - Added ``setup.py docs`` alias (installs ``Sphinx`` and dependencies). - Added ``setup.py dev`` alias (runs ``develop`` plus installs ``nose`` and ``coverage``). - Added support for CI under supported Pythons using ``tox``. - Bug: Remove potential race condition on lock in face of interrupts (Issue #10). 0.5 (2012-03-24) ---------------- - Feature: added a new "invalidate()" method to allow removal of items from the cache (issue #8). - Bug: LRUCache.put() could take multiple seconds on large caches (Issue #7). - Bug: LRUCache was not thread safe (Issue #6). - Bug: LRUCache.clock would waste RAM (Issue #4). - Bug: repeated pushing of an entry would remove other cache entries (Issue #3). - Bug: LRUCache would evict entries even when not full (Issue #2). 0.4 (2011-09-04) ---------------- - Moved to GitHub (https://github.com/repoze/repoze.lru). - Added Python 3.2 support. - Python 2.4 no longer supported. - Added tox.ini for easier testing. 0.3 (2009/06/16) ---------------- - Add a thread lock around ``clear`` logic. 0.2 (2009/06/15) ---------------- - Add a ``clear`` method. 0.1 (2009/06/14) ---------------- - Initial release. Keywords: repoze lru cache Platform: UNKNOWN Classifier: Intended Audience :: Developers Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2.6 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.2 Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Programming Language :: Python :: Implementation :: PyPy Classifier: License :: Repoze Public License repoze.lru-0.6/CONTRIBUTORS.txt0000664000175000017500000001107311775314136017342 0ustar tseavertseaver00000000000000Repoze Project Contributor Agreement ==================================== The submitter agrees by adding his or her name within the section below named "Contributors" and submitting the resulting modified document to the canonical shared repository location for this software project (whether directly, as a user with "direct commit access", or via a "pull request"), he or she is signing a contract electronically. The submitter becomes a Contributor after a) he or she signs this document by adding their name beneath the "Contributors" section below, and b) the resulting document is accepted into the canonical version control repository. Treatment of Account --------------------- Contributor will not allow anyone other than the Contributor to use his or her username or source repository login to submit code to a Repoze Project source repository. Should Contributor become aware of any such use, Contributor will immediately by notifying Agendaless Consulting. Notification must be performed by sending an email to webmaster@agendaless.com. Until such notice is received, Contributor will be presumed to have taken all actions made through Contributor's account. If the Contributor has direct commit access, Agendaless Consulting will have complete control and discretion over capabilities assigned to Contributor's account, and may disable Contributor's account for any reason at any time. Legal Effect of Contribution ---------------------------- Upon submitting a change or new work to a Repoze Project source Repository (a "Contribution"), you agree to assign, and hereby do assign, a one-half interest of all right, title and interest in and to copyright and other intellectual property rights with respect to your new and original portions of the Contribution to Agendaless Consulting. You and Agendaless Consulting each agree that the other shall be free to exercise any and all exclusive rights in and to the Contribution, without accounting to one another, including without limitation, the right to license the Contribution to others under the Repoze Public License. This agreement shall run with title to the Contribution. Agendaless Consulting does not convey to you any right, title or interest in or to the Program or such portions of the Contribution that were taken from the Program. Your transmission of a submission to the Repoze Project source Repository and marks of identification concerning the Contribution itself constitute your intent to contribute and your assignment of the work in accordance with the provisions of this Agreement. License Terms ------------- Code committed to the Repoze Project source repository (Committed Code) must be governed by the Repoze Public License (http://repoze.org/LICENSE.txt, aka "the RPL") or another license acceptable to Agendaless Consulting. Until Agendaless Consulting declares in writing an acceptable license other than the RPL, only the RPL shall be used. A list of exceptions is detailed within the "Licensing Exceptions" section of this document, if one exists. Representations, Warranty, and Indemnification ---------------------------------------------- Contributor represents and warrants that the Committed Code does not violate the rights of any person or entity, and that the Contributor has legal authority to enter into this Agreement and legal authority over Contributed Code. Further, Contributor indemnifies Agendaless Consulting against violations. Cryptography ------------ Contributor understands that cryptographic code may be subject to government regulations with which Agendaless Consulting and/or entities using Committed Code must comply. Any code which contains any of the items listed below must not be checked-in until Agendaless Consulting staff has been notified and has approved such contribution in writing. - Cryptographic capabilities or features - Calls to cryptographic features - User interface elements which provide context relating to cryptography - Code which may, under casual inspection, appear to be cryptographic. Notices ------- Contributor confirms that any notices required will be included in any Committed Code. Licensing Exceptions ==================== None. List of Contributors ==================== The below-signed are contributors to a code repository that is part of the project named "repoze.lru". Each below-signed contributor has read, understand and agrees to the terms above in the section within this document entitled "Repoze Project Contributor Agreement" as of the date beside his or her name. Contributors ------------ - Tres Seaver, 2011/02/22 - Joel Bohman, 2011/08/16 - Julien Tayon, 2012/07/04 repoze.lru-0.6/LICENSE.txt0000644000175000017500000000337711531744601016467 0ustar tseavertseaver00000000000000License A copyright notice accompanies this license document that identifies the copyright holders. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions in source code must retain the accompanying copyright notice, this list of conditions, and the following disclaimer. 2. Redistributions in binary form must reproduce the accompanying copyright notice, this list of conditions, and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Names of the copyright holders must not be used to endorse or promote products derived from this software without prior written permission from the copyright holders. 4. If any files are modified, you must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. Disclaimer THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.