├── .gitignore ├── .travis.yml ├── CHANGES ├── CONTRIBUTORS ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README ├── docs ├── Makefile ├── _static │ └── flask-cache.png ├── _themes │ ├── README │ ├── flask │ │ ├── static │ │ │ └── flasky.css_t │ │ └── theme.conf │ ├── flask_small │ │ ├── layout.html │ │ ├── static │ │ │ └── flasky.css_t │ │ └── theme.conf │ └── flask_theme_support.py ├── conf.py ├── index.rst └── make.bat ├── examples ├── hello.cfg └── hello.py ├── flask_cache ├── __init__.py ├── _compat.py ├── backends.py └── jinja2ext.py ├── requirements.txt ├── setup.cfg ├── setup.py ├── test_cache.py ├── test_template.html └── tox.ini /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[co] 2 | .*.swp 3 | .tox 4 | docs/_build/* 5 | Flask_Cache.egg-info/* 6 | build/* 7 | dist/ 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.7" 4 | - "3.3" 5 | install: 6 | - pip install . --use-mirrors 7 | - pip install coverage --use-mirrors 8 | - pip install redis --use-mirrors 9 | - "if [[ $TRAVIS_PYTHON_VERSION == 2.7* ]]; then pip install pylibmc --use-mirrors; fi" 10 | - "if [[ $TRAVIS_PYTHON_VERSION == 3.3* ]]; then pip install python3-memcached --use-mirrors; fi" 11 | script: nosetests --with-coverage --cover-package=flask_cache 12 | services: 13 | - memcached 14 | - redis-server 15 | -------------------------------------------------------------------------------- /CHANGES: -------------------------------------------------------------------------------- 1 | Changelog 2 | --------- 3 | 4 | Version 0.13 2014-04-21 5 | ``````````````````````` 6 | 7 | - Port to Python >= 3.3 (requiring Python 2.6/2.7 for 2.x). 8 | - Fixed bug with using per-memoize timeouts greater than the default timeout 9 | - Added better support for per-instance memoization. 10 | - Various bug fixes 11 | 12 | Version 0.12 2013-04-29 13 | ``````````````````````` 14 | 15 | - Changes jinja2 cache templates to use stable predictable keys. Previously 16 | the key for a cache tag included the line number of the template, which made 17 | it difficult to predict what the key would be outside of the application. 18 | - Adds config variable `CACHE_NO_NULL_WARNING` to silence warning messages 19 | when using 'null' cache as part of testing. 20 | - Adds passthrough to clear entire cache backend. 21 | 22 | Version 0.11.1 2013-04-7 23 | ```````````````````````` 24 | 25 | - Bugfix for using memoize on instance methods. 26 | The previous key was id(self), the new key is repr(self) 27 | 28 | Version 0.11 2013-03-23 29 | ``````````````````````` 30 | 31 | - Fail gracefully in production if cache backend raises an exception. 32 | - Support for redis DB number 33 | - Jinja2 templatetag cache now concats all args together into a single key 34 | instead of treating each arg as a separate key name. 35 | - Added delete memcache version hash function 36 | - Support for multiple cache objects on a single app again. 37 | - Added SpreadSASLMemcached, if a value is greater than the memcached threshold 38 | which defaults to 1MB, this splits the value across multiple keys. 39 | - Added support to use URL to connect to redis. 40 | 41 | Version 0.10.1 2013-01-13 42 | ````````````````````````` 43 | 44 | - Added warning message when using cache type of 'null' 45 | - Changed imports to relative instead of absolute for AppEngine compatibility 46 | 47 | Version 0.10.0 2013-01-05 48 | ````````````````````````` 49 | 50 | - Added `saslmemcached` backend to support Memcached behind SASL authentication. 51 | - Fixes a bug with memoize when the number of args != number of kwargs 52 | 53 | Version 0.9.2 2012-11-18 54 | ```````````````````````` 55 | 56 | - Bugfix with default kwargs 57 | 58 | Version 0.9.1 2012-11-16 59 | ```````````````````````` 60 | 61 | - Fixes broken memoized on functions that use default kwargs 62 | 63 | Version 0.9.0 2012-10-14 64 | ```````````````````````` 65 | 66 | - Fixes memoization to work on methods. 67 | 68 | Version 0.8.0 2012-09-30 69 | ```````````````````````` 70 | 71 | - Migrated to the new flask extension naming convention of flask_cache instead of flaskext.cache 72 | - Removed unnecessary dependencies in setup.py file. 73 | - Documentation updates 74 | 75 | Version 0.7.0 2012-08-25 76 | ```````````````````````` 77 | 78 | - Allows multiple cache objects to be instantiated with different configuration values. 79 | 80 | Version 0.6.0 2012-08-12 81 | ```````````````````````` 82 | 83 | - Memoization is now safer for multiple applications using the same backing store. 84 | - Removed the explicit set of NullCache if the Flask app is set testing=True 85 | - Swapped Conditional order for key_prefix 86 | 87 | Version 0.5.0 2012-02-03 88 | ```````````````````````` 89 | 90 | - Deleting memoized functions now properly functions in production 91 | environments where multiple instances of the application are running. 92 | - get_memoized_names and get_memoized_keys have been removed. 93 | - Added ``make_name`` to memoize, make_name is an optional callable that can be passed 94 | to memoize to modify the cache_key that gets generated. 95 | - Added ``unless`` to memoize, this is the same as the unless parameter in ``cached`` 96 | - memoization now converts all kwargs to positional arguments, this is so that 97 | when a function is called multiple ways, it would evaluate to the same cache_key 98 | 99 | Version 0.4.0 2011-12-11 100 | ```````````````````````` 101 | 102 | - Added attributes for uncached, make_cache_key, cache_timeout 103 | to the decorated functions. 104 | 105 | Version 0.3.4 2011-09-10 106 | ```````````````````````` 107 | 108 | - UTF-8 encoding of cache key 109 | - key_prefix argument of the cached decorator now supports callables. 110 | 111 | Version 0.3.3 2011-06-03 112 | ```````````````````````` 113 | 114 | Uses base64 for memoize caching. This fixes rare issues where the cache_key 115 | was either a tuple or larger than the caching backend would be able to 116 | support. 117 | 118 | Adds support for deleting memoized caches optionally based on function parameters. 119 | 120 | Python 2.5 compatibility, plus bugfix with string.format. 121 | 122 | Added the ability to retrieve memoized function names or cache keys. 123 | 124 | Version 0.3.2 125 | ````````````` 126 | 127 | Bugfix release. Fixes a bug that would cause an exception if no 128 | ``CACHE_TYPE`` was supplied. 129 | 130 | Version 0.3.1 131 | ````````````` 132 | 133 | Pypi egg fix. 134 | 135 | Version 0.3 136 | ``````````` 137 | 138 | - CACHE_TYPE changed. Now one of ['null', 'simple', 'memcached', 139 | 'gaememcached', 'filesystem'], or an import string to a function that will 140 | instantiate a cache object. This allows Flask-Cache to be much more 141 | extensible and configurable. 142 | 143 | Version 0.2 144 | ``````````` 145 | - CACHE_TYPE now uses an import_string. 146 | - Added CACHE_OPTIONS and CACHE_ARGS configuration values. 147 | - Added delete_memoized 148 | 149 | Version 0.1 150 | ``````````` 151 | 152 | - Initial public release 153 | -------------------------------------------------------------------------------- /CONTRIBUTORS: -------------------------------------------------------------------------------- 1 | dag (Dag Odenhall) 2 | sean- 3 | singingwolfboy (David Baumgold) 4 | nocko (Shawn Nock) 5 | mimamu 6 | dahlia (Hong Minhee) 7 | jab (Joshua Bronson) 8 | kennethreitz (Kenneth Reitz) 9 | Thomas Waldmann 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010 by Thadeus Burgess. 2 | 3 | Some rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are 7 | met: 8 | 9 | * Redistributions of source code must retain the above copyright 10 | notice, this list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above 13 | copyright notice, this list of conditions and the following 14 | disclaimer in the documentation and/or other materials provided 15 | with the distribution. 16 | 17 | * The names of the contributors may not be used to endorse or 18 | promote products derived from this software without specific 19 | prior written permission. 20 | 21 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 22 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 23 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 24 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 25 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 26 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 27 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 28 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 29 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 30 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 31 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 32 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE CHANGES *.py 2 | include test_template.html 3 | recursive-include docs * 4 | recursive-exclude docs *.pyc 5 | recursive-exclude docs *.pyo 6 | prune docs/_build 7 | prune docs/_themes/.git 8 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | test: 2 | python setup.py test 3 | 4 | pypi: 5 | python setup.py sdist bdist_egg upload 6 | python setup.py build_sphinx 7 | python setup.py upload_sphinx 8 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | Flask-Cache 2 | 3 | Adds easy cache support to Flask. 4 | -------------------------------------------------------------------------------- /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 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 14 | 15 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest 16 | 17 | help: 18 | @echo "Please use \`make ' where is one of" 19 | @echo " html to make standalone HTML files" 20 | @echo " dirhtml to make HTML files named index.html in directories" 21 | @echo " singlehtml to make a single large HTML file" 22 | @echo " pickle to make pickle files" 23 | @echo " json to make JSON files" 24 | @echo " htmlhelp to make HTML files and a HTML help project" 25 | @echo " qthelp to make HTML files and a qthelp project" 26 | @echo " devhelp to make HTML files and a Devhelp project" 27 | @echo " epub to make an epub" 28 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 29 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 30 | @echo " text to make text files" 31 | @echo " man to make manual pages" 32 | @echo " changes to make an overview of all changed/added/deprecated items" 33 | @echo " linkcheck to check all external links for integrity" 34 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 35 | 36 | clean: 37 | -rm -rf $(BUILDDIR)/* 38 | 39 | html: 40 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 41 | @echo 42 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 43 | 44 | dirhtml: 45 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 46 | @echo 47 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 48 | 49 | singlehtml: 50 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 51 | @echo 52 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 53 | 54 | pickle: 55 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 56 | @echo 57 | @echo "Build finished; now you can process the pickle files." 58 | 59 | json: 60 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 61 | @echo 62 | @echo "Build finished; now you can process the JSON files." 63 | 64 | htmlhelp: 65 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 66 | @echo 67 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 68 | ".hhp project file in $(BUILDDIR)/htmlhelp." 69 | 70 | qthelp: 71 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 72 | @echo 73 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 74 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 75 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Flask-SQLAlchemy.qhcp" 76 | @echo "To view the help file:" 77 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Flask-SQLAlchemy.qhc" 78 | 79 | devhelp: 80 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 81 | @echo 82 | @echo "Build finished." 83 | @echo "To view the help file:" 84 | @echo "# mkdir -p $$HOME/.local/share/devhelp/Flask-SQLAlchemy" 85 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Flask-SQLAlchemy" 86 | @echo "# devhelp" 87 | 88 | epub: 89 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 90 | @echo 91 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 92 | 93 | latex: 94 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 95 | @echo 96 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 97 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 98 | "(use \`make latexpdf' here to do that automatically)." 99 | 100 | latexpdf: 101 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 102 | @echo "Running LaTeX files through pdflatex..." 103 | make -C $(BUILDDIR)/latex all-pdf 104 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 105 | 106 | text: 107 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 108 | @echo 109 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 110 | 111 | man: 112 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 113 | @echo 114 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 115 | 116 | changes: 117 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 118 | @echo 119 | @echo "The overview file is in $(BUILDDIR)/changes." 120 | 121 | linkcheck: 122 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 123 | @echo 124 | @echo "Link check complete; look for any errors in the above output " \ 125 | "or in $(BUILDDIR)/linkcheck/output.txt." 126 | 127 | doctest: 128 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 129 | @echo "Testing of doctests in the sources finished, look at the " \ 130 | "results in $(BUILDDIR)/doctest/output.txt." 131 | -------------------------------------------------------------------------------- /docs/_static/flask-cache.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thadeusb/flask-cache/1c60076b6d4c2df0ac1de54c59e63b4f780cecbc/docs/_static/flask-cache.png -------------------------------------------------------------------------------- /docs/_themes/README: -------------------------------------------------------------------------------- 1 | Flask Sphinx Styles 2 | =================== 3 | 4 | This repository contains sphinx styles for Flask and Flask related 5 | projects. To use this style in your Sphinx documentation, follow 6 | this guide: 7 | 8 | 1. put this folder as _themes into your docs folder. Alternatively 9 | you can also use git submodules to check out the contents there. 10 | 2. add this to your conf.py: 11 | 12 | sys.path.append(os.path.abspath('_themes')) 13 | html_theme_path = ['_themes'] 14 | html_theme = 'flask' 15 | 16 | The following themes exist: 17 | 18 | - 'flask' - the standard flask documentation theme for large 19 | projects 20 | - 'flask_small' - small one-page theme. Intended to be used by 21 | very small addon libraries for flask. 22 | 23 | The following options exist for the flask_small theme: 24 | 25 | [options] 26 | index_logo = '' filename of a picture in _static 27 | to be used as replacement for the 28 | h1 in the index.rst file. 29 | index_logo_height = 120px height of the index logo 30 | github_fork = '' repository name on github for the 31 | "fork me" badge 32 | -------------------------------------------------------------------------------- /docs/_themes/flask/static/flasky.css_t: -------------------------------------------------------------------------------- 1 | /* 2 | * flasky.css_t 3 | * ~~~~~~~~~~~~ 4 | * 5 | * Sphinx stylesheet -- flasky theme based on nature theme. 6 | * 7 | * :copyright: Copyright 2007-2010 by the Sphinx team, see AUTHORS. 8 | * :license: BSD, see LICENSE for details. 9 | * 10 | */ 11 | 12 | @import url("basic.css"); 13 | 14 | /* -- page layout ----------------------------------------------------------- */ 15 | 16 | body { 17 | font-family: 'Georgia', serif; 18 | font-size: 17px; 19 | background-color: #ddd; 20 | color: #000; 21 | margin: 0; 22 | padding: 0; 23 | } 24 | 25 | div.document { 26 | background: #fafafa; 27 | } 28 | 29 | div.documentwrapper { 30 | float: left; 31 | width: 100%; 32 | } 33 | 34 | div.bodywrapper { 35 | margin: 0 0 0 230px; 36 | } 37 | 38 | hr { 39 | border: 1px solid #B1B4B6; 40 | } 41 | 42 | div.body { 43 | background-color: #ffffff; 44 | color: #3E4349; 45 | padding: 0 30px 30px 30px; 46 | min-height: 34em; 47 | } 48 | 49 | img.floatingflask { 50 | padding: 0 0 10px 10px; 51 | float: right; 52 | } 53 | 54 | div.footer { 55 | position: absolute; 56 | right: 0; 57 | margin-top: -70px; 58 | text-align: right; 59 | color: #888; 60 | padding: 10px; 61 | font-size: 14px; 62 | } 63 | 64 | div.footer a { 65 | color: #888; 66 | text-decoration: underline; 67 | } 68 | 69 | div.related { 70 | line-height: 32px; 71 | color: #888; 72 | } 73 | 74 | div.related ul { 75 | padding: 0 0 0 10px; 76 | } 77 | 78 | div.related a { 79 | color: #444; 80 | } 81 | 82 | div.sphinxsidebar { 83 | font-size: 14px; 84 | line-height: 1.5; 85 | } 86 | 87 | div.sphinxsidebarwrapper { 88 | padding: 0 20px; 89 | } 90 | 91 | div.sphinxsidebarwrapper p.logo { 92 | padding: 20px 0 10px 0; 93 | margin: 0; 94 | text-align: center; 95 | } 96 | 97 | div.sphinxsidebar h3, 98 | div.sphinxsidebar h4 { 99 | font-family: 'Garamond', 'Georgia', serif; 100 | color: #222; 101 | font-size: 24px; 102 | font-weight: normal; 103 | margin: 20px 0 5px 0; 104 | padding: 0; 105 | } 106 | 107 | div.sphinxsidebar h4 { 108 | font-size: 20px; 109 | } 110 | 111 | div.sphinxsidebar h3 a { 112 | color: #444; 113 | } 114 | 115 | div.sphinxsidebar p { 116 | color: #555; 117 | margin: 10px 0; 118 | } 119 | 120 | div.sphinxsidebar ul { 121 | margin: 10px 0; 122 | padding: 0; 123 | color: #000; 124 | } 125 | 126 | div.sphinxsidebar a { 127 | color: #444; 128 | text-decoration: none; 129 | } 130 | 131 | div.sphinxsidebar a:hover { 132 | text-decoration: underline; 133 | } 134 | 135 | div.sphinxsidebar input { 136 | border: 1px solid #ccc; 137 | font-family: 'Georgia', serif; 138 | font-size: 1em; 139 | } 140 | 141 | /* -- body styles ----------------------------------------------------------- */ 142 | 143 | a { 144 | color: #004B6B; 145 | text-decoration: underline; 146 | } 147 | 148 | a:hover { 149 | color: #6D4100; 150 | text-decoration: underline; 151 | } 152 | 153 | div.body { 154 | padding-bottom: 40px; /* saved for footer */ 155 | } 156 | 157 | div.body h1, 158 | div.body h2, 159 | div.body h3, 160 | div.body h4, 161 | div.body h5, 162 | div.body h6 { 163 | font-family: 'Garamond', 'Georgia', serif; 164 | font-weight: normal; 165 | margin: 30px 0px 10px 0px; 166 | padding: 0; 167 | } 168 | 169 | div.body h1 { margin-top: 0; padding-top: 20px; font-size: 240%; } 170 | div.body h2 { font-size: 180%; } 171 | div.body h3 { font-size: 150%; } 172 | div.body h4 { font-size: 130%; } 173 | div.body h5 { font-size: 100%; } 174 | div.body h6 { font-size: 100%; } 175 | 176 | a.headerlink { 177 | color: white; 178 | padding: 0 4px; 179 | text-decoration: none; 180 | } 181 | 182 | a.headerlink:hover { 183 | color: #444; 184 | background: #eaeaea; 185 | } 186 | 187 | div.body p, div.body dd, div.body li { 188 | line-height: 1.4em; 189 | } 190 | 191 | div.admonition { 192 | background: #fafafa; 193 | margin: 20px -30px; 194 | padding: 10px 30px; 195 | border-top: 1px solid #ccc; 196 | border-bottom: 1px solid #ccc; 197 | } 198 | 199 | div.admonition p.admonition-title { 200 | font-family: 'Garamond', 'Georgia', serif; 201 | font-weight: normal; 202 | font-size: 24px; 203 | margin: 0 0 10px 0; 204 | padding: 0; 205 | line-height: 1; 206 | } 207 | 208 | div.admonition p.last { 209 | margin-bottom: 0; 210 | } 211 | 212 | div.highlight{ 213 | background-color: white; 214 | } 215 | 216 | dt:target, .highlight { 217 | background: #FAF3E8; 218 | } 219 | 220 | div.note { 221 | background-color: #eee; 222 | border: 1px solid #ccc; 223 | } 224 | 225 | div.seealso { 226 | background-color: #ffc; 227 | border: 1px solid #ff6; 228 | } 229 | 230 | div.topic { 231 | background-color: #eee; 232 | } 233 | 234 | div.warning { 235 | background-color: #ffe4e4; 236 | border: 1px solid #f66; 237 | } 238 | 239 | p.admonition-title { 240 | display: inline; 241 | } 242 | 243 | p.admonition-title:after { 244 | content: ":"; 245 | } 246 | 247 | pre, tt { 248 | font-family: 'Consolas', 'Menlo', 'Deja Vu Sans Mono', 'Bitstream Vera Sans Mono', monospace; 249 | font-size: 0.9em; 250 | } 251 | 252 | img.screenshot { 253 | } 254 | 255 | tt.descname, tt.descclassname { 256 | font-size: 0.95em; 257 | } 258 | 259 | tt.descname { 260 | padding-right: 0.08em; 261 | } 262 | 263 | img.screenshot { 264 | -moz-box-shadow: 2px 2px 4px #eee; 265 | -webkit-box-shadow: 2px 2px 4px #eee; 266 | box-shadow: 2px 2px 4px #eee; 267 | } 268 | 269 | table.docutils { 270 | border: 1px solid #888; 271 | -moz-box-shadow: 2px 2px 4px #eee; 272 | -webkit-box-shadow: 2px 2px 4px #eee; 273 | box-shadow: 2px 2px 4px #eee; 274 | } 275 | 276 | table.docutils td, table.docutils th { 277 | border: 1px solid #888; 278 | padding: 0.25em 0.7em; 279 | } 280 | 281 | table.field-list, table.footnote { 282 | border: none; 283 | -moz-box-shadow: none; 284 | -webkit-box-shadow: none; 285 | box-shadow: none; 286 | } 287 | 288 | table.footnote { 289 | margin: 15px 0; 290 | width: 100%; 291 | border: 1px solid #eee; 292 | } 293 | 294 | table.field-list th { 295 | padding: 0 0.8em 0 0; 296 | } 297 | 298 | table.field-list td { 299 | padding: 0; 300 | } 301 | 302 | table.footnote td { 303 | padding: 0.5em; 304 | } 305 | 306 | dl { 307 | margin: 0; 308 | padding: 0; 309 | } 310 | 311 | dl dd { 312 | margin-left: 30px; 313 | } 314 | 315 | pre { 316 | background: #eee; 317 | padding: 7px 30px; 318 | margin: 15px -30px; 319 | line-height: 1.3em; 320 | } 321 | 322 | dl pre { 323 | margin-left: -60px; 324 | padding-left: 60px; 325 | } 326 | 327 | dl dl pre { 328 | margin-left: -90px; 329 | padding-left: 90px; 330 | } 331 | 332 | tt { 333 | background-color: #ecf0f3; 334 | color: #222; 335 | /* padding: 1px 2px; */ 336 | } 337 | 338 | tt.xref, a tt { 339 | background-color: #FBFBFB; 340 | text-decoration: none!important; 341 | } 342 | 343 | a:hover tt { 344 | background: #EEE; 345 | } 346 | -------------------------------------------------------------------------------- /docs/_themes/flask/theme.conf: -------------------------------------------------------------------------------- 1 | [theme] 2 | inherit = basic 3 | stylesheet = flasky.css 4 | pygments_style = flask_theme_support.FlaskyStyle 5 | -------------------------------------------------------------------------------- /docs/_themes/flask_small/layout.html: -------------------------------------------------------------------------------- 1 | {% extends "basic/layout.html" %} 2 | {% block header %} 3 | {{ super() }} 4 | {% if pagename == 'index' %} 5 |
6 | {% endif %} 7 | {% endblock %} 8 | {% block footer %} 9 | {% if pagename == 'index' %} 10 |
11 | {% endif %} 12 | {% endblock %} 13 | {# do not display relbars #} 14 | {% block relbar1 %}{% endblock %} 15 | {% block relbar2 %} 16 | {% if theme_github_fork %} 17 | Fork me on GitHub 19 | {% endif %} 20 | {% endblock %} 21 | {% block sidebar1 %}{% endblock %} 22 | {% block sidebar2 %}{% endblock %} 23 | -------------------------------------------------------------------------------- /docs/_themes/flask_small/static/flasky.css_t: -------------------------------------------------------------------------------- 1 | /* 2 | * flasky.css_t 3 | * ~~~~~~~~~~~~ 4 | * 5 | * Sphinx stylesheet -- flasky theme based on nature theme. 6 | * 7 | * :copyright: Copyright 2007-2010 by the Sphinx team, see AUTHORS. 8 | * :license: BSD, see LICENSE for details. 9 | * 10 | */ 11 | 12 | @import url("basic.css"); 13 | 14 | /* -- page layout ----------------------------------------------------------- */ 15 | 16 | body { 17 | font-family: 'Georgia', serif; 18 | font-size: 17px; 19 | color: #000; 20 | background: white; 21 | margin: 0; 22 | padding: 0; 23 | } 24 | 25 | div.documentwrapper { 26 | float: left; 27 | width: 100%; 28 | } 29 | 30 | div.bodywrapper { 31 | margin: 40px auto 0 auto; 32 | width: 700px; 33 | } 34 | 35 | hr { 36 | border: 1px solid #B1B4B6; 37 | } 38 | 39 | div.body { 40 | background-color: #ffffff; 41 | color: #3E4349; 42 | padding: 0 30px 30px 30px; 43 | } 44 | 45 | img.floatingflask { 46 | padding: 0 0 10px 10px; 47 | float: right; 48 | } 49 | 50 | div.footer { 51 | text-align: right; 52 | color: #888; 53 | padding: 10px; 54 | font-size: 14px; 55 | width: 650px; 56 | margin: 0 auto 40px auto; 57 | } 58 | 59 | div.footer a { 60 | color: #888; 61 | text-decoration: underline; 62 | } 63 | 64 | div.related { 65 | line-height: 32px; 66 | color: #888; 67 | } 68 | 69 | div.related ul { 70 | padding: 0 0 0 10px; 71 | } 72 | 73 | div.related a { 74 | color: #444; 75 | } 76 | 77 | /* -- body styles ----------------------------------------------------------- */ 78 | 79 | a { 80 | color: #004B6B; 81 | text-decoration: underline; 82 | } 83 | 84 | a:hover { 85 | color: #6D4100; 86 | text-decoration: underline; 87 | } 88 | 89 | div.body { 90 | padding-bottom: 40px; /* saved for footer */ 91 | } 92 | 93 | div.body h1, 94 | div.body h2, 95 | div.body h3, 96 | div.body h4, 97 | div.body h5, 98 | div.body h6 { 99 | font-family: 'Garamond', 'Georgia', serif; 100 | font-weight: normal; 101 | margin: 30px 0px 10px 0px; 102 | padding: 0; 103 | } 104 | 105 | {% if theme_index_logo %} 106 | div.indexwrapper h1 { 107 | text-indent: -999999px; 108 | background: url({{ theme_index_logo }}) no-repeat center center; 109 | height: {{ theme_index_logo_height }}; 110 | } 111 | {% endif %} 112 | 113 | div.body h2 { font-size: 180%; } 114 | div.body h3 { font-size: 150%; } 115 | div.body h4 { font-size: 130%; } 116 | div.body h5 { font-size: 100%; } 117 | div.body h6 { font-size: 100%; } 118 | 119 | a.headerlink { 120 | color: white; 121 | padding: 0 4px; 122 | text-decoration: none; 123 | } 124 | 125 | a.headerlink:hover { 126 | color: #444; 127 | background: #eaeaea; 128 | } 129 | 130 | div.body p, div.body dd, div.body li { 131 | line-height: 1.4em; 132 | } 133 | 134 | div.admonition { 135 | background: #fafafa; 136 | margin: 20px -30px; 137 | padding: 10px 30px; 138 | border-top: 1px solid #ccc; 139 | border-bottom: 1px solid #ccc; 140 | } 141 | 142 | div.admonition p.admonition-title { 143 | font-family: 'Garamond', 'Georgia', serif; 144 | font-weight: normal; 145 | font-size: 24px; 146 | margin: 0 0 10px 0; 147 | padding: 0; 148 | line-height: 1; 149 | } 150 | 151 | div.admonition p.last { 152 | margin-bottom: 0; 153 | } 154 | 155 | div.highlight{ 156 | background-color: white; 157 | } 158 | 159 | dt:target, .highlight { 160 | background: #FAF3E8; 161 | } 162 | 163 | div.note { 164 | background-color: #eee; 165 | border: 1px solid #ccc; 166 | } 167 | 168 | div.seealso { 169 | background-color: #ffc; 170 | border: 1px solid #ff6; 171 | } 172 | 173 | div.topic { 174 | background-color: #eee; 175 | } 176 | 177 | div.warning { 178 | background-color: #ffe4e4; 179 | border: 1px solid #f66; 180 | } 181 | 182 | p.admonition-title { 183 | display: inline; 184 | } 185 | 186 | p.admonition-title:after { 187 | content: ":"; 188 | } 189 | 190 | pre, tt { 191 | font-family: 'Consolas', 'Menlo', 'Deja Vu Sans Mono', 'Bitstream Vera Sans Mono', monospace; 192 | font-size: 0.85em; 193 | } 194 | 195 | img.screenshot { 196 | } 197 | 198 | tt.descname, tt.descclassname { 199 | font-size: 0.95em; 200 | } 201 | 202 | tt.descname { 203 | padding-right: 0.08em; 204 | } 205 | 206 | img.screenshot { 207 | -moz-box-shadow: 2px 2px 4px #eee; 208 | -webkit-box-shadow: 2px 2px 4px #eee; 209 | box-shadow: 2px 2px 4px #eee; 210 | } 211 | 212 | table.docutils { 213 | border: 1px solid #888; 214 | -moz-box-shadow: 2px 2px 4px #eee; 215 | -webkit-box-shadow: 2px 2px 4px #eee; 216 | box-shadow: 2px 2px 4px #eee; 217 | } 218 | 219 | table.docutils td, table.docutils th { 220 | border: 1px solid #888; 221 | padding: 0.25em 0.7em; 222 | } 223 | 224 | table.field-list, table.footnote { 225 | border: none; 226 | -moz-box-shadow: none; 227 | -webkit-box-shadow: none; 228 | box-shadow: none; 229 | } 230 | 231 | table.footnote { 232 | margin: 15px 0; 233 | width: 100%; 234 | border: 1px solid #eee; 235 | } 236 | 237 | table.field-list th { 238 | padding: 0 0.8em 0 0; 239 | } 240 | 241 | table.field-list td { 242 | padding: 0; 243 | } 244 | 245 | table.footnote td { 246 | padding: 0.5em; 247 | } 248 | 249 | dl { 250 | margin: 0; 251 | padding: 0; 252 | } 253 | 254 | dl dd { 255 | margin-left: 30px; 256 | } 257 | 258 | pre { 259 | padding: 0; 260 | margin: 15px -30px; 261 | padding: 8px; 262 | line-height: 1.3em; 263 | padding: 7px 30px; 264 | background: #eee; 265 | border-radius: 2px; 266 | -moz-border-radius: 2px; 267 | -webkit-border-radius: 2px; 268 | } 269 | 270 | dl pre { 271 | margin-left: -60px; 272 | padding-left: 60px; 273 | } 274 | 275 | tt { 276 | background-color: #ecf0f3; 277 | color: #222; 278 | /* padding: 1px 2px; */ 279 | } 280 | 281 | tt.xref, a tt { 282 | background-color: #FBFBFB; 283 | } 284 | 285 | a:hover tt { 286 | background: #EEE; 287 | } 288 | -------------------------------------------------------------------------------- /docs/_themes/flask_small/theme.conf: -------------------------------------------------------------------------------- 1 | [theme] 2 | inherit = basic 3 | stylesheet = flasky.css 4 | nosidebar = true 5 | pygments_style = flask_theme_support.FlaskyStyle 6 | 7 | [options] 8 | index_logo = '' 9 | index_logo_height = 120px 10 | github_fork = '' 11 | -------------------------------------------------------------------------------- /docs/_themes/flask_theme_support.py: -------------------------------------------------------------------------------- 1 | # flasky extensions. flasky pygments style based on tango style 2 | from pygments.style import Style 3 | from pygments.token import Keyword, Name, Comment, String, Error, \ 4 | Number, Operator, Generic, Whitespace, Punctuation, Other, Literal 5 | 6 | 7 | class FlaskyStyle(Style): 8 | background_color = "#f8f8f8" 9 | default_style = "" 10 | 11 | styles = { 12 | # No corresponding class for the following: 13 | #Text: "", # class: '' 14 | Whitespace: "underline #f8f8f8", # class: 'w' 15 | Error: "#a40000 border:#ef2929", # class: 'err' 16 | Other: "#000000", # class 'x' 17 | 18 | Comment: "italic #8f5902", # class: 'c' 19 | Comment.Preproc: "noitalic", # class: 'cp' 20 | 21 | Keyword: "bold #004461", # class: 'k' 22 | Keyword.Constant: "bold #004461", # class: 'kc' 23 | Keyword.Declaration: "bold #004461", # class: 'kd' 24 | Keyword.Namespace: "bold #004461", # class: 'kn' 25 | Keyword.Pseudo: "bold #004461", # class: 'kp' 26 | Keyword.Reserved: "bold #004461", # class: 'kr' 27 | Keyword.Type: "bold #004461", # class: 'kt' 28 | 29 | Operator: "#582800", # class: 'o' 30 | Operator.Word: "bold #004461", # class: 'ow' - like keywords 31 | 32 | Punctuation: "bold #000000", # class: 'p' 33 | 34 | # because special names such as Name.Class, Name.Function, etc. 35 | # are not recognized as such later in the parsing, we choose them 36 | # to look the same as ordinary variables. 37 | Name: "#000000", # class: 'n' 38 | Name.Attribute: "#c4a000", # class: 'na' - to be revised 39 | Name.Builtin: "#004461", # class: 'nb' 40 | Name.Builtin.Pseudo: "#3465a4", # class: 'bp' 41 | Name.Class: "#000000", # class: 'nc' - to be revised 42 | Name.Constant: "#000000", # class: 'no' - to be revised 43 | Name.Decorator: "#888", # class: 'nd' - to be revised 44 | Name.Entity: "#ce5c00", # class: 'ni' 45 | Name.Exception: "bold #cc0000", # class: 'ne' 46 | Name.Function: "#000000", # class: 'nf' 47 | Name.Property: "#000000", # class: 'py' 48 | Name.Label: "#f57900", # class: 'nl' 49 | Name.Namespace: "#000000", # class: 'nn' - to be revised 50 | Name.Other: "#000000", # class: 'nx' 51 | Name.Tag: "bold #004461", # class: 'nt' - like a keyword 52 | Name.Variable: "#000000", # class: 'nv' - to be revised 53 | Name.Variable.Class: "#000000", # class: 'vc' - to be revised 54 | Name.Variable.Global: "#000000", # class: 'vg' - to be revised 55 | Name.Variable.Instance: "#000000", # class: 'vi' - to be revised 56 | 57 | Number: "#990000", # class: 'm' 58 | 59 | Literal: "#000000", # class: 'l' 60 | Literal.Date: "#000000", # class: 'ld' 61 | 62 | String: "#4e9a06", # class: 's' 63 | String.Backtick: "#4e9a06", # class: 'sb' 64 | String.Char: "#4e9a06", # class: 'sc' 65 | String.Doc: "italic #8f5902", # class: 'sd' - like a comment 66 | String.Double: "#4e9a06", # class: 's2' 67 | String.Escape: "#4e9a06", # class: 'se' 68 | String.Heredoc: "#4e9a06", # class: 'sh' 69 | String.Interpol: "#4e9a06", # class: 'si' 70 | String.Other: "#4e9a06", # class: 'sx' 71 | String.Regex: "#4e9a06", # class: 'sr' 72 | String.Single: "#4e9a06", # class: 's1' 73 | String.Symbol: "#4e9a06", # class: 'ss' 74 | 75 | Generic: "#000000", # class: 'g' 76 | Generic.Deleted: "#a40000", # class: 'gd' 77 | Generic.Emph: "italic #000000", # class: 'ge' 78 | Generic.Error: "#ef2929", # class: 'gr' 79 | Generic.Heading: "bold #000080", # class: 'gh' 80 | Generic.Inserted: "#00A000", # class: 'gi' 81 | Generic.Output: "#888", # class: 'go' 82 | Generic.Prompt: "#745334", # class: 'gp' 83 | Generic.Strong: "bold #000000", # class: 'gs' 84 | Generic.Subheading: "bold #800080", # class: 'gu' 85 | Generic.Traceback: "bold #a40000", # class: 'gt' 86 | } 87 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Flask-Cache documentation build configuration file, created by 4 | # sphinx-quickstart on Mon Aug 30 02:31:57 2010. 5 | # 6 | # This file is execfile()d with the current directory set to its containing dir. 7 | # 8 | # Note that not all possible configuration values are present in this 9 | # autogenerated file. 10 | # 11 | # All configuration values have a default; values that are commented out 12 | # serve to show the default. 13 | 14 | import sys, os 15 | 16 | # If extensions (or modules to document with autodoc) are in another directory, 17 | # add these directories to sys.path here. If the directory is relative to the 18 | # documentation root, use os.path.abspath to make it absolute, like shown here. 19 | sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) 20 | sys.path.append(os.path.abspath('_themes')) 21 | 22 | from flask_cache import __version__, __versionfull__ 23 | 24 | # -- General configuration ----------------------------------------------------- 25 | 26 | # If your documentation needs a minimal Sphinx version, state it here. 27 | #needs_sphinx = '1.0' 28 | 29 | # Add any Sphinx extension module names here, as strings. They can be extensions 30 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 31 | extensions = ['sphinx.ext.autodoc', 'sphinx.ext.intersphinx'] 32 | 33 | # Add any paths that contain templates here, relative to this directory. 34 | templates_path = ['_templates'] 35 | 36 | # The suffix of source filenames. 37 | source_suffix = '.rst' 38 | 39 | # The encoding of source files. 40 | #source_encoding = 'utf-8-sig' 41 | 42 | # The master toctree document. 43 | master_doc = 'index' 44 | 45 | # General information about the project. 46 | project = u'Flask-Cache' 47 | copyright = u'2010, Thadeus Burgess' 48 | 49 | # The version info for the project you're documenting, acts as replacement for 50 | # |version| and |release|, also used in various other places throughout the 51 | # built documents. 52 | # 53 | # The short X.Y version. 54 | version = __version__ 55 | # The full version, including alpha/beta/rc tags. 56 | release = __versionfull__ 57 | 58 | # The language for content autogenerated by Sphinx. Refer to documentation 59 | # for a list of supported languages. 60 | #language = None 61 | 62 | # There are two options for replacing |today|: either, you set today to some 63 | # non-false value, then it is used: 64 | #today = '' 65 | # Else, today_fmt is used as the format for a strftime call. 66 | #today_fmt = '%B %d, %Y' 67 | 68 | # List of patterns, relative to source directory, that match files and 69 | # directories to ignore when looking for source files. 70 | exclude_patterns = ['_build'] 71 | 72 | # The reST default role (used for this markup: `text`) to use for all documents. 73 | #default_role = None 74 | 75 | # If true, '()' will be appended to :func: etc. cross-reference text. 76 | #add_function_parentheses = True 77 | 78 | # If true, the current module name will be prepended to all description 79 | # unit titles (such as .. function::). 80 | #add_module_names = True 81 | 82 | # If true, sectionauthor and moduleauthor directives will be shown in the 83 | # output. They are ignored by default. 84 | #show_authors = False 85 | 86 | # The name of the Pygments (syntax highlighting) style to use. 87 | #pygments_style = 'sphinx' 88 | 89 | # A list of ignored prefixes for module index sorting. 90 | #modindex_common_prefix = [] 91 | 92 | 93 | # -- Options for HTML output --------------------------------------------------- 94 | 95 | # The theme to use for HTML and HTML Help pages. See the documentation for 96 | # a list of builtin themes. 97 | html_theme = 'flask_small' 98 | 99 | # Theme options are theme-specific and customize the look and feel of a theme 100 | # further. For a list of options available for each theme, see the 101 | # documentation. 102 | html_theme_options = { 103 | 'index_logo': 'flask-cache.png', 104 | 'github_fork': 'thadeusb/flask-cache' 105 | } 106 | 107 | # Add any paths that contain custom themes here, relative to this directory. 108 | html_theme_path = ['_themes'] 109 | 110 | # The name for this set of Sphinx documents. If None, it defaults to 111 | # " v documentation". 112 | #html_title = None 113 | 114 | # A shorter title for the navigation bar. Default is the same as html_title. 115 | #html_short_title = None 116 | 117 | # The name of an image file (relative to this directory) to place at the top 118 | # of the sidebar. 119 | #html_logo = None 120 | 121 | # The name of an image file (within the static path) to use as favicon of the 122 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 123 | # pixels large. 124 | #html_favicon = None 125 | 126 | # Add any paths that contain custom static files (such as style sheets) here, 127 | # relative to this directory. They are copied after the builtin static files, 128 | # so a file named "default.css" will overwrite the builtin "default.css". 129 | html_static_path = ['_static'] 130 | 131 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 132 | # using the given strftime format. 133 | #html_last_updated_fmt = '%b %d, %Y' 134 | 135 | # If true, SmartyPants will be used to convert quotes and dashes to 136 | # typographically correct entities. 137 | #html_use_smartypants = True 138 | 139 | # Custom sidebar templates, maps document names to template names. 140 | #html_sidebars = {} 141 | 142 | # Additional templates that should be rendered to pages, maps page names to 143 | # template names. 144 | #html_additional_pages = {} 145 | 146 | # If false, no module index is generated. 147 | #html_domain_indices = True 148 | 149 | # If false, no index is generated. 150 | #html_use_index = True 151 | 152 | # If true, the index is split into individual pages for each letter. 153 | #html_split_index = False 154 | 155 | # If true, links to the reST sources are added to the pages. 156 | #html_show_sourcelink = True 157 | 158 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 159 | #html_show_sphinx = True 160 | 161 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 162 | #html_show_copyright = True 163 | 164 | # If true, an OpenSearch description file will be output, and all pages will 165 | # contain a tag referring to it. The value of this option must be the 166 | # base URL from which the finished HTML is served. 167 | #html_use_opensearch = '' 168 | 169 | # If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml"). 170 | #html_file_suffix = '' 171 | 172 | # Output file base name for HTML help builder. 173 | htmlhelp_basename = 'Flask-Cachedoc' 174 | 175 | 176 | # -- Options for LaTeX output -------------------------------------------------- 177 | 178 | # The paper size ('letter' or 'a4'). 179 | #latex_paper_size = 'letter' 180 | 181 | # The font size ('10pt', '11pt' or '12pt'). 182 | #latex_font_size = '10pt' 183 | 184 | # Grouping the document tree into LaTeX files. List of tuples 185 | # (source start file, target name, title, author, documentclass [howto/manual]). 186 | latex_documents = [ 187 | ('index', 'Flask-Cache.tex', u'Flask-Cache Documentation', 188 | u'Thadeus Burgess', 'manual'), 189 | ] 190 | 191 | # The name of an image file (relative to this directory) to place at the top of 192 | # the title page. 193 | #latex_logo = None 194 | 195 | # For "manual" documents, if this is true, then toplevel headings are parts, 196 | # not chapters. 197 | #latex_use_parts = False 198 | 199 | # If true, show page references after internal links. 200 | #latex_show_pagerefs = False 201 | 202 | # If true, show URL addresses after external links. 203 | #latex_show_urls = False 204 | 205 | # Additional stuff for the LaTeX preamble. 206 | #latex_preamble = '' 207 | 208 | # Documents to append as an appendix to all manuals. 209 | #latex_appendices = [] 210 | 211 | # If false, no module index is generated. 212 | #latex_domain_indices = True 213 | 214 | 215 | # -- Options for manual page output -------------------------------------------- 216 | 217 | # One entry per manual page. List of tuples 218 | # (source start file, name, description, authors, manual section). 219 | man_pages = [ 220 | ('index', 'flask-cache', u'Flask-Cache Documentation', 221 | [u'Thadeus Burgess'], 1) 222 | ] 223 | 224 | 225 | # Example configuration for intersphinx: refer to the Python standard library. 226 | intersphinx_mapping = {'http://docs.python.org/': None, 227 | 'http://flask.pocoo.org/docs/': None, 228 | 'http://werkzeug.pocoo.org/documentation/dev/': None,} 229 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | Flask-Cache 2 | ================ 3 | 4 | .. module:: flask.ext.cache 5 | 6 | Installation 7 | ------------ 8 | 9 | Install the extension with one of the following commands:: 10 | 11 | $ easy_install Flask-Cache 12 | 13 | or alternatively if you have pip installed:: 14 | 15 | $ pip install Flask-Cache 16 | 17 | Set Up 18 | ------ 19 | 20 | Cache is managed through a ``Cache`` instance:: 21 | 22 | from flask import Flask 23 | from flask.ext.cache import Cache 24 | 25 | app = Flask(__name__) 26 | # Check Configuring Flask-Cache section for more details 27 | cache = Cache(app,config={'CACHE_TYPE': 'simple'}) 28 | 29 | You may also set up your ``Cache`` instance later at configuration time using 30 | **init_app** method:: 31 | 32 | cache = Cache(config={'CACHE_TYPE': 'simple'}) 33 | 34 | app = Flask(__name__) 35 | cache.init_app(app) 36 | 37 | You may also provide an alternate configuration dictionary, useful if there will 38 | be multiple ``Cache`` instances each with a different backend.:: 39 | 40 | #: Method A: During instantiation of class 41 | cache = Cache(config={'CACHE_TYPE': 'simple'}) 42 | #: Method B: During init_app call 43 | cache.init_app(app, config={'CACHE_TYPE': 'simple'}) 44 | 45 | .. versionadded:: 0.7 46 | 47 | Caching View Functions 48 | ---------------------- 49 | 50 | To cache view functions you will use the :meth:`~Cache.cached` decorator. 51 | This decorator will use request.path by default for the cache_key.:: 52 | 53 | @cache.cached(timeout=50) 54 | def index(): 55 | return render_template('index.html') 56 | 57 | The cached decorator has another optional argument called ``unless``. This 58 | argument accepts a callable that returns True or False. If ``unless`` returns 59 | ``True`` then it will bypass the caching mechanism entirely. 60 | 61 | Caching Other Functions 62 | ----------------------- 63 | 64 | Using the same ``@cached`` decorator you are able to cache the result of other 65 | non-view related functions. The only stipulation is that you replace the 66 | ``key_prefix``, otherwise it will use the request.path cache_key.:: 67 | 68 | @cache.cached(timeout=50, key_prefix='all_comments') 69 | def get_all_comments(): 70 | comments = do_serious_dbio() 71 | return [x.author for x in comments] 72 | 73 | cached_comments = get_all_comments() 74 | 75 | Memoization 76 | ----------- 77 | 78 | See :meth:`~Cache.memoize` 79 | 80 | In memoization, the functions arguments are also included into the cache_key. 81 | 82 | .. note:: 83 | 84 | With functions that do not receive arguments, :meth:`~Cache.cached` and 85 | :meth:`~Cache.memoize` are effectively the same. 86 | 87 | Memoize is also designed for methods, since it will take into account 88 | the `identity `_. of the 89 | 'self' or 'cls' argument as part of the cache key. 90 | 91 | The theory behind memoization is that if you have a function you need 92 | to call several times in one request, it would only be calculated the first 93 | time that function is called with those arguments. For example, an sqlalchemy 94 | object that determines if a user has a role. You might need to call this 95 | function many times during a single request. To keep from hitting the database 96 | every time this information is needed you might do something like the following:: 97 | 98 | class Person(db.Model): 99 | @cache.memoize(50) 100 | def has_membership(self, role_id): 101 | return Group.query.filter_by(user=self, role_id=role_id).count() >= 1 102 | 103 | 104 | .. warning:: 105 | 106 | Using mutable objects (classes, etc) as part of the cache key can become 107 | tricky. It is suggested to not pass in an object instance into a memoized 108 | function. However, the memoize does perform a repr() on the passed in arguments 109 | so that if the object has a __repr__ function that returns a uniquely 110 | identifying string for that object, that will be used as part of the 111 | cache key. 112 | 113 | For example, an sqlalchemy person object that returns the database id as 114 | part of the unique identifier.:: 115 | 116 | class Person(db.Model): 117 | def __repr__(self): 118 | return "%s(%s)" % (self.__class__.__name__, self.id) 119 | 120 | Deleting memoize cache 121 | `````````````````````` 122 | 123 | .. versionadded:: 0.2 124 | 125 | You might need to delete the cache on a per-function bases. Using the above 126 | example, lets say you change the users permissions and assign them to a role, 127 | but now you need to re-calculate if they have certain memberships or not. 128 | You can do this with the :meth:`~Cache.delete_memoized` function.:: 129 | 130 | cache.delete_memoized('user_has_membership') 131 | 132 | .. note:: 133 | 134 | If only the function name is given as parameter, all the memoized versions 135 | of it will be invalidated. However, you can delete specific cache by 136 | providing the same parameter values as when caching. In following 137 | example only the ``user``-role cache is deleted: 138 | 139 | .. code-block:: python 140 | 141 | user_has_membership('demo', 'admin') 142 | user_has_membership('demo', 'user') 143 | 144 | cache.delete_memoized('user_has_membership', 'demo', 'user') 145 | 146 | Caching Jinja2 Snippets 147 | ----------------------- 148 | 149 | Usage:: 150 | 151 | {% cache [timeout [,[key1, [key2, ...]]]] %} 152 | ... 153 | {% endcache %} 154 | 155 | By default the value of "path to template file" + "block start line" is used as cache key. 156 | Also key name can be set manually. Keys are concated together into a single string. 157 | that can be used to avoid the same block evaluating in different templates. 158 | 159 | Set the timeout to None for no timeout, but with custom keys:: 160 | 161 | {% cache None "key" %}... 162 | 163 | Set timeout to "del" to delete cached value:: 164 | 165 | {% cache 'del' %}... 166 | 167 | If keys are provided, you may easily generate the template fragment key and 168 | delete it from outside of the template context:: 169 | 170 | from flask.ext.cache import make_template_fragment_key 171 | key = make_template_fragment_key("key1", vary_on=["key2", "key3"]) 172 | cache.delete(key) 173 | 174 | Example:: 175 | 176 | Considering we have render_form_field and render_submit macroses. 177 | {% cache 60*5 %} 178 |
179 |
180 | {% render_form_field form.username %} 181 | {% render_submit %} 182 |
183 |
184 | {% endcache %} 185 | 186 | Clearing Cache 187 | -------------- 188 | 189 | See :meth:`~Cache.clear`. 190 | 191 | Here's an example script to empty your application's cache: 192 | 193 | .. code-block:: python 194 | 195 | from flask.ext.cache import Cache 196 | 197 | from yourapp import app, your_cache_config 198 | 199 | cache = Cache() 200 | 201 | 202 | def main(): 203 | cache.init_app(app, config=your_cache_config) 204 | 205 | with app.app_context(): 206 | cache.clear() 207 | 208 | if __name__ == '__main__': 209 | main() 210 | 211 | 212 | .. warning:: 213 | 214 | Some backend implementations do not support completely clearing the cache. 215 | Also, if you're not using a key prefix, some implementations (e.g. Redis) 216 | will flush the whole database. Make sure you're not storing any other 217 | data in your caching database. 218 | 219 | Configuring Flask-Cache 220 | ----------------------- 221 | 222 | The following configuration values exist for Flask-Cache: 223 | 224 | .. tabularcolumns:: |p{6.5cm}|p{8.5cm}| 225 | 226 | 227 | =============================== ================================================================== 228 | ``CACHE_TYPE`` Specifies which type of caching object to 229 | use. This is an import string that will 230 | be imported and instantiated. It is 231 | assumed that the import object is a 232 | function that will return a cache 233 | object that adheres to the werkzeug cache 234 | API. 235 | 236 | For werkzeug.contrib.cache objects, you 237 | do not need to specify the entire 238 | import string, just one of the following 239 | names. 240 | 241 | Built-in cache types: 242 | 243 | * **null**: NullCache (default) 244 | * **simple**: SimpleCache 245 | * **memcached**: MemcachedCache (pylibmc or memcache required) 246 | * **gaememcached**: GAEMemcachedCache 247 | * **redis**: RedisCache (Werkzeug 0.7 required) 248 | * **filesystem**: FileSystemCache 249 | * **saslmemcached**: SASLMemcachedCache (pylibmc required) 250 | 251 | ``CACHE_NO_NULL_WARNING`` Silents the warning message when using 252 | cache type of 'null'. 253 | ``CACHE_ARGS`` Optional list to unpack and pass during 254 | the cache class instantiation. 255 | ``CACHE_OPTIONS`` Optional dictionary to pass during the 256 | cache class instantiation. 257 | ``CACHE_DEFAULT_TIMEOUT`` The default timeout that is used if no 258 | timeout is specified. Unit of time is 259 | seconds. 260 | ``CACHE_THRESHOLD`` The maximum number of items the cache 261 | will store before it starts deleting 262 | some. Used only for SimpleCache and 263 | FileSystemCache 264 | ``CACHE_KEY_PREFIX`` A prefix that is added before all keys. 265 | This makes it possible to use the same 266 | memcached server for different apps. 267 | Used only for RedisCache, MemcachedCache and 268 | GAEMemcachedCache. 269 | ``CACHE_MEMCACHED_SERVERS`` A list or a tuple of server addresses. 270 | Used only for MemcachedCache 271 | ``CACHE_MEMCACHED_USERNAME`` Username for SASL authentication with memcached. 272 | Used only for SASLMemcachedCache 273 | ``CACHE_MEMCACHED_PASSWORD`` Password for SASL authentication with memcached. 274 | Used only for SASLMemcachedCache 275 | ``CACHE_REDIS_HOST`` A Redis server host. Used only for RedisCache. 276 | ``CACHE_REDIS_PORT`` A Redis server port. Default is 6379. 277 | Used only for RedisCache. 278 | ``CACHE_REDIS_PASSWORD`` A Redis password for server. Used only for RedisCache. 279 | ``CACHE_REDIS_DB`` A Redis db (zero-based number index). Default is 0. 280 | Used only for RedisCache. 281 | ``CACHE_DIR`` Directory to store cache. Used only for 282 | FileSystemCache. 283 | ``CACHE_REDIS_URL`` URL to connect to Redis server. 284 | Example ``redis://user:password@localhost:6379/2``. 285 | Used only for RedisCache. 286 | =============================== ================================================================== 287 | 288 | 289 | In addition the standard Flask ``TESTING`` configuration option is used. If this 290 | is True then **Flask-Cache** will use NullCache only. 291 | 292 | Built-in Cache Backends 293 | ----------------------- 294 | 295 | NullCache -- null 296 | ````````````````` 297 | 298 | Cache that doesn't cache 299 | 300 | - CACHE_ARGS 301 | - CACHE_OPTIONS 302 | 303 | 304 | SimpleCache -- simple 305 | ````````````````````` 306 | 307 | Uses a local python dictionary for caching. This is not really thread safe. 308 | 309 | Relevant configuration values 310 | 311 | - CACHE_DEFAULT_TIMEOUT 312 | - CACHE_THRESHOLD 313 | - CACHE_ARGS 314 | - CACHE_OPTIONS 315 | 316 | FileSystemCache -- filesystem 317 | ````````````````````````````` 318 | 319 | Uses the filesystem to store cached values 320 | 321 | - CACHE_DEFAULT_TIMEOUT 322 | - CACHE_DIR 323 | - CACHE_THRESHOLD 324 | - CACHE_ARGS 325 | - CACHE_OPTIONS 326 | 327 | MemcachedCache -- memcached 328 | ``````````````````````````` 329 | 330 | Uses a memcached server as a backend. Supports either pylibmc or memcache or 331 | google app engine memcache library. 332 | 333 | Relevant configuration values 334 | 335 | - CACHE_DEFAULT_TIMEOUT 336 | - CACHE_KEY_PREFIX 337 | - CACHE_MEMCACHED_SERVERS 338 | - CACHE_ARGS 339 | - CACHE_OPTIONS 340 | 341 | GAEMemcachedCache -- gaememcached 342 | ````````````````````````````````` 343 | 344 | Is MemcachedCache under a different name 345 | 346 | SASLMemcachedCache -- saslmemcached 347 | ``````````````````````````````````` 348 | 349 | Uses a memcached server as a backend. Intended to be used with a SASL enabled 350 | connection to the memcached server. pylibmc is required and SASL must be supported 351 | by libmemcached. 352 | 353 | Relevant configuration values 354 | 355 | - CACHE_DEFAULT_TIMEOUT 356 | - CACHE_KEY_PREFIX 357 | - CACHE_MEMCACHED_SERVERS 358 | - CACHE_MEMCACHED_USERNAME 359 | - CACHE_MEMCACHED_PASSWORD 360 | - CACHE_ARGS 361 | - CACHE_OPTIONS 362 | 363 | .. versionadded:: 0.10 364 | 365 | SpreadSASLMemcachedCache -- spreadsaslmemcachedcache 366 | ```````````````````````````````````````````````````` 367 | 368 | Same as SASLMemcachedCache however, it has the ablity to spread value across 369 | multiple keys if it is bigger than the memcached treshold which by 370 | default is 1M. Uses pickle. 371 | 372 | .. versionadded:: 0.11 373 | 374 | RedisCache -- redis 375 | ``````````````````` 376 | 377 | - CACHE_DEFAULT_TIMEOUT 378 | - CACHE_KEY_PREFIX 379 | - CACHE_REDIS_HOST 380 | - CACHE_REDIS_PORT 381 | - CACHE_REDIS_PASSWORD 382 | - CACHE_REDIS_DB 383 | - CACHE_ARGS 384 | - CACHE_OPTIONS 385 | - CACHE_REDIS_URL 386 | 387 | 388 | Custom Cache Backends 389 | --------------------- 390 | 391 | You are able to easily add your own custom cache backends by exposing a function 392 | that can instantiate and return a cache object. ``CACHE_TYPE`` will be the 393 | import string to your custom function. It should expect to receive three 394 | arguments. 395 | 396 | * ``app`` 397 | * ``args`` 398 | * ``kwargs`` 399 | 400 | Your custom cache object must also subclass the 401 | :class:`werkzeug.contrib.cache.BaseCache` class. Flask-Cache will make sure 402 | that ``threshold`` is already included in the kwargs options dictionary since 403 | it is common to all BaseCache classes. 404 | 405 | An example Redis cache implementation:: 406 | 407 | #: the_app/custom.py 408 | class RedisCache(BaseCache): 409 | def __init__(self, servers, default_timeout=500): 410 | pass 411 | 412 | def redis(app, config, args, kwargs): 413 | args.append(app.config['REDIS_SERVERS']) 414 | return RedisCache(*args, **kwargs) 415 | 416 | With this example, your ``CACHE_TYPE`` might be ``the_app.custom.redis`` 417 | 418 | An example PylibMC cache implementation to change binary setting and provide 419 | username/password if SASL is enabled on the library:: 420 | 421 | #: the_app/custom.py 422 | def pylibmccache(app, config, args, kwargs): 423 | return pylibmc.Client(servers=config['CACHE_MEMCACHED_SERVERS'], 424 | username=config['CACHE_MEMCACHED_USERNAME'], 425 | password=config['CACHE_MEMCACHED_PASSWORD'], 426 | binary=True) 427 | 428 | With this example, your ``CACHE_TYPE`` might be ``the_app.custom.pylibmccache`` 429 | 430 | API 431 | --- 432 | 433 | .. autoclass:: Cache 434 | :members: init_app, 435 | get, set, add, delete, get_many, set_many, delete_many, clear, 436 | cached, memoize, delete_memoized, delete_memoized_verhash 437 | 438 | .. include:: ../CHANGES 439 | -------------------------------------------------------------------------------- /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% . 10 | if NOT "%PAPER%" == "" ( 11 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 12 | ) 13 | 14 | if "%1" == "" goto help 15 | 16 | if "%1" == "help" ( 17 | :help 18 | echo.Please use `make ^` where ^ is one of 19 | echo. html to make standalone HTML files 20 | echo. dirhtml to make HTML files named index.html in directories 21 | echo. singlehtml to make a single large HTML file 22 | echo. pickle to make pickle files 23 | echo. json to make JSON files 24 | echo. htmlhelp to make HTML files and a HTML help project 25 | echo. qthelp to make HTML files and a qthelp project 26 | echo. devhelp to make HTML files and a Devhelp project 27 | echo. epub to make an epub 28 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 29 | echo. text to make text files 30 | echo. man to make manual pages 31 | echo. changes to make an overview over all changed/added/deprecated items 32 | echo. linkcheck to check all external links for integrity 33 | echo. doctest to run all doctests embedded in the documentation if enabled 34 | goto end 35 | ) 36 | 37 | if "%1" == "clean" ( 38 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 39 | del /q /s %BUILDDIR%\* 40 | goto end 41 | ) 42 | 43 | if "%1" == "html" ( 44 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 45 | echo. 46 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 47 | goto end 48 | ) 49 | 50 | if "%1" == "dirhtml" ( 51 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 52 | echo. 53 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 54 | goto end 55 | ) 56 | 57 | if "%1" == "singlehtml" ( 58 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 59 | echo. 60 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 61 | goto end 62 | ) 63 | 64 | if "%1" == "pickle" ( 65 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 66 | echo. 67 | echo.Build finished; now you can process the pickle files. 68 | goto end 69 | ) 70 | 71 | if "%1" == "json" ( 72 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 73 | echo. 74 | echo.Build finished; now you can process the JSON files. 75 | goto end 76 | ) 77 | 78 | if "%1" == "htmlhelp" ( 79 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 80 | echo. 81 | echo.Build finished; now you can run HTML Help Workshop with the ^ 82 | .hhp project file in %BUILDDIR%/htmlhelp. 83 | goto end 84 | ) 85 | 86 | if "%1" == "qthelp" ( 87 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 88 | echo. 89 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 90 | .qhcp project file in %BUILDDIR%/qthelp, like this: 91 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\Flask-SQLAlchemy.qhcp 92 | echo.To view the help file: 93 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\Flask-SQLAlchemy.ghc 94 | goto end 95 | ) 96 | 97 | if "%1" == "devhelp" ( 98 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 99 | echo. 100 | echo.Build finished. 101 | goto end 102 | ) 103 | 104 | if "%1" == "epub" ( 105 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 106 | echo. 107 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 108 | goto end 109 | ) 110 | 111 | if "%1" == "latex" ( 112 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 113 | echo. 114 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 115 | goto end 116 | ) 117 | 118 | if "%1" == "text" ( 119 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 120 | echo. 121 | echo.Build finished. The text files are in %BUILDDIR%/text. 122 | goto end 123 | ) 124 | 125 | if "%1" == "man" ( 126 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 127 | echo. 128 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 129 | goto end 130 | ) 131 | 132 | if "%1" == "changes" ( 133 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 134 | echo. 135 | echo.The overview file is in %BUILDDIR%/changes. 136 | goto end 137 | ) 138 | 139 | if "%1" == "linkcheck" ( 140 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 141 | echo. 142 | echo.Link check complete; look for any errors in the above output ^ 143 | or in %BUILDDIR%/linkcheck/output.txt. 144 | goto end 145 | ) 146 | 147 | if "%1" == "doctest" ( 148 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 149 | echo. 150 | echo.Testing of doctests in the sources finished, look at the ^ 151 | results in %BUILDDIR%/doctest/output.txt. 152 | goto end 153 | ) 154 | 155 | :end 156 | -------------------------------------------------------------------------------- /examples/hello.cfg: -------------------------------------------------------------------------------- 1 | 2 | SECRET_KEY = '\xfb\x12\xdf\xa1@i\xd6>V\xc0\xbb\x8fp\x16#Z\x0b\x81\xeb\x16' 3 | DEBUG = True 4 | CACHE_TYPE = 'simple' -------------------------------------------------------------------------------- /examples/hello.py: -------------------------------------------------------------------------------- 1 | import random 2 | from datetime import datetime 3 | 4 | from flask import Flask, jsonify 5 | from flask.ext.cache import Cache 6 | 7 | 8 | app = Flask(__name__) 9 | app.config.from_pyfile('hello.cfg') 10 | cache = Cache(app) 11 | 12 | #: This is an example of a cached view 13 | @app.route('/api/now') 14 | @cache.cached(50) 15 | def current_time(): 16 | return str(datetime.now()) 17 | 18 | #: This is an example of a cached function 19 | @cache.cached(key_prefix='binary') 20 | def random_binary(): 21 | return [random.randrange(0, 2) for i in range(500)] 22 | 23 | @app.route('/api/get/binary') 24 | def get_binary(): 25 | return jsonify({'data': random_binary()}) 26 | 27 | #: This is an example of a memoized function 28 | @cache.memoize(60) 29 | def _add(a, b): 30 | return a + b + random.randrange(0, 1000) 31 | 32 | @cache.memoize(60) 33 | def _sub(a, b): 34 | return a - b - random.randrange(0, 1000) 35 | 36 | @app.route('/api/add//') 37 | def add(a, b): 38 | return str(_add(a, b)) 39 | 40 | @app.route('/api/sub//') 41 | def sub(a, b): 42 | return str(_sub(a, b)) 43 | 44 | @app.route('/api/cache/delete') 45 | def delete_cache(): 46 | cache.delete_memoized('_add', '_sub') 47 | return 'OK' 48 | 49 | if __name__ == '__main__': 50 | app.run() 51 | -------------------------------------------------------------------------------- /flask_cache/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | flask.ext.cache 4 | ~~~~~~~~~~~~~~ 5 | 6 | Adds cache support to your application. 7 | 8 | :copyright: (c) 2010 by Thadeus Burgess. 9 | :license: BSD, see LICENSE for more details 10 | """ 11 | 12 | __version__ = '0.13' 13 | __versionfull__ = __version__ 14 | 15 | import base64 16 | import functools 17 | import hashlib 18 | import inspect 19 | import logging 20 | import string 21 | import uuid 22 | import warnings 23 | 24 | from werkzeug import import_string 25 | from flask import request, current_app 26 | 27 | from ._compat import PY2 28 | 29 | logger = logging.getLogger(__name__) 30 | 31 | TEMPLATE_FRAGMENT_KEY_TEMPLATE = '_template_fragment_cache_%s%s' 32 | 33 | # Used to remove control characters and whitespace from cache keys. 34 | valid_chars = set(string.ascii_letters + string.digits + '_.') 35 | delchars = ''.join(c for c in map(chr, range(256)) if c not in valid_chars) 36 | if PY2: 37 | null_control = (None, delchars) 38 | else: 39 | null_control = (dict((k,None) for k in delchars),) 40 | 41 | def function_namespace(f, args=None): 42 | """ 43 | Attempts to returns unique namespace for function 44 | """ 45 | m_args = inspect.getargspec(f)[0] 46 | instance_token = None 47 | 48 | instance_self = getattr(f, '__self__', None) 49 | 50 | if instance_self \ 51 | and not inspect.isclass(instance_self): 52 | instance_token = repr(f.__self__) 53 | elif m_args \ 54 | and m_args[0] == 'self' \ 55 | and args: 56 | instance_token = repr(args[0]) 57 | 58 | module = f.__module__ 59 | 60 | if hasattr(f, '__qualname__'): 61 | name = f.__qualname__ 62 | else: 63 | klass = getattr(f, '__self__', None) 64 | 65 | if klass \ 66 | and not inspect.isclass(klass): 67 | klass = klass.__class__ 68 | 69 | if not klass: 70 | klass = getattr(f, 'im_class', None) 71 | 72 | if not klass: 73 | if m_args and args: 74 | if m_args[0] == 'self': 75 | klass = args[0].__class__ 76 | elif m_args[0] == 'cls': 77 | klass = args[0] 78 | 79 | if klass: 80 | name = klass.__name__ + '.' + f.__name__ 81 | else: 82 | name = f.__name__ 83 | 84 | ns = '.'.join((module, name)) 85 | ns = ns.translate(*null_control) 86 | 87 | if instance_token: 88 | ins = '.'.join((module, name, instance_token)) 89 | ins = ins.translate(*null_control) 90 | else: 91 | ins = None 92 | 93 | return ns, ins 94 | 95 | def make_template_fragment_key(fragment_name, vary_on=[]): 96 | """ 97 | Make a cache key for a specific fragment name 98 | """ 99 | if vary_on: 100 | fragment_name = "%s_" % fragment_name 101 | return TEMPLATE_FRAGMENT_KEY_TEMPLATE % (fragment_name, "_".join(vary_on)) 102 | 103 | 104 | #: Cache Object 105 | ################ 106 | 107 | class Cache(object): 108 | """ 109 | This class is used to control the cache objects. 110 | """ 111 | 112 | def __init__(self, app=None, with_jinja2_ext=True, config=None): 113 | if not (config is None or isinstance(config, dict)): 114 | raise ValueError("`config` must be an instance of dict or None") 115 | 116 | self.with_jinja2_ext = with_jinja2_ext 117 | self.config = config 118 | 119 | self.app = app 120 | if app is not None: 121 | self.init_app(app, config) 122 | 123 | def init_app(self, app, config=None): 124 | "This is used to initialize cache with your app object" 125 | if not (config is None or isinstance(config, dict)): 126 | raise ValueError("`config` must be an instance of dict or None") 127 | 128 | #: Ref PR #44. 129 | #: Do not set self.app in the case a single instance of the Cache 130 | #: object is being used for multiple app instances. 131 | #: Example use case would be Cache shipped as part of a blueprint 132 | #: or utility library. 133 | 134 | base_config = app.config.copy() 135 | if self.config: 136 | base_config.update(self.config) 137 | if config: 138 | base_config.update(config) 139 | config = base_config 140 | 141 | config.setdefault('CACHE_DEFAULT_TIMEOUT', 300) 142 | config.setdefault('CACHE_THRESHOLD', 500) 143 | config.setdefault('CACHE_KEY_PREFIX', 'flask_cache_') 144 | config.setdefault('CACHE_MEMCACHED_SERVERS', None) 145 | config.setdefault('CACHE_DIR', None) 146 | config.setdefault('CACHE_OPTIONS', None) 147 | config.setdefault('CACHE_ARGS', []) 148 | config.setdefault('CACHE_TYPE', 'null') 149 | config.setdefault('CACHE_NO_NULL_WARNING', False) 150 | 151 | if config['CACHE_TYPE'] == 'null' and not config['CACHE_NO_NULL_WARNING']: 152 | warnings.warn("Flask-Cache: CACHE_TYPE is set to null, " 153 | "caching is effectively disabled.") 154 | 155 | if self.with_jinja2_ext: 156 | from .jinja2ext import CacheExtension, JINJA_CACHE_ATTR_NAME 157 | 158 | setattr(app.jinja_env, JINJA_CACHE_ATTR_NAME, self) 159 | app.jinja_env.add_extension(CacheExtension) 160 | 161 | self._set_cache(app, config) 162 | 163 | def _set_cache(self, app, config): 164 | import_me = config['CACHE_TYPE'] 165 | if '.' not in import_me: 166 | from . import backends 167 | 168 | try: 169 | cache_obj = getattr(backends, import_me) 170 | except AttributeError: 171 | raise ImportError("%s is not a valid FlaskCache backend" % ( 172 | import_me)) 173 | else: 174 | cache_obj = import_string(import_me) 175 | 176 | cache_args = config['CACHE_ARGS'][:] 177 | cache_options = {'default_timeout': config['CACHE_DEFAULT_TIMEOUT']} 178 | 179 | if config['CACHE_OPTIONS']: 180 | cache_options.update(config['CACHE_OPTIONS']) 181 | 182 | if not hasattr(app, 'extensions'): 183 | app.extensions = {} 184 | 185 | app.extensions.setdefault('cache', {}) 186 | app.extensions['cache'][self] = cache_obj( 187 | app, config, cache_args, cache_options) 188 | 189 | @property 190 | def cache(self): 191 | app = self.app or current_app 192 | return app.extensions['cache'][self] 193 | 194 | def get(self, *args, **kwargs): 195 | "Proxy function for internal cache object." 196 | return self.cache.get(*args, **kwargs) 197 | 198 | def set(self, *args, **kwargs): 199 | "Proxy function for internal cache object." 200 | self.cache.set(*args, **kwargs) 201 | 202 | def add(self, *args, **kwargs): 203 | "Proxy function for internal cache object." 204 | self.cache.add(*args, **kwargs) 205 | 206 | def delete(self, *args, **kwargs): 207 | "Proxy function for internal cache object." 208 | self.cache.delete(*args, **kwargs) 209 | 210 | def delete_many(self, *args, **kwargs): 211 | "Proxy function for internal cache object." 212 | self.cache.delete_many(*args, **kwargs) 213 | 214 | def clear(self): 215 | "Proxy function for internal cache object." 216 | self.cache.clear() 217 | 218 | def get_many(self, *args, **kwargs): 219 | "Proxy function for internal cache object." 220 | return self.cache.get_many(*args, **kwargs) 221 | 222 | def set_many(self, *args, **kwargs): 223 | "Proxy function for internal cache object." 224 | self.cache.set_many(*args, **kwargs) 225 | 226 | def cached(self, timeout=None, key_prefix='view/%s', unless=None): 227 | """ 228 | Decorator. Use this to cache a function. By default the cache key 229 | is `view/request.path`. You are able to use this decorator with any 230 | function by changing the `key_prefix`. If the token `%s` is located 231 | within the `key_prefix` then it will replace that with `request.path` 232 | 233 | Example:: 234 | 235 | # An example view function 236 | @cache.cached(timeout=50) 237 | def big_foo(): 238 | return big_bar_calc() 239 | 240 | # An example misc function to cache. 241 | @cache.cached(key_prefix='MyCachedList') 242 | def get_list(): 243 | return [random.randrange(0, 1) for i in range(50000)] 244 | 245 | my_list = get_list() 246 | 247 | .. note:: 248 | 249 | You MUST have a request context to actually called any functions 250 | that are cached. 251 | 252 | .. versionadded:: 0.4 253 | The returned decorated function now has three function attributes 254 | assigned to it. These attributes are readable/writable. 255 | 256 | **uncached** 257 | The original undecorated function 258 | 259 | **cache_timeout** 260 | The cache timeout value for this function. For a custom value 261 | to take affect, this must be set before the function is called. 262 | 263 | **make_cache_key** 264 | A function used in generating the cache_key used. 265 | 266 | :param timeout: Default None. If set to an integer, will cache for that 267 | amount of time. Unit of time is in seconds. 268 | :param key_prefix: Default 'view/%(request.path)s'. Beginning key to . 269 | use for the cache key. 270 | 271 | .. versionadded:: 0.3.4 272 | Can optionally be a callable which takes no arguments 273 | but returns a string that will be used as the cache_key. 274 | 275 | :param unless: Default None. Cache will *always* execute the caching 276 | facilities unless this callable is true. 277 | This will bypass the caching entirely. 278 | """ 279 | 280 | def decorator(f): 281 | @functools.wraps(f) 282 | def decorated_function(*args, **kwargs): 283 | #: Bypass the cache entirely. 284 | if callable(unless) and unless() is True: 285 | return f(*args, **kwargs) 286 | 287 | try: 288 | cache_key = decorated_function.make_cache_key(*args, **kwargs) 289 | rv = self.cache.get(cache_key) 290 | except Exception: 291 | if current_app.debug: 292 | raise 293 | logger.exception("Exception possibly due to cache backend.") 294 | return f(*args, **kwargs) 295 | 296 | if rv is None: 297 | rv = f(*args, **kwargs) 298 | try: 299 | self.cache.set(cache_key, rv, 300 | timeout=decorated_function.cache_timeout) 301 | except Exception: 302 | if current_app.debug: 303 | raise 304 | logger.exception("Exception possibly due to cache backend.") 305 | return f(*args, **kwargs) 306 | return rv 307 | 308 | def make_cache_key(*args, **kwargs): 309 | if callable(key_prefix): 310 | cache_key = key_prefix() 311 | elif '%s' in key_prefix: 312 | cache_key = key_prefix % request.path 313 | else: 314 | cache_key = key_prefix 315 | 316 | return cache_key 317 | 318 | decorated_function.uncached = f 319 | decorated_function.cache_timeout = timeout 320 | decorated_function.make_cache_key = make_cache_key 321 | 322 | return decorated_function 323 | return decorator 324 | 325 | def _memvname(self, funcname): 326 | return funcname + '_memver' 327 | 328 | def _memoize_make_version_hash(self): 329 | return base64.b64encode(uuid.uuid4().bytes)[:6].decode('utf-8') 330 | 331 | def _memoize_version(self, f, args=None, 332 | reset=False, delete=False, timeout=None): 333 | """ 334 | Updates the hash version associated with a memoized function or method. 335 | """ 336 | fname, instance_fname = function_namespace(f, args=args) 337 | version_key = self._memvname(fname) 338 | fetch_keys = [version_key] 339 | 340 | if instance_fname: 341 | instance_version_key = self._memvname(instance_fname) 342 | fetch_keys.append(instance_version_key) 343 | 344 | # Only delete the per-instance version key or per-function version 345 | # key but not both. 346 | if delete: 347 | self.cache.delete_many(fetch_keys[-1]) 348 | return fname, None 349 | 350 | version_data_list = list(self.cache.get_many(*fetch_keys)) 351 | dirty = False 352 | 353 | if version_data_list[0] is None: 354 | version_data_list[0] = self._memoize_make_version_hash() 355 | dirty = True 356 | 357 | if instance_fname and version_data_list[1] is None: 358 | version_data_list[1] = self._memoize_make_version_hash() 359 | dirty = True 360 | 361 | # Only reset the per-instance version or the per-function version 362 | # but not both. 363 | if reset: 364 | fetch_keys = fetch_keys[-1:] 365 | version_data_list = [self._memoize_make_version_hash()] 366 | dirty = True 367 | 368 | if dirty: 369 | self.cache.set_many(dict(zip(fetch_keys, version_data_list)), 370 | timeout=timeout) 371 | 372 | return fname, ''.join(version_data_list) 373 | 374 | def _memoize_make_cache_key(self, make_name=None, timeout=None): 375 | """ 376 | Function used to create the cache_key for memoized functions. 377 | """ 378 | def make_cache_key(f, *args, **kwargs): 379 | _timeout = getattr(timeout, 'cache_timeout', timeout) 380 | fname, version_data = self._memoize_version(f, args=args, 381 | timeout=_timeout) 382 | 383 | #: this should have to be after version_data, so that it 384 | #: does not break the delete_memoized functionality. 385 | if callable(make_name): 386 | altfname = make_name(fname) 387 | else: 388 | altfname = fname 389 | 390 | if callable(f): 391 | keyargs, keykwargs = self._memoize_kwargs_to_args(f, 392 | *args, 393 | **kwargs) 394 | else: 395 | keyargs, keykwargs = args, kwargs 396 | 397 | try: 398 | updated = "{0}{1}{2}".format(altfname, keyargs, keykwargs) 399 | except AttributeError: 400 | updated = "%s%s%s" % (altfname, keyargs, keykwargs) 401 | 402 | cache_key = hashlib.md5() 403 | cache_key.update(updated.encode('utf-8')) 404 | cache_key = base64.b64encode(cache_key.digest())[:16] 405 | cache_key = cache_key.decode('utf-8') 406 | cache_key += version_data 407 | 408 | return cache_key 409 | return make_cache_key 410 | 411 | def _memoize_kwargs_to_args(self, f, *args, **kwargs): 412 | #: Inspect the arguments to the function 413 | #: This allows the memoization to be the same 414 | #: whether the function was called with 415 | #: 1, b=2 is equivilant to a=1, b=2, etc. 416 | new_args = [] 417 | arg_num = 0 418 | argspec = inspect.getargspec(f) 419 | 420 | args_len = len(argspec.args) 421 | for i in range(args_len): 422 | if i == 0 and argspec.args[i] in ('self', 'cls'): 423 | #: use the repr of the class instance 424 | #: this supports instance methods for 425 | #: the memoized functions, giving more 426 | #: flexibility to developers 427 | arg = repr(args[0]) 428 | arg_num += 1 429 | elif argspec.args[i] in kwargs: 430 | arg = kwargs[argspec.args[i]] 431 | elif arg_num < len(args): 432 | arg = args[arg_num] 433 | arg_num += 1 434 | elif abs(i-args_len) <= len(argspec.defaults): 435 | arg = argspec.defaults[i-args_len] 436 | arg_num += 1 437 | else: 438 | arg = None 439 | arg_num += 1 440 | 441 | #: Attempt to convert all arguments to a 442 | #: hash/id or a representation? 443 | #: Not sure if this is necessary, since 444 | #: using objects as keys gets tricky quickly. 445 | # if hasattr(arg, '__class__'): 446 | # try: 447 | # arg = hash(arg) 448 | # except: 449 | # arg = repr(arg) 450 | 451 | #: Or what about a special __cacherepr__ function 452 | #: on an object, this allows objects to act normal 453 | #: upon inspection, yet they can define a representation 454 | #: that can be used to make the object unique in the 455 | #: cache key. Given that a case comes across that 456 | #: an object "must" be used as a cache key 457 | # if hasattr(arg, '__cacherepr__'): 458 | # arg = arg.__cacherepr__ 459 | 460 | new_args.append(arg) 461 | 462 | return tuple(new_args), {} 463 | 464 | def memoize(self, timeout=None, make_name=None, unless=None): 465 | """ 466 | Use this to cache the result of a function, taking its arguments into 467 | account in the cache key. 468 | 469 | Information on 470 | `Memoization `_. 471 | 472 | Example:: 473 | 474 | @cache.memoize(timeout=50) 475 | def big_foo(a, b): 476 | return a + b + random.randrange(0, 1000) 477 | 478 | .. code-block:: pycon 479 | 480 | >>> big_foo(5, 2) 481 | 753 482 | >>> big_foo(5, 3) 483 | 234 484 | >>> big_foo(5, 2) 485 | 753 486 | 487 | .. versionadded:: 0.4 488 | The returned decorated function now has three function attributes 489 | assigned to it. 490 | 491 | **uncached** 492 | The original undecorated function. readable only 493 | 494 | **cache_timeout** 495 | The cache timeout value for this function. For a custom value 496 | to take affect, this must be set before the function is called. 497 | 498 | readable and writable 499 | 500 | **make_cache_key** 501 | A function used in generating the cache_key used. 502 | 503 | readable and writable 504 | 505 | 506 | :param timeout: Default None. If set to an integer, will cache for that 507 | amount of time. Unit of time is in seconds. 508 | :param make_name: Default None. If set this is a function that accepts 509 | a single argument, the function name, and returns a 510 | new string to be used as the function name. If not set 511 | then the function name is used. 512 | :param unless: Default None. Cache will *always* execute the caching 513 | facilities unelss this callable is true. 514 | This will bypass the caching entirely. 515 | 516 | .. versionadded:: 0.5 517 | params ``make_name``, ``unless`` 518 | """ 519 | 520 | def memoize(f): 521 | @functools.wraps(f) 522 | def decorated_function(*args, **kwargs): 523 | #: bypass cache 524 | if callable(unless) and unless() is True: 525 | return f(*args, **kwargs) 526 | 527 | try: 528 | cache_key = decorated_function.make_cache_key(f, *args, **kwargs) 529 | rv = self.cache.get(cache_key) 530 | except Exception: 531 | if current_app.debug: 532 | raise 533 | logger.exception("Exception possibly due to cache backend.") 534 | return f(*args, **kwargs) 535 | 536 | if rv is None: 537 | rv = f(*args, **kwargs) 538 | try: 539 | self.cache.set(cache_key, rv, 540 | timeout=decorated_function.cache_timeout) 541 | except Exception: 542 | if current_app.debug: 543 | raise 544 | logger.exception("Exception possibly due to cache backend.") 545 | return rv 546 | 547 | decorated_function.uncached = f 548 | decorated_function.cache_timeout = timeout 549 | decorated_function.make_cache_key = self._memoize_make_cache_key( 550 | make_name, decorated_function) 551 | decorated_function.delete_memoized = lambda: self.delete_memoized(f) 552 | 553 | return decorated_function 554 | return memoize 555 | 556 | def delete_memoized(self, f, *args, **kwargs): 557 | """ 558 | Deletes the specified functions caches, based by given parameters. 559 | If parameters are given, only the functions that were memoized with them 560 | will be erased. Otherwise all versions of the caches will be forgotten. 561 | 562 | Example:: 563 | 564 | @cache.memoize(50) 565 | def random_func(): 566 | return random.randrange(1, 50) 567 | 568 | @cache.memoize() 569 | def param_func(a, b): 570 | return a+b+random.randrange(1, 50) 571 | 572 | .. code-block:: pycon 573 | 574 | >>> random_func() 575 | 43 576 | >>> random_func() 577 | 43 578 | >>> cache.delete_memoized('random_func') 579 | >>> random_func() 580 | 16 581 | >>> param_func(1, 2) 582 | 32 583 | >>> param_func(1, 2) 584 | 32 585 | >>> param_func(2, 2) 586 | 47 587 | >>> cache.delete_memoized('param_func', 1, 2) 588 | >>> param_func(1, 2) 589 | 13 590 | >>> param_func(2, 2) 591 | 47 592 | 593 | Delete memoized is also smart about instance methods vs class methods. 594 | 595 | When passing a instancemethod, it will only clear the cache related 596 | to that instance of that object. (object uniqueness can be overridden 597 | by defining the __repr__ method, such as user id). 598 | 599 | When passing a classmethod, it will clear all caches related across 600 | all instances of that class. 601 | 602 | Example:: 603 | 604 | class Adder(object): 605 | @cache.memoize() 606 | def add(self, b): 607 | return b + random.random() 608 | 609 | .. code-block:: pycon 610 | 611 | >>> adder1 = Adder() 612 | >>> adder2 = Adder() 613 | >>> adder1.add(3) 614 | 3.23214234 615 | >>> adder2.add(3) 616 | 3.60898509 617 | >>> cache.delete_memoized(adder.add) 618 | >>> adder1.add(3) 619 | 3.01348673 620 | >>> adder2.add(3) 621 | 3.60898509 622 | >>> cache.delete_memoized(Adder.add) 623 | >>> adder1.add(3) 624 | 3.53235667 625 | >>> adder2.add(3) 626 | 3.72341788 627 | 628 | :param fname: Name of the memoized function, or a reference to the function. 629 | :param \*args: A list of positional parameters used with memoized function. 630 | :param \**kwargs: A dict of named parameters used with memoized function. 631 | 632 | .. note:: 633 | 634 | Flask-Cache uses inspect to order kwargs into positional args when 635 | the function is memoized. If you pass a function reference into ``fname`` 636 | instead of the function name, Flask-Cache will be able to place 637 | the args/kwargs in the proper order, and delete the positional cache. 638 | 639 | However, if ``delete_memoized`` is just called with the name of the 640 | function, be sure to pass in potential arguments in the same order 641 | as defined in your function as args only, otherwise Flask-Cache 642 | will not be able to compute the same cache key. 643 | 644 | .. note:: 645 | 646 | Flask-Cache maintains an internal random version hash for the function. 647 | Using delete_memoized will only swap out the version hash, causing 648 | the memoize function to recompute results and put them into another key. 649 | 650 | This leaves any computed caches for this memoized function within the 651 | caching backend. 652 | 653 | It is recommended to use a very high timeout with memoize if using 654 | this function, so that when the version has is swapped, the old cached 655 | results would eventually be reclaimed by the caching backend. 656 | """ 657 | if not callable(f): 658 | raise DeprecationWarning("Deleting messages by relative name is no longer" 659 | " reliable, please switch to a function reference") 660 | 661 | 662 | try: 663 | if not args and not kwargs: 664 | self._memoize_version(f, reset=True) 665 | else: 666 | cache_key = f.make_cache_key(f.uncached, *args, **kwargs) 667 | self.cache.delete(cache_key) 668 | except Exception: 669 | if current_app.debug: 670 | raise 671 | logger.exception("Exception possibly due to cache backend.") 672 | 673 | def delete_memoized_verhash(self, f, *args): 674 | """ 675 | Delete the version hash associated with the function. 676 | 677 | ..warning:: 678 | 679 | Performing this operation could leave keys behind that have 680 | been created with this version hash. It is up to the application 681 | to make sure that all keys that may have been created with this 682 | version hash at least have timeouts so they will not sit orphaned 683 | in the cache backend. 684 | """ 685 | if not callable(f): 686 | raise DeprecationWarning("Deleting messages by relative name is no longer" 687 | " reliable, please use a function reference") 688 | 689 | try: 690 | self._memoize_version(f, delete=True) 691 | except Exception: 692 | if current_app.debug: 693 | raise 694 | logger.exception("Exception possibly due to cache backend.") 695 | -------------------------------------------------------------------------------- /flask_cache/_compat.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | flask_cache._compat 4 | ~~~~~~~~~~~~~~~~~~~ 5 | 6 | Some py2/py3 compatibility support based on a stripped down 7 | version of six so we don't have to depend on a specific version 8 | of it. 9 | 10 | :copyright: (c) 2013 by Armin Ronacher. 11 | :license: BSD, see LICENSE for more details. 12 | """ 13 | import sys 14 | 15 | PY2 = sys.version_info[0] == 2 16 | PYPY = hasattr(sys, 'pypy_translation_info') 17 | 18 | 19 | if not PY2: 20 | range_type = range 21 | else: 22 | range_type = xrange 23 | -------------------------------------------------------------------------------- /flask_cache/backends.py: -------------------------------------------------------------------------------- 1 | import pickle 2 | from werkzeug.contrib.cache import (BaseCache, NullCache, SimpleCache, MemcachedCache, 3 | GAEMemcachedCache, FileSystemCache) 4 | from ._compat import range_type 5 | 6 | 7 | class SASLMemcachedCache(MemcachedCache): 8 | 9 | def __init__(self, servers=None, default_timeout=300, key_prefix=None, 10 | username=None, password=None): 11 | BaseCache.__init__(self, default_timeout) 12 | 13 | if servers is None: 14 | servers = ['127.0.0.1:11211'] 15 | 16 | import pylibmc 17 | self._client = pylibmc.Client(servers, 18 | username=username, 19 | password=password, 20 | binary=True) 21 | 22 | self.key_prefix = key_prefix 23 | 24 | 25 | def null(app, config, args, kwargs): 26 | return NullCache() 27 | 28 | def simple(app, config, args, kwargs): 29 | kwargs.update(dict(threshold=config['CACHE_THRESHOLD'])) 30 | return SimpleCache(*args, **kwargs) 31 | 32 | def memcached(app, config, args, kwargs): 33 | args.append(config['CACHE_MEMCACHED_SERVERS']) 34 | kwargs.update(dict(key_prefix=config['CACHE_KEY_PREFIX'])) 35 | return MemcachedCache(*args, **kwargs) 36 | 37 | def saslmemcached(app, config, args, kwargs): 38 | args.append(config['CACHE_MEMCACHED_SERVERS']) 39 | kwargs.update(dict(username=config['CACHE_MEMCACHED_USERNAME'], 40 | password=config['CACHE_MEMCACHED_PASSWORD'], 41 | key_prefix=config['CACHE_KEY_PREFIX'])) 42 | return SASLMemcachedCache(*args, **kwargs) 43 | 44 | def gaememcached(app, config, args, kwargs): 45 | kwargs.update(dict(key_prefix=config['CACHE_KEY_PREFIX'])) 46 | return GAEMemcachedCache(*args, **kwargs) 47 | 48 | def filesystem(app, config, args, kwargs): 49 | args.insert(0, config['CACHE_DIR']) 50 | kwargs.update(dict(threshold=config['CACHE_THRESHOLD'])) 51 | return FileSystemCache(*args, **kwargs) 52 | 53 | # RedisCache is supported since Werkzeug 0.7. 54 | try: 55 | from werkzeug.contrib.cache import RedisCache 56 | from redis import from_url as redis_from_url 57 | except ImportError: 58 | pass 59 | else: 60 | def redis(app, config, args, kwargs): 61 | kwargs.update(dict( 62 | host=config.get('CACHE_REDIS_HOST', 'localhost'), 63 | port=config.get('CACHE_REDIS_PORT', 6379), 64 | )) 65 | password = config.get('CACHE_REDIS_PASSWORD') 66 | if password: 67 | kwargs['password'] = password 68 | 69 | key_prefix = config.get('CACHE_KEY_PREFIX') 70 | if key_prefix: 71 | kwargs['key_prefix'] = key_prefix 72 | 73 | db_number = config.get('CACHE_REDIS_DB') 74 | if db_number: 75 | kwargs['db'] = db_number 76 | 77 | redis_url = config.get('CACHE_REDIS_URL') 78 | if redis_url: 79 | kwargs['host'] = redis_from_url( 80 | redis_url, 81 | db=kwargs.pop('db', None), 82 | ) 83 | 84 | return RedisCache(*args, **kwargs) 85 | 86 | 87 | class SpreadSASLMemcachedCache(SASLMemcachedCache): 88 | """ 89 | Simple Subclass of SASLMemcached client that spread value across multiple 90 | key is they are bigger than a given treshhold. 91 | 92 | Spreading require using pickle to store the value, wich can significantly 93 | impact the performances. 94 | """ 95 | 96 | 97 | def __init__(self, *args, **kwargs): 98 | """ 99 | chunksize : (int) max size in bytes of chunk stored in memcached 100 | """ 101 | self.chunksize = kwargs.get('chunksize', 950000) 102 | self.maxchunk = kwargs.get('maxchunk', 32) 103 | super(SpreadSASLMemcachedCache, self).__init__(*args, **kwargs) 104 | 105 | def delete(self, key): 106 | for skey in self._genkeys(key): 107 | super(SpreadSASLMemcachedCache, self).delete(skey) 108 | 109 | 110 | def set(self, key, value, timeout=None, chunk=True): 111 | """set a value in cache, potentially spreding it across multiple key. 112 | 113 | chunk : (Bool) if set to false, does not try to spread across multiple key. 114 | this can be faster, but will fail if value is bigger than chunks, 115 | and require you to get back the object by specifying that it is not spread. 116 | 117 | """ 118 | if chunk: 119 | return self._set(key, value, timeout=timeout) 120 | else: 121 | return super(SpreadSASLMemcachedCache, self).set(key, value, timeout=timeout) 122 | 123 | def _set(self, key, value, timeout=None): 124 | # pickling/unpickling add an overhed, 125 | # I didn't found a good way to avoid pickling/unpickling if 126 | # key is smaller than chunksize, because in case or 127 | # getting the length consume the data iterator. 128 | serialized = pickle.dumps(value, 2) 129 | values = {} 130 | len_ser = len(serialized) 131 | chks = range_type(0, len_ser, self.chunksize) 132 | if len(chks) > self.maxchunk: 133 | raise ValueError('Cannot store value in less than %s keys'%(self.maxchunk)) 134 | for i in chks: 135 | values['%s.%s' % (key, i//self.chunksize)] = serialized[i : i+self.chunksize] 136 | super(SpreadSASLMemcachedCache, self).set_many(values, timeout) 137 | 138 | def get(self, key, chunk=True): 139 | """get a value in cache, potentially spreded it across multiple key. 140 | 141 | chunk : (Bool) if set to false, get a value set with set(..., chunk=False) 142 | """ 143 | if chunk : 144 | return self._get(key) 145 | else : 146 | return super(SpreadSASLMemcachedCache, self).get(key) 147 | 148 | def _genkeys(self, key): 149 | return ['%s.%s' % (key, i) for i in range_type(self.maxchunk)] 150 | 151 | def _get(self, key): 152 | to_get = ['%s.%s' % (key, i) for i in range_type(self.maxchunk)] 153 | result = super(SpreadSASLMemcachedCache, self).get_many( *to_get) 154 | serialized = ''.join([v for v in result if v is not None]) 155 | if not serialized: 156 | return None 157 | return pickle.loads(serialized) 158 | 159 | def spreadsaslmemcachedcache(app, config, args, kwargs): 160 | 161 | args.append(config['CACHE_MEMCACHED_SERVERS']) 162 | kwargs.update(dict(username=config.get('CACHE_MEMCACHED_USERNAME'), 163 | password=config.get('CACHE_MEMCACHED_PASSWORD'), 164 | key_prefix=config.get('CACHE_KEY_PREFIX') 165 | )) 166 | return SpreadSASLMemcachedCache(*args, **kwargs) 167 | -------------------------------------------------------------------------------- /flask_cache/jinja2ext.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | 4 | Jinja2 extension that adds support for caching template fragments. 5 | 6 | Usage: 7 | {% cache timeout key1[, [key2, ...]] %} 8 | ... 9 | {% endcache %} 10 | 11 | By default the value of "path to template file" + "block start line" is used as cache key. 12 | Also key name can be set manually. Keys are concated together into a single string. 13 | that can be used to avoid the same block evaluating in different templates. 14 | 15 | Set timeout to "del" to delete cached value: 16 | {% cache 'del' key1 %}... 17 | 18 | Example: 19 | Considering we have render_form_field and render_submit macros. 20 | {% cache 60*5 'myform' %} 21 |
22 |
23 | {% render_form_field form.username %} 24 | {% render_submit %} 25 |
26 |
27 | {% endcache %} 28 | 29 | """ 30 | 31 | from jinja2 import nodes 32 | from jinja2.ext import Extension 33 | from flask.ext.cache import make_template_fragment_key 34 | 35 | JINJA_CACHE_ATTR_NAME = '_template_fragment_cache' 36 | 37 | 38 | class CacheExtension(Extension): 39 | tags = set(['cache']) 40 | 41 | def parse(self, parser): 42 | lineno = next(parser.stream).lineno 43 | 44 | #: Parse timeout 45 | args = [parser.parse_expression()] 46 | 47 | #: Parse fragment name 48 | #: Grab the fragment name if it exists 49 | #: otherwise, default to the old method of using the templates 50 | #: lineno to maintain backwards compatibility. 51 | if parser.stream.skip_if('comma'): 52 | args.append(parser.parse_expression()) 53 | else: 54 | args.append(nodes.Const("%s%s" % (parser.filename, lineno))) 55 | 56 | #: Parse vary_on parameters 57 | vary_on = [] 58 | while parser.stream.skip_if('comma'): 59 | vary_on.append(parser.parse_expression()) 60 | 61 | if vary_on: 62 | args.append(nodes.List(vary_on)) 63 | else: 64 | args.append(nodes.Const([])) 65 | 66 | body = parser.parse_statements(['name:endcache'], drop_needle=True) 67 | return nodes.CallBlock(self.call_method('_cache', args), 68 | [], [], body).set_lineno(lineno) 69 | 70 | def _cache(self, timeout, fragment_name, vary_on, caller): 71 | try: 72 | cache = getattr(self.environment, JINJA_CACHE_ATTR_NAME) 73 | except AttributeError as e: 74 | raise e 75 | 76 | key = make_template_fragment_key(fragment_name, vary_on=vary_on) 77 | 78 | #: Delete key if timeout is 'del' 79 | if timeout == "del": 80 | cache.delete(key) 81 | return caller() 82 | 83 | rv = cache.get(key) 84 | if rv is None: 85 | rv = caller() 86 | cache.set(key, rv, timeout) 87 | return rv 88 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | git+git://github.com/mitsuhiko/werkzeug.git#egg=werkzeug 2 | git+git://github.com/mitsuhiko/flask.git#egg=flask 3 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [build_sphinx] 2 | source-dir = docs/ 3 | build-dir = docs/_build 4 | all_files = 1 5 | 6 | [upload_sphinx] 7 | upload-dir = docs/_build/html 8 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | Flask-Cache 4 | ----------- 5 | 6 | Adds cache support to your Flask application 7 | 8 | """ 9 | 10 | from setuptools import setup 11 | 12 | setup( 13 | name='Flask-Cache', 14 | version='0.13', 15 | url='http://github.com/thadeusb/flask-cache', 16 | license='BSD', 17 | author='Thadeus Burgess', 18 | author_email='thadeusb@thadeusb.com', 19 | description='Adds cache support to your Flask application', 20 | long_description=__doc__, 21 | packages=[ 22 | 'flask_cache', 23 | ], 24 | zip_safe=False, 25 | platforms='any', 26 | install_requires=[ 27 | 'Flask' 28 | ], 29 | test_suite='test_cache', 30 | classifiers=[ 31 | 'Development Status :: 4 - Beta', 32 | 'Environment :: Web Environment', 33 | 'Intended Audience :: Developers', 34 | 'License :: OSI Approved :: BSD License', 35 | 'Operating System :: OS Independent', 36 | 'Programming Language :: Python', 37 | 'Programming Language :: Python :: 2', 38 | 'Programming Language :: Python :: 2.6', 39 | 'Programming Language :: Python :: 2.7', 40 | 'Programming Language :: Python :: 3', 41 | 'Programming Language :: Python :: 3.3', 42 | 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', 43 | 'Topic :: Software Development :: Libraries :: Python Modules' 44 | ] 45 | ) 46 | -------------------------------------------------------------------------------- /test_cache.py: -------------------------------------------------------------------------------- 1 | from __future__ import with_statement 2 | 3 | import sys 4 | import os 5 | import time 6 | import random 7 | import string 8 | 9 | from flask import Flask, render_template, render_template_string 10 | from flask.ext.cache import Cache, function_namespace, make_template_fragment_key 11 | 12 | if sys.version_info < (2,7): 13 | import unittest2 as unittest 14 | else: 15 | import unittest 16 | 17 | class CacheTestCase(unittest.TestCase): 18 | 19 | def _set_app_config(self, app): 20 | app.config['CACHE_TYPE'] = 'simple' 21 | 22 | def setUp(self): 23 | app = Flask(__name__, template_folder=os.path.dirname(__file__)) 24 | 25 | app.debug = True 26 | self._set_app_config(app) 27 | 28 | self.cache = Cache(app) 29 | 30 | self.app = app 31 | 32 | def tearDown(self): 33 | self.app = None 34 | self.cache = None 35 | self.tc = None 36 | 37 | def test_00_set(self): 38 | self.cache.set('hi', 'hello') 39 | 40 | assert self.cache.get('hi') == 'hello' 41 | 42 | def test_01_add(self): 43 | self.cache.add('hi', 'hello') 44 | 45 | assert self.cache.get('hi') == 'hello' 46 | 47 | self.cache.add('hi', 'foobar') 48 | 49 | assert self.cache.get('hi') == 'hello' 50 | 51 | def test_02_delete(self): 52 | self.cache.set('hi', 'hello') 53 | 54 | self.cache.delete('hi') 55 | 56 | assert self.cache.get('hi') is None 57 | 58 | def test_03_cached_view(self): 59 | 60 | @self.app.route('/') 61 | @self.cache.cached(5) 62 | def cached_view(): 63 | return str(time.time()) 64 | 65 | tc = self.app.test_client() 66 | 67 | rv = tc.get('/') 68 | the_time = rv.data.decode('utf-8') 69 | 70 | time.sleep(2) 71 | 72 | rv = tc.get('/') 73 | 74 | assert the_time == rv.data.decode('utf-8') 75 | 76 | time.sleep(5) 77 | 78 | rv = tc.get('/') 79 | assert the_time != rv.data.decode('utf-8') 80 | 81 | def test_04_cached_view_unless(self): 82 | @self.app.route('/a') 83 | @self.cache.cached(5, unless=lambda: True) 84 | def non_cached_view(): 85 | return str(time.time()) 86 | 87 | @self.app.route('/b') 88 | @self.cache.cached(5, unless=lambda: False) 89 | def cached_view(): 90 | return str(time.time()) 91 | 92 | tc = self.app.test_client() 93 | 94 | rv = tc.get('/a') 95 | the_time = rv.data.decode('utf-8') 96 | 97 | time.sleep(1) 98 | 99 | rv = tc.get('/a') 100 | assert the_time != rv.data.decode('utf-8') 101 | 102 | rv = tc.get('/b') 103 | the_time = rv.data.decode('utf-8') 104 | 105 | time.sleep(1) 106 | rv = tc.get('/b') 107 | 108 | assert the_time == rv.data.decode('utf-8') 109 | 110 | def test_05_cached_function(self): 111 | 112 | with self.app.test_request_context(): 113 | @self.cache.cached(2, key_prefix='MyBits') 114 | def get_random_bits(): 115 | return [random.randrange(0, 2) for i in range(50)] 116 | 117 | my_list = get_random_bits() 118 | his_list = get_random_bits() 119 | 120 | assert my_list == his_list 121 | 122 | time.sleep(4) 123 | 124 | his_list = get_random_bits() 125 | 126 | assert my_list != his_list 127 | 128 | def test_06_memoize(self): 129 | 130 | with self.app.test_request_context(): 131 | @self.cache.memoize(5) 132 | def big_foo(a, b): 133 | return a+b+random.randrange(0, 100000) 134 | 135 | result = big_foo(5, 2) 136 | 137 | time.sleep(1) 138 | 139 | assert big_foo(5, 2) == result 140 | 141 | result2 = big_foo(5, 3) 142 | assert result2 != result 143 | 144 | time.sleep(6) 145 | 146 | assert big_foo(5, 2) != result 147 | 148 | time.sleep(1) 149 | 150 | assert big_foo(5, 3) != result2 151 | 152 | def test_06a_memoize(self): 153 | self.app.config['CACHE_DEFAULT_TIMEOUT'] = 1 154 | self.cache = Cache(self.app) 155 | 156 | with self.app.test_request_context(): 157 | @self.cache.memoize(50) 158 | def big_foo(a, b): 159 | return a+b+random.randrange(0, 100000) 160 | 161 | result = big_foo(5, 2) 162 | 163 | time.sleep(2) 164 | 165 | assert big_foo(5, 2) == result 166 | 167 | def test_07_delete_memoize(self): 168 | 169 | with self.app.test_request_context(): 170 | @self.cache.memoize(5) 171 | def big_foo(a, b): 172 | return a+b+random.randrange(0, 100000) 173 | 174 | result = big_foo(5, 2) 175 | result2 = big_foo(5, 3) 176 | 177 | time.sleep(1) 178 | 179 | assert big_foo(5, 2) == result 180 | assert big_foo(5, 2) == result 181 | assert big_foo(5, 3) != result 182 | assert big_foo(5, 3) == result2 183 | 184 | self.cache.delete_memoized(big_foo) 185 | 186 | assert big_foo(5, 2) != result 187 | assert big_foo(5, 3) != result2 188 | 189 | def test_07b_delete_memoized_verhash(self): 190 | with self.app.test_request_context(): 191 | @self.cache.memoize(5) 192 | def big_foo(a, b): 193 | return a+b+random.randrange(0, 100000) 194 | 195 | result = big_foo(5, 2) 196 | result2 = big_foo(5, 3) 197 | 198 | time.sleep(1) 199 | 200 | assert big_foo(5, 2) == result 201 | assert big_foo(5, 2) == result 202 | assert big_foo(5, 3) != result 203 | assert big_foo(5, 3) == result2 204 | 205 | self.cache.delete_memoized_verhash(big_foo) 206 | 207 | _fname, _fname_instance = function_namespace(big_foo) 208 | version_key = self.cache._memvname(_fname) 209 | assert self.cache.get(version_key) is None 210 | 211 | assert big_foo(5, 2) != result 212 | assert big_foo(5, 3) != result2 213 | 214 | assert self.cache.get(version_key) is not None 215 | 216 | def test_08_delete_memoize(self): 217 | 218 | with self.app.test_request_context(): 219 | @self.cache.memoize() 220 | def big_foo(a, b): 221 | return a+b+random.randrange(0, 100000) 222 | 223 | result_a = big_foo(5, 1) 224 | result_b = big_foo(5, 2) 225 | 226 | assert big_foo(5, 1) == result_a 227 | assert big_foo(5, 2) == result_b 228 | self.cache.delete_memoized(big_foo, 5, 2) 229 | 230 | assert big_foo(5, 1) == result_a 231 | assert big_foo(5, 2) != result_b 232 | 233 | ## Cleanup bigfoo 5,1 5,2 or it might conflict with 234 | ## following run if it also uses memecache 235 | self.cache.delete_memoized(big_foo, 5, 2) 236 | self.cache.delete_memoized(big_foo, 5, 1) 237 | 238 | def test_09_args_memoize(self): 239 | 240 | with self.app.test_request_context(): 241 | @self.cache.memoize() 242 | def big_foo(a, b): 243 | return sum(a)+sum(b)+random.randrange(0, 100000) 244 | 245 | result_a = big_foo([5,3,2], [1]) 246 | result_b = big_foo([3,3], [3,1]) 247 | 248 | assert big_foo([5,3,2], [1]) == result_a 249 | assert big_foo([3,3], [3,1]) == result_b 250 | 251 | self.cache.delete_memoized(big_foo, [5,3,2], [1]) 252 | 253 | assert big_foo([5,3,2], [1]) != result_a 254 | assert big_foo([3,3], [3,1]) == result_b 255 | 256 | ## Cleanup bigfoo 5,1 5,2 or it might conflict with 257 | ## following run if it also uses memecache 258 | self.cache.delete_memoized(big_foo, [5,3,2], [1]) 259 | self.cache.delete_memoized(big_foo, [3,3], [1]) 260 | 261 | def test_10_kwargs_memoize(self): 262 | 263 | with self.app.test_request_context(): 264 | @self.cache.memoize() 265 | def big_foo(a, b=None): 266 | return a+sum(b.values())+random.randrange(0, 100000) 267 | 268 | result_a = big_foo(1, dict(one=1,two=2)) 269 | result_b = big_foo(5, dict(three=3,four=4)) 270 | 271 | assert big_foo(1, dict(one=1,two=2)) == result_a 272 | assert big_foo(5, dict(three=3,four=4)) == result_b 273 | 274 | self.cache.delete_memoized(big_foo, 1, dict(one=1,two=2)) 275 | 276 | assert big_foo(1, dict(one=1,two=2)) != result_a 277 | assert big_foo(5, dict(three=3,four=4)) == result_b 278 | 279 | def test_10a_kwargonly_memoize(self): 280 | 281 | with self.app.test_request_context(): 282 | @self.cache.memoize() 283 | def big_foo(a=None): 284 | if a is None: 285 | a = 0 286 | return a+random.random() 287 | 288 | result_a = big_foo() 289 | result_b = big_foo(5) 290 | 291 | assert big_foo() == result_a 292 | assert big_foo() < 1 293 | assert big_foo(5) == result_b 294 | assert big_foo(5) >= 5 and big_foo(5) < 6 295 | 296 | def test_10a_arg_kwarg_memoize(self): 297 | with self.app.test_request_context(): 298 | @self.cache.memoize() 299 | def f(a, b, c=1): 300 | return a+b+c+random.randrange(0, 100000) 301 | 302 | assert f(1,2) == f(1,2,c=1) 303 | assert f(1,2) == f(1,2,1) 304 | assert f(1,2) == f(1,2) 305 | assert f(1,2,3) != f(1,2) 306 | with self.assertRaises(TypeError): 307 | f(1) 308 | 309 | def test_10b_classarg_memoize(self): 310 | 311 | @self.cache.memoize() 312 | def bar(a): 313 | return a.value + random.random() 314 | 315 | class Adder(object): 316 | def __init__(self, value): 317 | self.value = value 318 | 319 | adder = Adder(15) 320 | adder2 = Adder(20) 321 | 322 | y = bar(adder) 323 | z = bar(adder2) 324 | 325 | assert y != z 326 | assert bar(adder) == y 327 | assert bar(adder) != z 328 | adder.value = 14 329 | assert bar(adder) == y 330 | assert bar(adder) != z 331 | 332 | assert bar(adder) != bar(adder2) 333 | assert bar(adder2) == z 334 | 335 | def test_10c_classfunc_memoize(self): 336 | class Adder(object): 337 | def __init__(self, initial): 338 | self.initial = initial 339 | 340 | @self.cache.memoize() 341 | def add(self, b): 342 | return self.initial + b 343 | 344 | adder1 = Adder(1) 345 | adder2 = Adder(2) 346 | 347 | x = adder1.add(3) 348 | assert adder1.add(3) == x 349 | assert adder1.add(4) != x 350 | assert adder1.add(3) != adder2.add(3) 351 | 352 | def test_10d_classfunc_memoize_delete(self): 353 | with self.app.test_request_context(): 354 | class Adder(object): 355 | def __init__(self, initial): 356 | self.initial = initial 357 | 358 | @self.cache.memoize() 359 | def add(self, b): 360 | return self.initial + b + random.random() 361 | 362 | adder1 = Adder(1) 363 | adder2 = Adder(2) 364 | 365 | a1 = adder1.add(3) 366 | a2 = adder2.add(3) 367 | 368 | assert a1 != a2 369 | assert adder1.add(3) == a1 370 | assert adder2.add(3) == a2 371 | 372 | self.cache.delete_memoized(adder1.add) 373 | 374 | a3 = adder1.add(3) 375 | a4 = adder2.add(3) 376 | 377 | self.assertNotEqual(a1, a3) 378 | assert a1 != a3 379 | self.assertEqual(a2, a4) 380 | 381 | self.cache.delete_memoized(Adder.add) 382 | 383 | a5 = adder1.add(3) 384 | a6 = adder2.add(3) 385 | 386 | self.assertNotEqual(a5, a6) 387 | self.assertNotEqual(a3, a5) 388 | self.assertNotEqual(a4, a6) 389 | 390 | def test_10e_delete_memoize_classmethod(self): 391 | with self.app.test_request_context(): 392 | class Mock(object): 393 | @classmethod 394 | @self.cache.memoize(5) 395 | def big_foo(cls, a, b): 396 | return a+b+random.randrange(0, 100000) 397 | 398 | result = Mock.big_foo(5, 2) 399 | result2 = Mock.big_foo(5, 3) 400 | 401 | time.sleep(1) 402 | 403 | assert Mock.big_foo(5, 2) == result 404 | assert Mock.big_foo(5, 2) == result 405 | assert Mock.big_foo(5, 3) != result 406 | assert Mock.big_foo(5, 3) == result2 407 | 408 | self.cache.delete_memoized(Mock.big_foo) 409 | 410 | assert Mock.big_foo(5, 2) != result 411 | assert Mock.big_foo(5, 3) != result2 412 | 413 | def test_11_cache_key_property(self): 414 | @self.app.route('/') 415 | @self.cache.cached(5) 416 | def cached_view(): 417 | return str(time.time()) 418 | 419 | assert hasattr(cached_view, "make_cache_key") 420 | assert callable(cached_view.make_cache_key) 421 | 422 | tc = self.app.test_client() 423 | 424 | rv = tc.get('/') 425 | the_time = rv.data.decode('utf-8') 426 | 427 | with self.app.test_request_context(): 428 | cache_data = self.cache.get(cached_view.make_cache_key()) 429 | assert the_time == cache_data 430 | 431 | def test_12_make_cache_key_function_property(self): 432 | @self.app.route('//') 433 | @self.cache.memoize(5) 434 | def cached_view(foo, bar): 435 | return str(time.time()) 436 | 437 | assert hasattr(cached_view, "make_cache_key") 438 | assert callable(cached_view.make_cache_key) 439 | 440 | tc = self.app.test_client() 441 | 442 | rv = tc.get('/a/b') 443 | the_time = rv.data.decode('utf-8') 444 | 445 | cache_key = cached_view.make_cache_key(cached_view.uncached, foo=u"a", bar=u"b") 446 | cache_data = self.cache.get(cache_key) 447 | assert the_time == cache_data 448 | 449 | different_key = cached_view.make_cache_key(cached_view.uncached, foo=u"b", bar=u"a") 450 | different_data = self.cache.get(different_key) 451 | assert the_time != different_data 452 | 453 | def test_13_cache_timeout_property(self): 454 | @self.app.route('/') 455 | @self.cache.memoize(5) 456 | def cached_view1(): 457 | return str(time.time()) 458 | 459 | @self.app.route('//') 460 | @self.cache.memoize(10) 461 | def cached_view2(foo, bar): 462 | return str(time.time()) 463 | 464 | assert hasattr(cached_view1, "cache_timeout") 465 | assert hasattr(cached_view2, "cache_timeout") 466 | assert cached_view1.cache_timeout == 5 467 | assert cached_view2.cache_timeout == 10 468 | 469 | # test that this is a read-write property 470 | cached_view1.cache_timeout = 15 471 | cached_view2.cache_timeout = 30 472 | 473 | assert cached_view1.cache_timeout == 15 474 | assert cached_view2.cache_timeout == 30 475 | 476 | tc = self.app.test_client() 477 | 478 | rv1 = tc.get('/') 479 | time1 = rv1.data.decode('utf-8') 480 | time.sleep(1) 481 | rv2 = tc.get('/a/b') 482 | time2 = rv2.data.decode('utf-8') 483 | 484 | # VIEW1 485 | # it's been 1 second, cache is still active 486 | assert time1 == tc.get('/').data.decode('utf-8') 487 | time.sleep(16) 488 | # it's been >15 seconds, cache is not still active 489 | assert time1 != tc.get('/').data.decode('utf-8') 490 | 491 | # VIEW2 492 | # it's been >17 seconds, cache is still active 493 | self.assertEqual(time2, tc.get('/a/b').data.decode('utf-8')) 494 | time.sleep(30) 495 | # it's been >30 seconds, cache is not still active 496 | assert time2 != tc.get('/a/b').data.decode('utf-8') 497 | 498 | def test_14_memoized_multiple_arg_kwarg_calls(self): 499 | with self.app.test_request_context(): 500 | @self.cache.memoize() 501 | def big_foo(a, b,c=[1,1],d=[1,1]): 502 | return sum(a)+sum(b)+sum(c)+sum(d)+random.randrange(0, 100000) 503 | 504 | result_a = big_foo([5,3,2], [1], c=[3,3], d=[3,3]) 505 | 506 | assert big_foo([5,3,2], [1], d=[3,3], c=[3,3]) == result_a 507 | assert big_foo(b=[1],a=[5,3,2],c=[3,3],d=[3,3]) == result_a 508 | assert big_foo([5,3,2], [1], [3,3], [3,3]) == result_a 509 | 510 | def test_15_memoize_multiple_arg_kwarg_delete(self): 511 | with self.app.test_request_context(): 512 | @self.cache.memoize() 513 | def big_foo(a, b,c=[1,1],d=[1,1]): 514 | return sum(a)+sum(b)+sum(c)+sum(d)+random.randrange(0, 100000) 515 | 516 | result_a = big_foo([5,3,2], [1], c=[3,3], d=[3,3]) 517 | self.cache.delete_memoized(big_foo, [5,3,2],[1],[3,3],[3,3]) 518 | result_b = big_foo([5,3,2], [1], c=[3,3], d=[3,3]) 519 | assert result_a != result_b 520 | 521 | self.cache.delete_memoized(big_foo, [5,3,2],b=[1],c=[3,3],d=[3,3]) 522 | result_b = big_foo([5,3,2], [1], c=[3,3], d=[3,3]) 523 | assert result_a != result_b 524 | 525 | self.cache.delete_memoized(big_foo, [5,3,2],[1],c=[3,3],d=[3,3]) 526 | result_a = big_foo([5,3,2], [1], c=[3,3], d=[3,3]) 527 | assert result_a != result_b 528 | 529 | self.cache.delete_memoized(big_foo, [5,3,2],b=[1],c=[3,3],d=[3,3]) 530 | result_a = big_foo([5,3,2], [1], c=[3,3], d=[3,3]) 531 | assert result_a != result_b 532 | 533 | self.cache.delete_memoized(big_foo, [5,3,2],[1],c=[3,3],d=[3,3]) 534 | result_b = big_foo([5,3,2], [1], c=[3,3], d=[3,3]) 535 | assert result_a != result_b 536 | 537 | self.cache.delete_memoized(big_foo, [5,3,2],[1],[3,3],[3,3]) 538 | result_a = big_foo([5,3,2], [1], c=[3,3], d=[3,3]) 539 | assert result_a != result_b 540 | 541 | def test_16_memoize_kwargs_to_args(self): 542 | with self.app.test_request_context(): 543 | def big_foo(a, b, c=None, d=None): 544 | return sum(a)+sum(b)+random.randrange(0, 100000) 545 | 546 | expected = (1,2,'foo','bar') 547 | 548 | args, kwargs = self.cache._memoize_kwargs_to_args(big_foo, 1,2,'foo','bar') 549 | assert (args == expected) 550 | args, kwargs = self.cache._memoize_kwargs_to_args(big_foo, 2,'foo','bar',a=1) 551 | assert (args == expected) 552 | args, kwargs = self.cache._memoize_kwargs_to_args(big_foo, a=1,b=2,c='foo',d='bar') 553 | assert (args == expected) 554 | args, kwargs = self.cache._memoize_kwargs_to_args(big_foo, d='bar',b=2,a=1,c='foo') 555 | assert (args == expected) 556 | args, kwargs = self.cache._memoize_kwargs_to_args(big_foo, 1,2,d='bar',c='foo') 557 | assert (args == expected) 558 | 559 | def test_17_dict_config(self): 560 | cache = Cache(config={'CACHE_TYPE': 'simple'}) 561 | cache.init_app(self.app) 562 | 563 | assert cache.config['CACHE_TYPE'] == 'simple' 564 | 565 | def test_18_dict_config_initapp(self): 566 | cache = Cache() 567 | cache.init_app(self.app, config={'CACHE_TYPE': 'simple'}) 568 | from werkzeug.contrib.cache import SimpleCache 569 | assert isinstance(self.app.extensions['cache'][cache], SimpleCache) 570 | 571 | def test_19_dict_config_both(self): 572 | cache = Cache(config={'CACHE_TYPE': 'null'}) 573 | cache.init_app(self.app, config={'CACHE_TYPE': 'simple'}) 574 | from werkzeug.contrib.cache import SimpleCache 575 | assert isinstance(self.app.extensions['cache'][cache], SimpleCache) 576 | 577 | def test_20_jinja2ext_cache(self): 578 | somevar = ''.join([random.choice(string.ascii_letters) for x in range(6)]) 579 | 580 | testkeys = [ 581 | make_template_fragment_key("fragment1"), 582 | make_template_fragment_key("fragment1", vary_on=["key1"]), 583 | make_template_fragment_key("fragment1", vary_on=["key1", somevar]), 584 | ] 585 | delkey = make_template_fragment_key("fragment2") 586 | 587 | with self.app.test_request_context(): 588 | #: Test if elements are cached 589 | render_template("test_template.html", somevar=somevar, timeout=60) 590 | for k in testkeys: 591 | assert self.cache.get(k) == somevar 592 | assert self.cache.get(delkey) == somevar 593 | 594 | #: Test timeout=del to delete key 595 | render_template("test_template.html", somevar=somevar, timeout="del") 596 | for k in testkeys: 597 | assert self.cache.get(k) == somevar 598 | assert self.cache.get(delkey) is None 599 | 600 | #: Test rendering templates from strings 601 | output = render_template_string( 602 | """{% cache 60, "fragment3" %}{{somevar}}{% endcache %}""", 603 | somevar=somevar 604 | ) 605 | assert self.cache.get(make_template_fragment_key("fragment3")) == somevar 606 | assert output == somevar 607 | 608 | #: Test backwards compatibility 609 | output = render_template_string( 610 | """{% cache 30 %}{{somevar}}{% endcache %}""", 611 | somevar=somevar) 612 | assert self.cache.get(make_template_fragment_key("None1")) == somevar 613 | assert output == somevar 614 | 615 | output = render_template_string( 616 | """{% cache 30, "fragment4", "fragment5"%}{{somevar}}{% endcache %}""", 617 | somevar=somevar) 618 | k = make_template_fragment_key("fragment4", vary_on=["fragment5"]) 619 | assert self.cache.get(k) == somevar 620 | assert output == somevar 621 | 622 | 623 | if 'TRAVIS' in os.environ: 624 | try: 625 | import redis 626 | has_redis = True 627 | except ImportError: 628 | has_redis = False 629 | 630 | if sys.version_info <= (2,7): 631 | 632 | class CacheMemcachedTestCase(CacheTestCase): 633 | def _set_app_config(self, app): 634 | app.config['CACHE_TYPE'] = 'memcached' 635 | 636 | class SpreadCacheMemcachedTestCase(CacheTestCase): 637 | def _set_app_config(self, app): 638 | app.config['CACHE_TYPE'] = 'spreadsaslmemcachedcache' 639 | 640 | 641 | class CacheRedisTestCase(CacheTestCase): 642 | def _set_app_config(self, app): 643 | app.config['CACHE_TYPE'] = 'redis' 644 | 645 | @unittest.skipUnless(has_redis, "requires Redis") 646 | def test_20_redis_url_default_db(self): 647 | config = { 648 | 'CACHE_TYPE': 'redis', 649 | 'CACHE_REDIS_URL': 'redis://localhost:6379', 650 | } 651 | cache = Cache() 652 | cache.init_app(self.app, config=config) 653 | from werkzeug.contrib.cache import RedisCache 654 | assert isinstance(self.app.extensions['cache'][cache], RedisCache) 655 | rconn = self.app.extensions['cache'][cache] \ 656 | ._client.connection_pool.get_connection('foo') 657 | assert rconn.db == 0 658 | 659 | @unittest.skipUnless(has_redis, "requires Redis") 660 | def test_21_redis_url_custom_db(self): 661 | config = { 662 | 'CACHE_TYPE': 'redis', 663 | 'CACHE_REDIS_URL': 'redis://localhost:6379/2', 664 | } 665 | cache = Cache() 666 | cache.init_app(self.app, config=config) 667 | rconn = self.app.extensions['cache'][cache] \ 668 | ._client.connection_pool.get_connection('foo') 669 | assert rconn.db == 2 670 | 671 | @unittest.skipUnless(has_redis, "requires Redis") 672 | def test_22_redis_url_explicit_db_arg(self): 673 | config = { 674 | 'CACHE_TYPE': 'redis', 675 | 'CACHE_REDIS_URL': 'redis://localhost:6379/2', 676 | 'CACHE_REDIS_DB': 1, 677 | } 678 | cache = Cache() 679 | cache.init_app(self.app, config=config) 680 | rconn = self.app.extensions['cache'][cache] \ 681 | ._client.connection_pool.get_connection('foo') 682 | assert rconn.db == 1 683 | 684 | 685 | class CacheFilesystemTestCase(CacheTestCase): 686 | def _set_app_config(self, app): 687 | app.config['CACHE_TYPE'] = 'filesystem' 688 | app.config['CACHE_DIR'] = '/tmp' 689 | 690 | 691 | if __name__ == '__main__': 692 | unittest.main() 693 | -------------------------------------------------------------------------------- /test_template.html: -------------------------------------------------------------------------------- 1 | {% cache 60, "fragment1" %}{{somevar}}{% endcache %} 2 | {% cache 60, "fragment1", "key1" %}{{somevar}}{% endcache %} 3 | {% cache 60, "fragment1", "key1", somevar %}{{somevar}}{% endcache %} 4 | {% cache timeout, "fragment2" %}{{somevar}}{% endcache %} -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | # Tox (http://tox.testrun.org/) is a tool for running tests 2 | # in multiple virtualenvs. This configuration file will run the 3 | # test suite on all supported python versions. To use it, "pip install tox" 4 | # and then run "tox" from this directory. 5 | 6 | [tox] 7 | envlist = py26, py27, py33 8 | 9 | [testenv] 10 | deps = -r{toxinidir}/requirements.txt 11 | unittest2 12 | commands = unit2 discover [] 13 | 14 | [testenv:py33] 15 | deps = -r{toxinidir}/requirements.txt 16 | unittest2py3k 17 | --------------------------------------------------------------------------------