├── .gitignore ├── LICENSE ├── README.rst ├── docs ├── Makefile ├── make.bat └── source │ ├── conf.py │ ├── index.rst │ └── license.rst ├── setup.cfg ├── setup.py ├── supycache ├── __init__.py ├── backends │ ├── __init__.py │ ├── base.py │ ├── dict_cache.py │ └── memcached.py └── cdf.py └── tests ├── test_backends └── test_dict_cache.py └── test_supycache.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | *.egg-info/ 23 | .installed.cfg 24 | *.egg 25 | 26 | # PyInstaller 27 | # Usually these files are written by a python script from a template 28 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 29 | *.manifest 30 | *.spec 31 | 32 | # Installer logs 33 | pip-log.txt 34 | pip-delete-this-directory.txt 35 | 36 | # Unit test / coverage reports 37 | htmlcov/ 38 | .tox/ 39 | .coverage 40 | .coverage.* 41 | .cache 42 | nosetests.xml 43 | coverage.xml 44 | *,cover 45 | 46 | # Translations 47 | *.mo 48 | *.pot 49 | 50 | # Django stuff: 51 | *.log 52 | 53 | # Sphinx documentation 54 | docs/_build/ 55 | 56 | # PyBuilder 57 | target/ 58 | 59 | # local 60 | lib64 61 | include 62 | bin 63 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Steven Fernandez 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | supycache 2 | ========= 3 | 4 | Simple yet capable caching decorator for python 5 | 6 | Source code: https://github.com/lonetwin/supycache 7 | 8 | Install using pip: ``pip install supycache`` or download from https://pypi.python.org/pypi/supycache 9 | 10 | What is supycache ? 11 | ------------------- 12 | 13 | ``supycache`` is a decorator that enables caching of return values for 14 | time-consuming functions, either in memory or on a cache server such as 15 | `memcached `_ or `redis `_. 16 | 17 | The cache keys can either be *independent* or dependent (completely or 18 | *partially*) on the arguments passed to the function. 19 | 20 | This is **different** from other similar caching decorators, for 21 | instance, 22 | `functools.lru_cache `_ 23 | which is dependent on all the arguments passed to the function and 24 | requires the arguments to be hashable. 25 | 26 | If you use the default cache backend (ie: `supycache.backends.DictCache`), you 27 | can also provide an age for the cached values 28 | 29 | Here's an example of how you might use ``supycache`` 30 | 31 | .. code:: python 32 | 33 | import time 34 | import supycache 35 | 36 | @supycache.supycache(cache_key='result', max_age=5) 37 | def execute_expensive(): 38 | print 'original function called' 39 | time.sleep(15) 40 | return 42 41 | 42 | print execute_expensive() # This will take 15 seconds to execute ... 43 | original function called 44 | 42 45 | print execute_expensive() # ...not this tho', because the value is cached ... 46 | 42 47 | print supycache.default_backend.get('result') # ..keyed as `result` 48 | 42 49 | time.sleep(5) # wait for the cache to expire... 50 | execute_expensive() # This will again take 15 seconds to execute ... 51 | original function called 52 | 42 53 | print execute_expensive() # ...not this tho', because the value is re-cached ... 54 | 42 55 | 56 | 57 | Sometimes you might want to be aware of the arguments that are passed to 58 | the function: 59 | 60 | .. code:: python 61 | 62 | 63 | @supycache(cache_key='sum_of_{0}_and_{1}') # Cache the sum of x and y creating a 64 | def cached_sum(x, y): # key based on the arguments passed 65 | return x + y 66 | 67 | print cached_sum(28, 14) 68 | 42 69 | print supycache.default_backend.get('sum_of_28_and_14') 70 | 42 71 | 72 | You can also create the key based on **partial arguments** or on the 73 | ``attributes``/``items`` within the arguments. 74 | 75 | .. code:: python 76 | 77 | 78 | class User: 79 | def __init__(self, name, session_key): 80 | self.name = name 81 | self.session_key = session_key 82 | 83 | @supycache(cache_key='{user_obj.name}') # build the cache key dependent on *just* 84 | def get_username(user_obj): # the `.name` attribute 85 | time.sleep(15) 86 | return user_obj.name 87 | 88 | a = User(name='steve', session_key='0123456789') 89 | b = User(name='steve', session_key='9876543210') # same name, different session 90 | 91 | print get_username(user_obj=a) # This will take 15 seconds to execute ... 92 | steve 93 | print get_username(user_obj=a) # ...not this tho'... 94 | steve 95 | print get_username(user_obj=b) # ...and neither will this ! 96 | steve 97 | 98 | 99 | @supycache(cache_key='{choices[0]}_{menu[lunch]}') # build the cache 100 | def supersized_lunch(ignored, choices=None, menu=None): # key dependent on 101 | time.sleep(15) # partial arguments 102 | return 'You get a %s %s' % (choices[-1], menu['lunch']) 103 | 104 | menu = {'breakfast' : 'eggs', 105 | 'lunch' : 'pizza', 106 | 'dinner' : 'steak'} 107 | 108 | sizes = ['small', 'medium', 'large', 'supersize'] 109 | 110 | print supersized_lunch('ignored', choices=sizes, menu=menu) 111 | You get a supersize pizza # This will take 15 seconds to execute ... 112 | 113 | print supersized_lunch('changed', choices=sizes, menu=menu) 114 | You get a supersize pizza # ...not this tho'... 115 | 116 | If that format specification for the ``cache_key`` looks familiar, 117 | you've discovered the *secret* of supycache ! 118 | 119 | .. code:: python 120 | 121 | 122 | @supycache(backend=memcached_backend, cache_key='{0}_{kw[foo]}_{obj.x}') 123 | def custom_key_built_from_args(positional, kw=None, obj=None): 124 | # now, supycache will build the `cache_key` from the arguments passed and 125 | # use the memcached_backend instance to `set` the key with the return value 126 | # of this function 127 | return 'cached' 128 | 129 | The *secret* of supycache is quite simple -- it calls ``.format()`` on 130 | the ``cache_key/expire_key`` with the passed ``args`` and ``kwargs`` to 131 | build the actual key. 132 | 133 | However, if you'd like to have more control on the way the 134 | ``cache_key/expire_key`` are created, simply pass in a callable ! 135 | 136 | .. code:: python 137 | 138 | def extract_path(url=None, *args, **kwargs): 139 | return urlparse.urlparse(url).path 140 | 141 | @supycache(cache_key=extract_path, ignore_errors=False) 142 | def do_something_with(url): 143 | # will call `extract_path` at runtime passing `url` as parameter and 144 | # will use the returned value as the cache key. Also, don't ignore any 145 | # errors in the entire process if something fails (the default is to 146 | # ignore any caching errors and just return the result as tho' this 147 | # function was undecorated. 148 | return 'cached' 149 | 150 | do_something_with('http://www.example.com/foo/bar') 151 | 'cached' 152 | supycache.default_backend.get('/foo/bar') 153 | 'cached' 154 | 155 | 156 | The ``backend`` interface is abstracted out neatly so that backends can be 157 | swapped out without too much hassle. As long as the passed in object has a 158 | ``get()``, ``set()`` and ``delete()`` methods, it can be passed to 159 | ``supycache`` as a backend or can be set as the ``default_backend``. 160 | 161 | 162 | Right now though, this project has only the code and tests, no docs 163 | (barring some docstrings !). I'll be adding them soon. If interested take a 164 | look at the tests to see the typical usage and try it out. Feedback, bug 165 | reports and pull requests would be great ! 166 | 167 | Help required 168 | ------------- 169 | 170 | I would really appreciate any help you could offer, not just in implementation 171 | but also in validating the packaging and distribution of this module via pypi 172 | since I've not distributed any packages before. 173 | 174 | Besides that I plan on adding a few more things: 175 | 176 | * Ability to specify a ``max_age`` for all backends. 177 | * I'm not sure whether I am doing the right thing for the not the packaging 178 | of the memcached dependency. I'd like to automatically include the 179 | support for ``memcached`` or ``redis`` backends if the python memcached 180 | or redis modules are installed. 181 | * logging support 182 | 183 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source 21 | 22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 23 | 24 | help: 25 | @echo "Please use \`make ' where is one of" 26 | @echo " html to make standalone HTML files" 27 | @echo " dirhtml to make HTML files named index.html in directories" 28 | @echo " singlehtml to make a single large HTML file" 29 | @echo " pickle to make pickle files" 30 | @echo " json to make JSON files" 31 | @echo " htmlhelp to make HTML files and a HTML help project" 32 | @echo " qthelp to make HTML files and a qthelp project" 33 | @echo " devhelp to make HTML files and a Devhelp project" 34 | @echo " epub to make an epub" 35 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 36 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 37 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 38 | @echo " text to make text files" 39 | @echo " man to make manual pages" 40 | @echo " texinfo to make Texinfo files" 41 | @echo " info to make Texinfo files and run them through makeinfo" 42 | @echo " gettext to make PO message catalogs" 43 | @echo " changes to make an overview of all changed/added/deprecated items" 44 | @echo " xml to make Docutils-native XML files" 45 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 46 | @echo " linkcheck to check all external links for integrity" 47 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 48 | 49 | clean: 50 | rm -rf $(BUILDDIR)/* 51 | 52 | html: 53 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 54 | @echo 55 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 56 | 57 | dirhtml: 58 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 59 | @echo 60 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 61 | 62 | singlehtml: 63 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 64 | @echo 65 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 66 | 67 | pickle: 68 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 69 | @echo 70 | @echo "Build finished; now you can process the pickle files." 71 | 72 | json: 73 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 74 | @echo 75 | @echo "Build finished; now you can process the JSON files." 76 | 77 | htmlhelp: 78 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 79 | @echo 80 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 81 | ".hhp project file in $(BUILDDIR)/htmlhelp." 82 | 83 | qthelp: 84 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 85 | @echo 86 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 87 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 88 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/supycache.qhcp" 89 | @echo "To view the help file:" 90 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/supycache.qhc" 91 | 92 | devhelp: 93 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 94 | @echo 95 | @echo "Build finished." 96 | @echo "To view the help file:" 97 | @echo "# mkdir -p $$HOME/.local/share/devhelp/supycache" 98 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/supycache" 99 | @echo "# devhelp" 100 | 101 | epub: 102 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 103 | @echo 104 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 105 | 106 | latex: 107 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 108 | @echo 109 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 110 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 111 | "(use \`make latexpdf' here to do that automatically)." 112 | 113 | latexpdf: 114 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 115 | @echo "Running LaTeX files through pdflatex..." 116 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 117 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 118 | 119 | latexpdfja: 120 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 121 | @echo "Running LaTeX files through platex and dvipdfmx..." 122 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 123 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 124 | 125 | text: 126 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 127 | @echo 128 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 129 | 130 | man: 131 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 132 | @echo 133 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 134 | 135 | texinfo: 136 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 137 | @echo 138 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 139 | @echo "Run \`make' in that directory to run these through makeinfo" \ 140 | "(use \`make info' here to do that automatically)." 141 | 142 | info: 143 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 144 | @echo "Running Texinfo files through makeinfo..." 145 | make -C $(BUILDDIR)/texinfo info 146 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 147 | 148 | gettext: 149 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 150 | @echo 151 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 152 | 153 | changes: 154 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 155 | @echo 156 | @echo "The overview file is in $(BUILDDIR)/changes." 157 | 158 | linkcheck: 159 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 160 | @echo 161 | @echo "Link check complete; look for any errors in the above output " \ 162 | "or in $(BUILDDIR)/linkcheck/output.txt." 163 | 164 | doctest: 165 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 166 | @echo "Testing of doctests in the sources finished, look at the " \ 167 | "results in $(BUILDDIR)/doctest/output.txt." 168 | 169 | xml: 170 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 171 | @echo 172 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 173 | 174 | pseudoxml: 175 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 176 | @echo 177 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 178 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% source 10 | set I18NSPHINXOPTS=%SPHINXOPTS% source 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 31 | echo. text to make text files 32 | echo. man to make manual pages 33 | echo. texinfo to make Texinfo files 34 | echo. gettext to make PO message catalogs 35 | echo. changes to make an overview over all changed/added/deprecated items 36 | echo. xml to make Docutils-native XML files 37 | echo. pseudoxml to make pseudoxml-XML files for display purposes 38 | echo. linkcheck to check all external links for integrity 39 | echo. doctest to run all doctests embedded in the documentation if enabled 40 | goto end 41 | ) 42 | 43 | if "%1" == "clean" ( 44 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 45 | del /q /s %BUILDDIR%\* 46 | goto end 47 | ) 48 | 49 | 50 | %SPHINXBUILD% 2> nul 51 | if errorlevel 9009 ( 52 | echo. 53 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 54 | echo.installed, then set the SPHINXBUILD environment variable to point 55 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 56 | echo.may add the Sphinx directory to PATH. 57 | echo. 58 | echo.If you don't have Sphinx installed, grab it from 59 | echo.http://sphinx-doc.org/ 60 | exit /b 1 61 | ) 62 | 63 | if "%1" == "html" ( 64 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 65 | if errorlevel 1 exit /b 1 66 | echo. 67 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 68 | goto end 69 | ) 70 | 71 | if "%1" == "dirhtml" ( 72 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 73 | if errorlevel 1 exit /b 1 74 | echo. 75 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 76 | goto end 77 | ) 78 | 79 | if "%1" == "singlehtml" ( 80 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 81 | if errorlevel 1 exit /b 1 82 | echo. 83 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 84 | goto end 85 | ) 86 | 87 | if "%1" == "pickle" ( 88 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 89 | if errorlevel 1 exit /b 1 90 | echo. 91 | echo.Build finished; now you can process the pickle files. 92 | goto end 93 | ) 94 | 95 | if "%1" == "json" ( 96 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 97 | if errorlevel 1 exit /b 1 98 | echo. 99 | echo.Build finished; now you can process the JSON files. 100 | goto end 101 | ) 102 | 103 | if "%1" == "htmlhelp" ( 104 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 105 | if errorlevel 1 exit /b 1 106 | echo. 107 | echo.Build finished; now you can run HTML Help Workshop with the ^ 108 | .hhp project file in %BUILDDIR%/htmlhelp. 109 | goto end 110 | ) 111 | 112 | if "%1" == "qthelp" ( 113 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 114 | if errorlevel 1 exit /b 1 115 | echo. 116 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 117 | .qhcp project file in %BUILDDIR%/qthelp, like this: 118 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\supycache.qhcp 119 | echo.To view the help file: 120 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\supycache.ghc 121 | goto end 122 | ) 123 | 124 | if "%1" == "devhelp" ( 125 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 126 | if errorlevel 1 exit /b 1 127 | echo. 128 | echo.Build finished. 129 | goto end 130 | ) 131 | 132 | if "%1" == "epub" ( 133 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 134 | if errorlevel 1 exit /b 1 135 | echo. 136 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 137 | goto end 138 | ) 139 | 140 | if "%1" == "latex" ( 141 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 142 | if errorlevel 1 exit /b 1 143 | echo. 144 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 145 | goto end 146 | ) 147 | 148 | if "%1" == "latexpdf" ( 149 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 150 | cd %BUILDDIR%/latex 151 | make all-pdf 152 | cd %BUILDDIR%/.. 153 | echo. 154 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 155 | goto end 156 | ) 157 | 158 | if "%1" == "latexpdfja" ( 159 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 160 | cd %BUILDDIR%/latex 161 | make all-pdf-ja 162 | cd %BUILDDIR%/.. 163 | echo. 164 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 165 | goto end 166 | ) 167 | 168 | if "%1" == "text" ( 169 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 170 | if errorlevel 1 exit /b 1 171 | echo. 172 | echo.Build finished. The text files are in %BUILDDIR%/text. 173 | goto end 174 | ) 175 | 176 | if "%1" == "man" ( 177 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 178 | if errorlevel 1 exit /b 1 179 | echo. 180 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 181 | goto end 182 | ) 183 | 184 | if "%1" == "texinfo" ( 185 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 186 | if errorlevel 1 exit /b 1 187 | echo. 188 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 189 | goto end 190 | ) 191 | 192 | if "%1" == "gettext" ( 193 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 194 | if errorlevel 1 exit /b 1 195 | echo. 196 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 197 | goto end 198 | ) 199 | 200 | if "%1" == "changes" ( 201 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 202 | if errorlevel 1 exit /b 1 203 | echo. 204 | echo.The overview file is in %BUILDDIR%/changes. 205 | goto end 206 | ) 207 | 208 | if "%1" == "linkcheck" ( 209 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 210 | if errorlevel 1 exit /b 1 211 | echo. 212 | echo.Link check complete; look for any errors in the above output ^ 213 | or in %BUILDDIR%/linkcheck/output.txt. 214 | goto end 215 | ) 216 | 217 | if "%1" == "doctest" ( 218 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 219 | if errorlevel 1 exit /b 1 220 | echo. 221 | echo.Testing of doctests in the sources finished, look at the ^ 222 | results in %BUILDDIR%/doctest/output.txt. 223 | goto end 224 | ) 225 | 226 | if "%1" == "xml" ( 227 | %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml 228 | if errorlevel 1 exit /b 1 229 | echo. 230 | echo.Build finished. The XML files are in %BUILDDIR%/xml. 231 | goto end 232 | ) 233 | 234 | if "%1" == "pseudoxml" ( 235 | %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml 236 | if errorlevel 1 exit /b 1 237 | echo. 238 | echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. 239 | goto end 240 | ) 241 | 242 | :end 243 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # supycache documentation build configuration file, created by 4 | # sphinx-quickstart on Fri Apr 17 15:33:16 2015. 5 | # 6 | # This file is execfile()d with the current directory set to its 7 | # containing dir. 8 | # 9 | # Note that not all possible configuration values are present in this 10 | # autogenerated file. 11 | # 12 | # All configuration values have a default; values that are commented out 13 | # serve to show the default. 14 | 15 | import sys 16 | import os 17 | 18 | # If extensions (or modules to document with autodoc) are in another directory, 19 | # add these directories to sys.path here. If the directory is relative to the 20 | # documentation root, use os.path.abspath to make it absolute, like shown here. 21 | #sys.path.insert(0, os.path.abspath('.')) 22 | 23 | # -- General configuration ------------------------------------------------ 24 | 25 | # If your documentation needs a minimal Sphinx version, state it here. 26 | #needs_sphinx = '1.0' 27 | 28 | # Add any Sphinx extension module names here, as strings. They can be 29 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 30 | # ones. 31 | extensions = [ 32 | 'sphinx.ext.autodoc', 33 | 'sphinx.ext.intersphinx', 34 | 'sphinx.ext.coverage', 35 | 'sphinx.ext.viewcode', 36 | ] 37 | 38 | # Add any paths that contain templates here, relative to this directory. 39 | templates_path = ['_templates'] 40 | 41 | # The suffix of source filenames. 42 | source_suffix = '.rst' 43 | 44 | # The encoding of source files. 45 | #source_encoding = 'utf-8-sig' 46 | 47 | # The master toctree document. 48 | master_doc = 'index' 49 | 50 | # General information about the project. 51 | project = u'supycache' 52 | copyright = u'2015, Steven Fernandez' 53 | 54 | # The version info for the project you're documenting, acts as replacement for 55 | # |version| and |release|, also used in various other places throughout the 56 | # built documents. 57 | # 58 | # The short X.Y version. 59 | version = '0.1' 60 | # The full version, including alpha/beta/rc tags. 61 | release = '0.1' 62 | 63 | # The language for content autogenerated by Sphinx. Refer to documentation 64 | # for a list of supported languages. 65 | #language = None 66 | 67 | # There are two options for replacing |today|: either, you set today to some 68 | # non-false value, then it is used: 69 | #today = '' 70 | # Else, today_fmt is used as the format for a strftime call. 71 | #today_fmt = '%B %d, %Y' 72 | 73 | # List of patterns, relative to source directory, that match files and 74 | # directories to ignore when looking for source files. 75 | exclude_patterns = [] 76 | 77 | # The reST default role (used for this markup: `text`) to use for all 78 | # documents. 79 | #default_role = None 80 | 81 | # If true, '()' will be appended to :func: etc. cross-reference text. 82 | #add_function_parentheses = True 83 | 84 | # If true, the current module name will be prepended to all description 85 | # unit titles (such as .. function::). 86 | #add_module_names = True 87 | 88 | # If true, sectionauthor and moduleauthor directives will be shown in the 89 | # output. They are ignored by default. 90 | #show_authors = False 91 | 92 | # The name of the Pygments (syntax highlighting) style to use. 93 | pygments_style = 'sphinx' 94 | 95 | # A list of ignored prefixes for module index sorting. 96 | #modindex_common_prefix = [] 97 | 98 | # If true, keep warnings as "system message" paragraphs in the built documents. 99 | #keep_warnings = False 100 | 101 | 102 | # -- Options for HTML output ---------------------------------------------- 103 | 104 | # The theme to use for HTML and HTML Help pages. See the documentation for 105 | # a list of builtin themes. 106 | html_theme = 'default' 107 | 108 | # Theme options are theme-specific and customize the look and feel of a theme 109 | # further. For a list of options available for each theme, see the 110 | # documentation. 111 | #html_theme_options = {} 112 | 113 | # Add any paths that contain custom themes here, relative to this directory. 114 | #html_theme_path = [] 115 | 116 | # The name for this set of Sphinx documents. If None, it defaults to 117 | # " v documentation". 118 | #html_title = None 119 | 120 | # A shorter title for the navigation bar. Default is the same as html_title. 121 | #html_short_title = None 122 | 123 | # The name of an image file (relative to this directory) to place at the top 124 | # of the sidebar. 125 | #html_logo = None 126 | 127 | # The name of an image file (within the static path) to use as favicon of the 128 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 129 | # pixels large. 130 | #html_favicon = None 131 | 132 | # Add any paths that contain custom static files (such as style sheets) here, 133 | # relative to this directory. They are copied after the builtin static files, 134 | # so a file named "default.css" will overwrite the builtin "default.css". 135 | html_static_path = ['_static'] 136 | 137 | # Add any extra paths that contain custom files (such as robots.txt or 138 | # .htaccess) here, relative to this directory. These files are copied 139 | # directly to the root of the documentation. 140 | #html_extra_path = [] 141 | 142 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 143 | # using the given strftime format. 144 | #html_last_updated_fmt = '%b %d, %Y' 145 | 146 | # If true, SmartyPants will be used to convert quotes and dashes to 147 | # typographically correct entities. 148 | #html_use_smartypants = True 149 | 150 | # Custom sidebar templates, maps document names to template names. 151 | #html_sidebars = {} 152 | 153 | # Additional templates that should be rendered to pages, maps page names to 154 | # template names. 155 | #html_additional_pages = {} 156 | 157 | # If false, no module index is generated. 158 | #html_domain_indices = True 159 | 160 | # If false, no index is generated. 161 | #html_use_index = True 162 | 163 | # If true, the index is split into individual pages for each letter. 164 | #html_split_index = False 165 | 166 | # If true, links to the reST sources are added to the pages. 167 | #html_show_sourcelink = True 168 | 169 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 170 | #html_show_sphinx = True 171 | 172 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 173 | #html_show_copyright = True 174 | 175 | # If true, an OpenSearch description file will be output, and all pages will 176 | # contain a tag referring to it. The value of this option must be the 177 | # base URL from which the finished HTML is served. 178 | #html_use_opensearch = '' 179 | 180 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 181 | #html_file_suffix = None 182 | 183 | # Output file base name for HTML help builder. 184 | htmlhelp_basename = 'supycachedoc' 185 | 186 | 187 | # -- Options for LaTeX output --------------------------------------------- 188 | 189 | latex_elements = { 190 | # The paper size ('letterpaper' or 'a4paper'). 191 | #'papersize': 'letterpaper', 192 | 193 | # The font size ('10pt', '11pt' or '12pt'). 194 | #'pointsize': '10pt', 195 | 196 | # Additional stuff for the LaTeX preamble. 197 | #'preamble': '', 198 | } 199 | 200 | # Grouping the document tree into LaTeX files. List of tuples 201 | # (source start file, target name, title, 202 | # author, documentclass [howto, manual, or own class]). 203 | latex_documents = [ 204 | ('index', 'supycache.tex', u'supycache Documentation', 205 | u'Steven Fernandez', 'manual'), 206 | ] 207 | 208 | # The name of an image file (relative to this directory) to place at the top of 209 | # the title page. 210 | #latex_logo = None 211 | 212 | # For "manual" documents, if this is true, then toplevel headings are parts, 213 | # not chapters. 214 | #latex_use_parts = False 215 | 216 | # If true, show page references after internal links. 217 | #latex_show_pagerefs = False 218 | 219 | # If true, show URL addresses after external links. 220 | #latex_show_urls = False 221 | 222 | # Documents to append as an appendix to all manuals. 223 | #latex_appendices = [] 224 | 225 | # If false, no module index is generated. 226 | #latex_domain_indices = True 227 | 228 | 229 | # -- Options for manual page output --------------------------------------- 230 | 231 | # One entry per manual page. List of tuples 232 | # (source start file, name, description, authors, manual section). 233 | man_pages = [ 234 | ('index', 'supycache', u'supycache Documentation', 235 | [u'Steven Fernandez'], 1) 236 | ] 237 | 238 | # If true, show URL addresses after external links. 239 | #man_show_urls = False 240 | 241 | 242 | # -- Options for Texinfo output ------------------------------------------- 243 | 244 | # Grouping the document tree into Texinfo files. List of tuples 245 | # (source start file, target name, title, author, 246 | # dir menu entry, description, category) 247 | texinfo_documents = [ 248 | ('index', 'supycache', u'supycache Documentation', 249 | u'Steven Fernandez', 'supycache', 'One line description of project.', 250 | 'Miscellaneous'), 251 | ] 252 | 253 | # Documents to append as an appendix to all manuals. 254 | #texinfo_appendices = [] 255 | 256 | # If false, no module index is generated. 257 | #texinfo_domain_indices = True 258 | 259 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 260 | #texinfo_show_urls = 'footnote' 261 | 262 | # If true, do not generate a @detailmenu in the "Top" node's menu. 263 | #texinfo_no_detailmenu = False 264 | 265 | 266 | # Example configuration for intersphinx: refer to the Python standard library. 267 | intersphinx_mapping = {'http://docs.python.org/': None} 268 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | .. supycache documentation master file, created by 2 | sphinx-quickstart on Fri Apr 17 15:33:16 2015. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to supycache's documentation! 7 | ===================================== 8 | 9 | Contents: 10 | 11 | .. toctree:: 12 | :maxdepth: 2 13 | 14 | license 15 | 16 | 17 | 18 | Indices and tables 19 | ================== 20 | 21 | * :ref:`genindex` 22 | * :ref:`modindex` 23 | * :ref:`search` 24 | 25 | -------------------------------------------------------------------------------- /docs/source/license.rst: -------------------------------------------------------------------------------- 1 | License 2 | ======= 3 | 4 | The MIT License (MIT) 5 | 6 | Copyright (c) 2015 Steven Fernandez 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a copy 9 | of this software and associated documentation files (the "Software"), to deal 10 | in the Software without restriction, including without limitation the rights 11 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | copies of the Software, and to permit persons to whom the Software is 13 | furnished to do so, subject to the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be included in all 16 | copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | SOFTWARE. 25 | 26 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | # This flag says that the code is written to work on both Python 2 and Python 3 | # 3. If at all possible, it is good practice to do this. If you cannot, you 4 | # will need to generate wheels for each Python version that you support. 5 | universal=1 6 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """ supycache - Simple yet capable caching decorator for python 4 | 5 | Source code: https://github.com/lonetwin/supycache 6 | """ 7 | 8 | from setuptools import setup, find_packages 9 | 10 | from codecs import open 11 | from os import path 12 | 13 | here = path.abspath(path.dirname(__file__)) 14 | 15 | # Get the long description from the relevant file 16 | with open(path.join(here, 'README.rst'), encoding='utf-8') as f: 17 | long_description = f.read() 18 | 19 | setup( 20 | name = 'supycache', 21 | version = '0.3.0', 22 | description = 'Simple yet capable caching decorator for python', 23 | long_description = long_description, 24 | url = 'https://github.com/lonetwin/supycache', 25 | author = 'Steven Fernandez', 26 | author_email = 'steve@lonetwin.net', 27 | license = 'MIT', 28 | classifiers = [ 29 | 'Development Status :: 4 - Beta', 30 | 'Intended Audience :: Developers', 31 | 'Topic :: Software Development :: Libraries', 32 | 'License :: OSI Approved :: MIT License', 33 | 'Programming Language :: Python :: 2.7', 34 | 'Programming Language :: Python :: 3', 35 | 'Programming Language :: Python :: 3.2', 36 | 'Programming Language :: Python :: 3.3', 37 | 'Programming Language :: Python :: 3.4', 38 | ], 39 | keywords = 'cache, caching, memcached, redis, memoize, memoization', 40 | packages = find_packages(exclude=['contrib', 'docs', 'tests*']), 41 | ) 42 | 43 | -------------------------------------------------------------------------------- /supycache/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """ 4 | supycache - Simple yet capable caching decorator for python. 5 | 6 | https://github.com/lonetwin/supycache 7 | https://supycache.readthedocs.org/en/latest/ 8 | """ 9 | 10 | __author__ = "Steven Fernandez " 11 | __license__ = "MIT" 12 | __version__ = '0.3.0' 13 | 14 | from .backends import DictCache 15 | from .cdf import CacheDecoratorFactory 16 | 17 | default_backend = None 18 | 19 | 20 | def get_default_backend(): 21 | """Returns the currently configured `default_backend`. 22 | 23 | If not set, the `default_backend` is a `supycache.DictCache` instance. Use 24 | `supycache.set_default_backend` to change this. A `backend` is any 25 | (caching) object that has at least the `.get()`, `.set()` and `.delete()` 26 | methods. 27 | """ 28 | global default_backend 29 | if not default_backend: 30 | default_backend = DictCache() 31 | return default_backend 32 | 33 | 34 | def set_default_backend(backend): 35 | """Sets the `default_backend`. 36 | """ 37 | global default_backend 38 | default_backend = backend 39 | 40 | 41 | def supycache(**options): 42 | """Decorates a function for caching/expiring cache depending on arguments. 43 | 44 | This is the primary interface to use `supycache`. This decorator accepts 45 | the following parameters: 46 | 47 | - `backend` : The `backend` cache store to use for this cache key, if it is 48 | different than `supycache.default_backend`. 49 | 50 | - `cache_key` : Either a simple string, a format string or callable used to 51 | create the key used for caching the result of the function being 52 | decorated. This key will be resolved at run-time and would be evaluated 53 | against/with the parameters passed to the function being decorated. 54 | 55 | - `expire_key` : Either a simple string, a format string or callable used 56 | to create the key that would be expired before the decorated function 57 | is called. This key will be resolved at run-time and would be evaluated 58 | against/with the parameters pass to the function being decorated. 59 | 60 | - `ignore_errors` : A boolean to indicate whether errors in getting, 61 | setting or expiring cache should be ignored or re-raised on being 62 | caught. 63 | 64 | """ 65 | recognized_options = {'backend', 66 | 'cache_key', 67 | 'expire_key', 68 | 'ignore_errors', 69 | } 70 | 71 | if recognized_options.isdisjoint(options): 72 | raise KeyError('expecting one of %s as an argument' % 73 | ', '.join(recognized_options)) 74 | 75 | backend = options.pop('backend', get_default_backend()) 76 | 77 | def prepare_inner(function): 78 | cdf = CacheDecoratorFactory(backend, **options) 79 | return cdf(function) 80 | return prepare_inner 81 | -------------------------------------------------------------------------------- /supycache/backends/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | import warnings 4 | from .dict_cache import DictCache 5 | 6 | try: 7 | from .memcached import MemcachedCache 8 | except ImportError: 9 | warnings.warn('missing optional dependency: pylibmc, ' 10 | 'MemcachedCache backend will not be available') 11 | -------------------------------------------------------------------------------- /supycache/backends/base.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | 5 | class BaseCache(object): # pragma: no cover 6 | 7 | def __init__(self, config=None): 8 | self.config = config if config else {} 9 | 10 | def get(self): 11 | raise NotImplementedError() 12 | 13 | def set(self, key, value): 14 | raise NotImplementedError() 15 | 16 | def delete(self, key): 17 | raise NotImplementedError() 18 | 19 | def clear(self): 20 | raise NotImplementedError() 21 | -------------------------------------------------------------------------------- /supycache/backends/dict_cache.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | import time 4 | from collections import defaultdict 5 | from .base import BaseCache 6 | 7 | 8 | class DictCache(BaseCache): 9 | 10 | def __init__(self, config=None): 11 | super(DictCache, self).__init__(config) 12 | self._data = None 13 | 14 | @property 15 | def data(self): 16 | if self._data is None: 17 | self._data = defaultdict(lambda: ('', 0)) \ 18 | if self.config.get('max_age') else defaultdict(str) 19 | return self._data 20 | 21 | def get(self, key): 22 | if self.config.get('max_age'): 23 | value, expiry_time = self.data[key] 24 | if time.time() > expiry_time: 25 | self.delete(key) 26 | raise KeyError(key) 27 | else: 28 | value = self.data[key] 29 | return value 30 | 31 | def set(self, key, value): 32 | max_age = self.config.get('max_age') 33 | if max_age: 34 | _, expiry_time = self.data[key] 35 | if expiry_time == 0: 36 | # ie: if we got the default value 37 | expiry_time = time.time() + max_age 38 | self.data[key] = (value, expiry_time) 39 | else: 40 | self.data[key] = value 41 | 42 | def delete(self, key): 43 | del(self.data[key]) 44 | 45 | def clear(self): 46 | return self.data.clear() 47 | -------------------------------------------------------------------------------- /supycache/backends/memcached.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | import pylibmc 4 | from .base import BaseCache 5 | 6 | 7 | class MemcachedCache(pylibmc.Client, BaseCache): 8 | 9 | clear = pylibmc.Client.flush_all 10 | -------------------------------------------------------------------------------- /supycache/cdf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | from functools import wraps 4 | 5 | 6 | class CacheDecoratorFactory: 7 | 8 | def __init__(self, backend, cache_key='', expire_key='', **other_kwargs): 9 | self._backend = backend 10 | self._backend.config.update(other_kwargs) 11 | 12 | if cache_key: 13 | self.key = cache_key 14 | self._wrapped = self._caching_wrapper 15 | 16 | if expire_key: 17 | self.key = expire_key 18 | self._wrapped = self._expiry_wrapper 19 | 20 | self.ignore_errors = other_kwargs.get('ignore_errors', True) 21 | 22 | def __call__(self, func): 23 | return self._wrapped(func) 24 | 25 | def _expiry_wrapper(self, func): 26 | @wraps(func) 27 | def cache_deleter(*args, **kwargs): 28 | key = self.key(*args, **kwargs) if callable(self.key) \ 29 | else self.key.format(*args, **kwargs) 30 | try: 31 | self._backend.delete(key) 32 | except: 33 | if not self.ignore_errors: 34 | raise 35 | 36 | return func(*args, **kwargs) 37 | return cache_deleter 38 | 39 | def _caching_wrapper(self, func): 40 | @wraps(func) 41 | def cache_setter(*args, **kwargs): 42 | result = None 43 | key = self.key(*args, **kwargs) if callable(self.key) \ 44 | else self.key.format(*args, **kwargs) 45 | try: 46 | result = self._backend.get(key) 47 | except: 48 | if not self.ignore_errors: 49 | raise 50 | 51 | if not result: 52 | result = func(*args, **kwargs) 53 | try: 54 | self._backend.set(key, result) 55 | except: 56 | if not self.ignore_errors: 57 | raise 58 | 59 | return result 60 | return cache_setter 61 | -------------------------------------------------------------------------------- /tests/test_backends/test_dict_cache.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import unittest 5 | import supycache 6 | 7 | class TestDictCache(unittest.TestCase): 8 | """ Test the DictCache backend 9 | """ 10 | 11 | def setUp(self): 12 | self.cache = supycache.backends.DictCache() 13 | supycache.set_default_backend(self.cache) 14 | 15 | def tearDown(self): 16 | self.cache.clear() 17 | 18 | def test_init(self): 19 | """Testing DictCache constructor""" 20 | self.assertTrue(hasattr(self.cache, 'set')) 21 | self.assertTrue(hasattr(self.cache, 'get')) 22 | self.assertTrue(hasattr(self.cache, 'clear')) 23 | 24 | def test_methods(self): 25 | """Testing DictCache methods""" 26 | self.cache.set('key', 'value') 27 | self.assertTrue(self.cache.get('key') == 'value') 28 | self.assertTrue(bool(self.cache.get('non-existent')) == False) 29 | self.assertTrue(self.cache.clear() == None) 30 | self.assertTrue(len(self.cache.data) == 0) 31 | 32 | 33 | class TestExpiringDictCache(unittest.TestCase): 34 | """ Test the DictCache backend with max_age parameter 35 | """ 36 | 37 | def setUp(self): 38 | self.cache = supycache.backends.DictCache() 39 | supycache.set_default_backend(self.cache) 40 | 41 | def tearDown(self): 42 | self.cache.clear() 43 | 44 | def test_init(self): 45 | """Testing expiring DictCache constructor""" 46 | @supycache.supycache(cache_key='simple_key', ignore_errors=False, max_age=10) 47 | def simple_function(): 48 | return 'simple_value' 49 | 50 | self.assertTrue(self.cache.data['DoesNotExist'] == ('', 0)) 51 | 52 | def test_get_without_ignoring_errors(self): 53 | """Testing expiring DictCache get() method without ignoring errors""" 54 | 55 | @supycache.supycache(cache_key='simple_key', ignore_errors=False, max_age=10) 56 | def simple_function(): 57 | return 'simple_value' 58 | 59 | with self.assertRaises(KeyError) as context: 60 | simple_function() 61 | 62 | 63 | def test_get_with_ignoring_errors(self): 64 | """Testing expiring DictCache get() method with ignoring errors""" 65 | 66 | @supycache.supycache(cache_key='simple_key', max_age=10) 67 | def simple_function(): 68 | return 'simple_value' 69 | 70 | self.assertTrue(simple_function() == 'simple_value') 71 | self.assertTrue(self.cache.get('simple_key') == 'simple_value') 72 | self.assertTrue(self.cache.data['simple_key'][1] != 0) 73 | -------------------------------------------------------------------------------- /tests/test_supycache.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import unittest 5 | import supycache 6 | 7 | class TestDictCache(unittest.TestCase): 8 | """ Test the DictCache backend 9 | """ 10 | 11 | def setUp(self): 12 | from supycache.backends import DictCache 13 | self.cache = DictCache() 14 | 15 | def tearDown(self): 16 | self.cache.clear() 17 | 18 | def test_init(self): 19 | """Testing DictCache constructor""" 20 | self.assertTrue(hasattr(self.cache, 'set')) 21 | self.assertTrue(hasattr(self.cache, 'get')) 22 | self.assertTrue(hasattr(self.cache, 'clear')) 23 | 24 | def test_methods(self): 25 | """Testing DictCache methods""" 26 | self.cache.set('key', 'value') 27 | self.assertTrue(self.cache.get('key') == 'value') 28 | self.assertTrue(bool(self.cache.get('non-existent')) == False) 29 | self.assertTrue(self.cache.clear() == None) 30 | self.assertTrue(len(self.cache._data) == 0) 31 | 32 | 33 | def test_get_set_default_backend(): 34 | """Testing get/set default_backend""" 35 | reload(supycache) # - re-init 36 | from supycache.backends import DictCache 37 | assert(supycache.default_backend == None) 38 | assert(isinstance(supycache.get_default_backend(), DictCache)) 39 | assert(isinstance(supycache.default_backend, DictCache)) 40 | new_backend = DictCache() 41 | supycache.set_default_backend(new_backend) 42 | assert(supycache.get_default_backend() is new_backend) 43 | assert(supycache.default_backend is new_backend) 44 | 45 | class TestCacheDecorators(unittest.TestCase): 46 | """ Test the CacheDecorators 47 | """ 48 | 49 | def setUp(self): 50 | from supycache.backends import DictCache 51 | self.backend = DictCache() 52 | supycache.default_backend = self.backend 53 | 54 | def tearDown(self): 55 | self.backend.clear() 56 | 57 | def test_missing_options(self): 58 | """ missing option 59 | """ 60 | with self.assertRaises(KeyError) as context: 61 | @supycache.supycache() 62 | def simple_function(): 63 | return 'dummy' 64 | 65 | self.assertTrue('expecting one of' in context.exception.message) 66 | 67 | 68 | def test_do_not_ignore_errors(self): 69 | """ do not ignore errors 70 | """ 71 | from supycache.backends import DictCache 72 | 73 | class TestException(Exception): 74 | pass 75 | 76 | class DummyBackend: 77 | config = {} 78 | 79 | def raise_exc(self, *args): 80 | """dummy function to raise exception, used later""" 81 | raise TestException() 82 | 83 | backend = DummyBackend() 84 | supycache.set_default_backend(backend) # override setUp() 85 | 86 | @supycache.supycache(cache_key='simple_key', ignore_errors=False) 87 | def simple_function(): 88 | return 'simple_value' 89 | 90 | # - test exception in get() with ignore_errors=False 91 | with self.assertRaises(TestException) as context: 92 | backend.get = backend.raise_exc 93 | simple_function() 94 | 95 | # - test exception in set() with ignore_errors=False 96 | with self.assertRaises(TestException) as context: 97 | backend.get = lambda key: None 98 | backend.set = backend.raise_exc 99 | simple_function() 100 | 101 | # - test exception in delete() with ignore_errors=False 102 | with self.assertRaises(TestException) as context: 103 | backend.delete = backend.raise_exc 104 | simple_function() 105 | 106 | 107 | def test_decorator_for_cache_key_cache_miss(self): 108 | """ caching a simple key on a cache miss 109 | """ 110 | @supycache.supycache(cache_key='simple_key') 111 | def simple_function(): 112 | return 'simple_value' 113 | 114 | simple_function() 115 | self.assertTrue(self.backend.get('simple_key') == 'simple_value') 116 | 117 | def test_decorator_for_cache_key_cached(self): 118 | """ caching a simple key and return from cache 119 | """ 120 | 121 | @supycache.supycache(cache_key='simple_key') 122 | def simple_function(call_count): 123 | return '%d:cached_value' % call_count 124 | 125 | simple_function(1) 126 | self.assertTrue(self.backend.get('simple_key') == '1:cached_value') 127 | 128 | simple_function(2) 129 | self.assertTrue(self.backend.get('simple_key') == '1:cached_value') 130 | 131 | simple_function(3) 132 | self.assertTrue(self.backend.get('simple_key') == '1:cached_value') 133 | 134 | 135 | def test_decorator_for_cache_key_positional_args(self): 136 | """ caching a key built from positional arguments and returning from cache 137 | """ 138 | 139 | @supycache.supycache(cache_key='{0}') 140 | def simple_function(x, y): 141 | return '%d:cached_value' % y 142 | 143 | simple_function('key', 1) 144 | self.assertTrue(self.backend.get('key') == '1:cached_value') 145 | 146 | simple_function('key', 2) 147 | self.assertTrue(self.backend.get('key') == '1:cached_value') 148 | 149 | simple_function('new_key', 3) 150 | self.assertTrue(self.backend.get('new_key') == '3:cached_value') 151 | 152 | 153 | def test_decorator_for_cache_key_keyword_args(self): 154 | """ caching a key built from keyword arguments and returning from cache 155 | """ 156 | 157 | @supycache.supycache(cache_key='{somearg}') 158 | def simple_function(call_count, somearg): 159 | return '%d:cached_value' % call_count 160 | 161 | simple_function(1, somearg='key') 162 | self.assertTrue(self.backend.get('key') == '1:cached_value') 163 | 164 | simple_function(2, somearg='key') 165 | self.assertTrue(self.backend.get('key') == '1:cached_value') 166 | 167 | simple_function(3, somearg='new_key') 168 | self.assertTrue(self.backend.get('new_key') == '3:cached_value') 169 | 170 | 171 | def test_decorator_for_cache_key_multi_args_simple(self): 172 | """ caching a key built from both positional and keyword arguments and returning from cache 173 | """ 174 | 175 | @supycache.supycache(cache_key='{0}_{keyword}') 176 | def simple_function(positional, call_count, keyword=''): 177 | return '%d:cached_value' % call_count 178 | 179 | simple_function('some', 1, keyword='key') 180 | self.assertTrue(self.backend.get('some_key') == '1:cached_value') 181 | 182 | simple_function('some', 2, keyword='key') 183 | self.assertTrue(self.backend.get('some_key') == '1:cached_value') 184 | 185 | simple_function('some_other', 1, keyword='new_key') 186 | self.assertTrue(self.backend.get('some_other_new_key') == '1:cached_value') 187 | 188 | 189 | def test_decorator_for_cache_key_multi_args_complex_list(self): 190 | """ caching a key built from elements of a list passed as an argument 191 | """ 192 | 193 | @supycache.supycache(cache_key='{0}_{arglist[0]}') 194 | def simple_function(positional, call_count, arglist=None): 195 | return '%d:cached_value' % call_count 196 | 197 | simple_function('some', 1, arglist=['key', 'dummy']) 198 | self.assertTrue(self.backend.get('some_key') == '1:cached_value') 199 | 200 | simple_function('some', 2, arglist=['key', 'changed']) 201 | self.assertTrue(self.backend.get('some_key') == '1:cached_value') 202 | 203 | simple_function('some_other', 1, arglist=['new_key', 'dummy']) 204 | self.assertTrue(self.backend.get('some_other_new_key') == '1:cached_value') 205 | 206 | simple_function('yet_another', 1, arglist=['new_key', 'dummy']) 207 | self.assertTrue(self.backend.get('yet_another_new_key') == '1:cached_value') 208 | 209 | 210 | def test_decorator_for_cache_key_multi_args_complex_dict(self): 211 | """ caching a key built from elements of a dict passed as an argument 212 | """ 213 | 214 | @supycache.supycache(cache_key='{0}_{argdict[lookup]}') 215 | def simple_function(positional, call_count, argdict=None): 216 | return '%d:cached_value' % call_count 217 | 218 | simple_function('some', 1, argdict={'lookup' : 'key'}) 219 | self.assertTrue(self.backend.get('some_key') == '1:cached_value') 220 | 221 | simple_function('some', 2, argdict={'lookup' : 'key'}) 222 | self.assertTrue(self.backend.get('some_key') == '1:cached_value') 223 | 224 | simple_function('some_other', 1, argdict={'lookup' : 'new_key'}) 225 | self.assertTrue(self.backend.get('some_other_new_key') == '1:cached_value') 226 | 227 | 228 | def test_decorator_for_cache_key_multi_args_complex_object(self): 229 | """ caching a key built from attributes of an object passed as an argument 230 | """ 231 | 232 | @supycache.supycache(cache_key='{0}_{arg.name}') 233 | def simple_function(positional, call_count, arg): 234 | return '%d:cached_value' % call_count 235 | 236 | class DummyArg: 237 | def __init__(self, value): 238 | self.name = value 239 | 240 | simple_function('some', 1, arg=DummyArg('key')) 241 | self.assertTrue(self.backend.get('some_key') == '1:cached_value') 242 | 243 | simple_function('some', 2, arg=DummyArg('key')) 244 | self.assertTrue(self.backend.get('some_key') == '1:cached_value') 245 | 246 | simple_function('some_other', 1, arg=DummyArg('new_key')) 247 | self.assertTrue(self.backend.get('some_other_new_key') == '1:cached_value') 248 | 249 | 250 | def test_decorator_for_expire_key_with_cached_key(self): 251 | """ expire a simple key which exists in cache 252 | """ 253 | @supycache.supycache(cache_key='simple_key') 254 | def simple_function(): 255 | return 'simple_value' 256 | 257 | @supycache.supycache(expire_key='simple_key') 258 | def simple_expiry(): 259 | return 'ignored_value' 260 | 261 | simple_function() 262 | self.assertTrue(self.backend.get('simple_key') == 'simple_value') 263 | 264 | simple_expiry() 265 | self.assertFalse(bool(self.backend.get('simple_key'))) 266 | 267 | 268 | def test_decorator_for_expire_key_with_non_cached_key(self): 269 | """ expire a simple key with does not exist in cache 270 | """ 271 | 272 | @supycache.supycache(cache_key='simple_key') 273 | def simple_function(): 274 | return 'simple_value' 275 | 276 | @supycache.supycache(expire_key='simple_key') 277 | def simple_expiry(): 278 | return 'ignored_value' 279 | 280 | simple_function() 281 | self.assertTrue(self.backend.get('simple_key') == 'simple_value') 282 | 283 | simple_expiry() 284 | self.assertFalse(bool(self.backend.get('simple_key'))) 285 | self.assertFalse(bool(self.backend.get('simple_key'))) 286 | 287 | 288 | def test_decorator_for_expire_key_positional_args(self): 289 | """ expire a key built from positional arguments 290 | """ 291 | 292 | @supycache.supycache(cache_key='{0}') 293 | def simple_function(x, y): 294 | return 'cached_value' 295 | 296 | @supycache.supycache(expire_key='{0}') 297 | def simple_expiry(x, y): 298 | return 'ignored_value' 299 | 300 | simple_function('simple_key', 1) 301 | self.assertTrue(self.backend.get('simple_key') == 'cached_value') 302 | 303 | simple_expiry('simple_key', 'dummy') 304 | self.assertFalse(bool(self.backend.get('simple_key'))) 305 | --------------------------------------------------------------------------------