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