Repository: pbrady/fastcache Branch: master Commit: 6b7bbed5076f Files: 19 Total size: 78.6 KB Directory structure: gitextract_4grauvkc/ ├── .gitignore ├── .travis.yml ├── CHANGELOG ├── LICENSE ├── MANIFEST.in ├── README.md ├── bin/ │ └── test_travis.sh ├── build.sh ├── fastcache/ │ ├── __init__.py │ ├── benchmark.py │ └── tests/ │ ├── __init__.py │ ├── test_clrucache.py │ ├── test_functools.py │ └── test_thread.py ├── meta.yaml ├── scripts/ │ └── threadsafety.py ├── setup.cfg ├── setup.py └── src/ └── _lrucache.c ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] # C extensions *.so # Distribution / packaging .Python env/ build/ develop-eggs/ dist/ eggs/ lib/ lib64/ parts/ sdist/ var/ *.egg-info/ .installed.cfg *.egg # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .coverage .cache nosetests.xml coverage.xml # Translations *.mo # Mr Developer .mr.developer.cfg .project .pydevproject # Rope .ropeproject # Django stuff: *.log *.pot # Sphinx documentation docs/_build/ ================================================ FILE: .travis.yml ================================================ language: python matrix: include: - arch: arm64 python: 2.7 - arch: amd64 python: 2.7 - arch: arm64 python: 3.4 - arch: amd64 python: 3.4 - arch: arm64 python: 3.5 - arch: amd64 python: 3.5 - arch: arm64 python: 3.6 - arch: amd64 python: 3.6 - arch: arm64 python: 3.7 - arch: amd64 python: 3.7 install: python setup.py install script: bash bin/test_travis.sh ================================================ FILE: CHANGELOG ================================================ *1.0.2* - use pytest for testing - Bug fix for windows compatibility *1.0.1* - better error checking so fastcache now plays well with signals. There is a performance hit for this. Next Release should handle this better. *1.0.0* - clru_cache now supports dynamic attributes. - (c)lru_cache is now threadsafe via custom reentrant locks. *0.4.3* - Fixed bug in hash computations which resulted in `stack overflow`. The appropriate error (RuntimeError) is now returned *0.4.2* - The 'state' argument to clru_cache can now be a list or a dict - Slight performance improvemants - Fixed compiler warnings for Python 2 builds - Use setuptools by default. The environment variable USE_DISTUTILS=True forces the use of distutils *0.4.0* API change: Default behavior of fastcache is changed to raise TypeError on unhashable arguments to be 100% consistent with stdlib. Introduce a new argument 'unhashable' which controls how fastcache responds to unhashable arguments: *'error' (default) - raise TypeError *'warning' - raise UserWarning and call decorated function with args *'ignore' - call decorated function ================================================ FILE: LICENSE ================================================ The MIT License (MIT) Copyright (c) 2014 Peter Brady Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: MANIFEST.in ================================================ include LICENSE ================================================ FILE: README.md ================================================ fastcache ========= [![Gitter](https://badges.gitter.im/Join Chat.svg)](https://gitter.im/pbrady/fastcache?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) C implementation of Python 3 lru_cache for Python 2.6, 2.7, 3.2, 3.3, 3.4 Passes all tests in the standard library for functools.lru_cache. Obeys same API as Python 3.3/3.4 functools.lru_cache with 2 enhancements: 1. An additional argument `state` may be supplied which must be a `list` or `dict`. This allows one to safely cache functions for which the result depends on some context which is not a part of the function call signature. 2. An additional argument `unhashable` may be supplied to control how the cached function responds to unhashable arguments. The options are: * "error" (default) - Raise a `TypeError` * "warning" - Raise a `UserWarning` and call the wrapped function with the supplied arguments. * "ignore" - Just call the wrapped function with the supplied arguments. Performance Warning ------- As of Python 3.5, the CPython interpreter implements `functools.lru_cache` in C. It is generally faster than this library due to its use of a more performant internal API for dictionaries (and perhaps other reasons). Therefore this library is only recommended for Python 2.6-3.4 Install ------- Via [pip](https://pypi.python.org/pypi/fastcache): pip install fastcache Manually : git clone https://github.com/pbrady/fastcache.git cd fastcache python setup.py install Via [conda](http://conda.pydata.org/docs/index.html) : * build latest and greatest github version ```bash git clone https://github.com/pbrady/fastcache.git conda-build fastcache conda install --use-local fastcache ``` * build latest released version on pypi ```bash git clone https://github.com/conda/conda-recipes.git conda-build conda-recipes/fastcache conda install --use-local fastcache ``` Test ---- ```python >>> import fastcache >>> fastcache.test() ``` Travis CI status : [![alt text][2]][1] [2]: https://travis-ci.org/pbrady/fastcache.svg?branch=master (Travis build status) [1]: http://travis-ci.org/pbrady/fastcache Tests include the official suite of tests from Python standard library for functools.lru_cache Use --- >>> from fastcache import clru_cache, __version__ >>> __version__ '0.3.3' >>> @clru_cache(maxsize=325, typed=False) ... def fib(n): ... """Terrible Fibonacci number generator.""" ... return n if n < 2 else fib(n-1) + fib(n-2) ... >>> fib(300) 222232244629420445529739893461909967206666939096499764990979600 >>> fib.cache_info() CacheInfo(hits=298, misses=301, maxsize=325, currsize=301) >>> print(fib.__doc__) Terrible Fibonacci number generator. >>> fib.cache_clear() >>> fib.cache_info() CacheInfo(hits=0, misses=0, maxsize=325, currsize=0) >>> fib.__wrapped__(300) 222232244629420445529739893461909967206666939096499764990979600 Speed ----- The speed up vs `lru_cache` provided by `functools` in 3.3 or 3.4 is 10x-30x depending on the function signature and whether one is comparing with 3.3 or 3.4. A sample run of the benchmarking suite for 3.3 is >>> import sys >>> sys.version_info sys.version_info(major=3, minor=3, micro=5, releaselevel='final', serial=0) >>> from fastcache import benchmark >>> benchmark.run() Test Suite 1 : Primarily tests cost of function call, hashing and cache hits. Benchmark script based on http://bugs.python.org/file28400/lru_cache_bench.py function call speed up untyped(i) 11.31, typed(i) 31.20 untyped("spam", i) 16.71, typed("spam", i) 27.50 untyped("spam", "spam", i) 14.24, typed("spam", "spam", i) 22.62 untyped(a=i) 13.25, typed(a=i) 23.92 untyped(a="spam", b=i) 10.51, typed(a="spam", b=i) 18.58 untyped(a="spam", b="spam", c=i) 9.34, typed(a="spam", b="spam", c=i) 16.40 min mean max untyped 9.337 12.559 16.706 typed 16.398 23.368 31.197 Test Suite 2 : Tests millions of misses and millions of hits to quantify cache behavior when cache is full. function call speed up untyped(i, j, a="spammy") 8.94, typed(i, j, a="spammy") 14.09 A sample run of the benchmarking suite for 3.4 is >>> import sys >>> sys.version_info sys.version_info(major=3, minor=4, micro=1, releaselevel='final', serial=0) >>> from fastcache import benchmark >>> benchmark.run() Test Suite 1 : Primarily tests cost of function call, hashing and cache hits. Benchmark script based on http://bugs.python.org/file28400/lru_cache_bench.py function call speed up untyped(i) 9.74, typed(i) 23.31 untyped("spam", i) 15.21, typed("spam", i) 20.82 untyped("spam", "spam", i) 13.35, typed("spam", "spam", i) 17.43 untyped(a=i) 12.27, typed(a=i) 19.04 untyped(a="spam", b=i) 9.81, typed(a="spam", b=i) 14.25 untyped(a="spam", b="spam", c=i) 7.77, typed(a="spam", b="spam", c=i) 11.61 min mean max untyped 7.770 11.359 15.210 typed 11.608 17.743 23.311 Test Suite 2 : Tests millions of misses and millions of hits to quantify cache behavior when cache is full. function call speed up untyped(i, j, a="spammy") 8.27, typed(i, j, a="spammy") 11.18 ================================================ FILE: bin/test_travis.sh ================================================ #! /usr/bin/env bash # Exit on error set -e # Echo each command set -x mkdir -p empty cd empty cat << EOF | python import fastcache if not fastcache.test(): raise Exception('Tests failed') EOF ================================================ FILE: build.sh ================================================ #!/bin/bash $PYTHON setup.py install ================================================ FILE: fastcache/__init__.py ================================================ """ C implementation of LRU caching. Provides 2 LRU caching function decorators: clru_cache - built-in (faster) >>> from fastcache import clru_cache >>> @clru_cache(maxsize=128,typed=False) ... def f(a, b): ... return (a, ) + (b, ) ... >>> type(f) >>> lru_cache - python wrapper around clru_cache (slower) >>> from fastcache import lru_cache >>> @lru_cache(maxsize=128,typed=False) ... def f(a, b): ... return (a, ) + (b, ) ... >>> type(f) >>> """ __version__ = "1.1.0" from ._lrucache import clru_cache from functools import update_wrapper def lru_cache(maxsize=128, typed=False, state=None, unhashable='error'): """Least-recently-used cache decorator. If *maxsize* is set to None, the LRU features are disabled and the cache can grow without bound. If *typed* is True, arguments of different types will be cached separately. For example, f(3.0) and f(3) will be treated as distinct calls with distinct results. If *state* is a list or dict, the items will be incorporated into argument hash. The result of calling the cached function with unhashable (mutable) arguments depends on the value of *unhashable*: If *unhashable* is 'error', a TypeError will be raised. If *unhashable* is 'warning', a UserWarning will be raised, and the wrapped function will be called with the supplied arguments. A miss will be recorded in the cache statistics. If *unhashable* is 'ignore', the wrapped function will be called with the supplied arguments. A miss will will be recorded in the cache statistics. View the cache statistics named tuple (hits, misses, maxsize, currsize) with f.cache_info(). Clear the cache and statistics with f.cache_clear(). Access the underlying function with f.__wrapped__. See: http://en.wikipedia.org/wiki/Cache_algorithms#Least_Recently_Used """ def func_wrapper(func): _cached_func = clru_cache(maxsize, typed, state, unhashable)(func) def wrapper(*args, **kwargs): return _cached_func(*args, **kwargs) wrapper.__wrapped__ = func wrapper.cache_info = _cached_func.cache_info wrapper.cache_clear = _cached_func.cache_clear return update_wrapper(wrapper,func) return func_wrapper def test(*args): import pytest, os return not pytest.main([os.path.dirname(os.path.abspath(__file__))] + list(args)) ================================================ FILE: fastcache/benchmark.py ================================================ """ Benchmark against functools.lru_cache. Benchmark script from http://bugs.python.org/file28400/lru_cache_bench.py with a few modifications. Not available for Py < 3.3. """ from __future__ import print_function import sys if sys.version_info[:2] >= (3, 3): import functools import fastcache import timeit from itertools import count def _untyped(*args, **kwargs): pass def _typed(*args, **kwargs): pass _py_untyped = functools.lru_cache(maxsize=100)(_untyped) _c_untyped = fastcache.clru_cache(maxsize=100)(_untyped) _py_typed = functools.lru_cache(maxsize=100, typed=True)(_typed) _c_typed = fastcache.clru_cache(maxsize=100, typed=True)(_typed) def _arg_gen(min=1, max=100, repeat=3): for i in range(min, max): for r in range(repeat): for j, k in zip(range(i), count(i, -1)): yield j, k def _print_speedup(results): print('') print('{:9s} {:>6s} {:>6s} {:>6s}'.format('','min', 'mean', 'max')) def print_stats(name,off0, off1): arr = [py[0]/c[0] for py, c in zip(results[off0::4], results[off1::4])] print('{:9s} {:6.3f} {:6.3f} {:6.3f}'.format(name, min(arr), sum(arr)/len(arr), max(arr))) print_stats('untyped', 0, 1) print_stats('typed', 2, 3) def _print_single_speedup(res=None, init=False): if init: print('{:29s} {:>8s}'.format('function call', 'speed up')) else: print('{:32s} {:5.2f}'.format(res[0][1].split('_')[-1], res[0][0]/res[1][0]), end = ', ') print('{:32s} {:5.2f}'.format(res[2][1].split('_')[-1], res[2][0]/res[3][0])) def run(): print("Test Suite 1 : ", end='\n\n') print("Primarily tests cost of function call, hashing and cache hits.") print("Benchmark script based on") print(" http://bugs.python.org/file28400/lru_cache_bench.py", end = '\n\n') _print_single_speedup(init=True) results = [] args = ['i', '"spam", i', '"spam", "spam", i', 'a=i', 'a="spam", b=i', 'a="spam", b="spam", c=i'] for a in args: for f in ['_py_untyped', '_c_untyped', '_py_typed', '_c_typed']: s = '%s(%s)' % (f, a) t = min(timeit.repeat(''' for i in range(100): {} '''.format(s), setup='from fastcache.benchmark import %s' % f, repeat=10, number=1000)) results.append([t, s]) _print_single_speedup(results[-4:]) _print_speedup(results) print("\n\nTest Suite 2 :", end='\n\n') print("Tests millions of misses and millions of hits to quantify") print("cache behavior when cache is full.", end='\n\n') setup = "from fastcache.benchmark import {}\n" + \ "from fastcache.benchmark import _arg_gen" results = [] for f in ['_py_untyped', '_c_untyped', '_py_typed', '_c_typed']: s = '%s(i, j, a="spammy")' % f t = min(timeit.repeat(''' for i, j in _arg_gen(): %s ''' % s, setup=setup.format(f), repeat=3, number=100)) results.append([t, s]) _print_single_speedup(init=True) _print_single_speedup(results) ================================================ FILE: fastcache/tests/__init__.py ================================================ ================================================ FILE: fastcache/tests/test_clrucache.py ================================================ import pytest import fastcache import itertools import warnings try: itertools.count(start=0, step=-1) count = itertools.count except TypeError: def count(start=0, step=1): i = step-1 for j, c in enumerate(itertools.count(start)): yield c + i*j def arg_gen(min=1, max=100, repeat=3): for i in range(min, max): for r in range(repeat): for j, k in zip(range(i), count(i, -1)): yield j, k @pytest.fixture(scope='module', params=[fastcache.clru_cache, fastcache.lru_cache]) def cache(request): param = request.param return param def test_function_attributes(cache): """ Simple tests for attribute preservation. """ def tfunc(a, b): """test function docstring.""" return a + b cfunc = cache()(tfunc) assert cfunc.__doc__ == tfunc.__doc__ assert hasattr(cfunc, 'cache_info') assert hasattr(cfunc, 'cache_clear') assert hasattr(cfunc, '__wrapped__') def test_function_cache(cache): """ Test that cache returns appropriate values. """ cat_tuples = [True] def tfunc(a, b, c=None): if (cat_tuples[0] == True): return (a, b, c) + (c, a) else: return 2*a-10*b cfunc = cache(maxsize=100, state=cat_tuples)(tfunc) for i, j in arg_gen(max=75, repeat=5): assert cfunc(i, j) == tfunc(i, j) # change extra state cat_tuples[0] = False for i, j in arg_gen(max=75, repeat=5): assert cfunc(i, j) == tfunc(i, j) # test dict state d = {} cfunc = cache(maxsize=100, state=d)(tfunc) cfunc(1, 2) assert cfunc.cache_info().misses == 1 d['a'] = 42 cfunc(1, 2) assert cfunc.cache_info().misses == 2 cfunc(1, 2) assert cfunc.cache_info().misses == 2 assert cfunc.cache_info().hits == 1 d.clear() cfunc(1, 2) assert cfunc.cache_info().misses == 2 assert cfunc.cache_info().hits == 2 d['a'] = 44 cfunc(1, 2) assert cfunc.cache_info().misses == 3 def test_memory_leaks(cache): """ Longer running test to check for memory leaks. """ def tfunc(a, b, c): return (a-1, 2*c) + (10*b-1, a*b, a*b+c) cfunc = cache(maxsize=2000)(tfunc) for i, j in arg_gen(max=1500, repeat=5): assert cfunc(i, j, c=i-j) == tfunc(i, j, c=i-j) def test_warn_unhashable_args(cache, recwarn): """ Function arguments must be hashable. """ @cache(unhashable='warning') def f(a, b): return (a, ) + (b, ) with warnings.catch_warnings() : warnings.simplefilter("always") assert f([1], 2) == f.__wrapped__([1], 2) w = recwarn.pop(UserWarning) assert issubclass(w.category, UserWarning) assert "Unhashable arguments cannot be cached" in str(w.message) assert w.filename assert w.lineno def test_ignore_unhashable_args(cache): """ Function arguments must be hashable. """ @cache(unhashable='ignore') def f(a, b): return (a, ) + (b, ) assert f([1], 2) == f.__wrapped__([1], 2) def test_default_unhashable_args(cache): @cache() def f(a, b): return (a, ) + (b, ) with pytest.raises(TypeError): f([1], 2) @cache(unhashable='error') def f(a, b): pass with pytest.raises(TypeError): f([1], 2) def test_state_type(cache): """ State must be a list or dict. """ f = lambda x : x with pytest.raises(TypeError): cache(state=(1, ))(f) with pytest.raises(TypeError): cache(state=-1)(f) def test_typed_False(cache): """ Verify typed==False. """ @cache(typed=False) def cfunc(a, b): return a+b # initialize cache with integer args cfunc(1, 2) assert cfunc(1, 2) is cfunc(1.0, 2) assert cfunc(1, 2) is cfunc(1, 2.0) # test keywords cfunc(1, b=2) assert cfunc(1,b=2) is cfunc(1.0,b=2) assert cfunc(1,b=2) is cfunc(1,b=2.0) def test_typed_True(cache): """ Verify typed==True. """ @cache(typed=True) def cfunc(a, b): return a+b assert cfunc(1, 2) is not cfunc(1.0, 2) assert cfunc(1, 2) is not cfunc(1, 2.0) # test keywords assert cfunc(1,b=2) is not cfunc(1.0,b=2) assert cfunc(1,b=2) is not cfunc(1,b=2.0) def test_dynamic_attribute(cache): f = lambda x : x cfunc = cache()(f) cfunc.new_attr = 5 assert cfunc.new_attr == 5 ================================================ FILE: fastcache/tests/test_functools.py ================================================ # Copied from python src Python-3.4.0/Lib/test/test_functools.py import abc import collections from itertools import permutations import pickle from random import choice import sys import unittest import fastcache import functools try: from functools import _CacheInfo except ImportError: _CacheInfo = collections.namedtuple("CacheInfo", ["hits", "misses", "maxsize", "currsize"]) class TestLRU(unittest.TestCase): def test_lru(self): def orig(x, y): return 3 * x + y f = fastcache.clru_cache(maxsize=20)(orig) hits, misses, maxsize, currsize = f.cache_info() self.assertEqual(maxsize, 20) self.assertEqual(currsize, 0) self.assertEqual(hits, 0) self.assertEqual(misses, 0) domain = range(5) for i in range(1000): x, y = choice(domain), choice(domain) actual = f(x, y) expected = orig(x, y) self.assertEqual(actual, expected) hits, misses, maxsize, currsize = f.cache_info() self.assertTrue(hits > misses) self.assertEqual(hits + misses, 1000) self.assertEqual(currsize, 20) f.cache_clear() # test clearing hits, misses, maxsize, currsize = f.cache_info() self.assertEqual(hits, 0) self.assertEqual(misses, 0) self.assertEqual(currsize, 0) f(x, y) hits, misses, maxsize, currsize = f.cache_info() self.assertEqual(hits, 0) self.assertEqual(misses, 1) self.assertEqual(currsize, 1) # Test bypassing the cache if hasattr(self, 'assertIs'): self.assertIs(f.__wrapped__, orig) f.__wrapped__(x, y) hits, misses, maxsize, currsize = f.cache_info() self.assertEqual(hits, 0) self.assertEqual(misses, 1) self.assertEqual(currsize, 1) # test size zero (which means "never-cache") @fastcache.clru_cache(0) def f(): #nonlocal f_cnt f_cnt[0] += 1 return 20 self.assertEqual(f.cache_info().maxsize, 0) f_cnt = [0] for i in range(5): self.assertEqual(f(), 20) self.assertEqual(f_cnt, [5]) hits, misses, maxsize, currsize = f.cache_info() self.assertEqual(hits, 0) self.assertEqual(misses, 5) self.assertEqual(currsize, 0) # test size one @fastcache.clru_cache(1) def f(): #nonlocal f_cnt f_cnt[0] += 1 return 20 self.assertEqual(f.cache_info().maxsize, 1) f_cnt[0] = 0 for i in range(5): self.assertEqual(f(), 20) self.assertEqual(f_cnt, [1]) hits, misses, maxsize, currsize = f.cache_info() self.assertEqual(hits, 4) self.assertEqual(misses, 1) self.assertEqual(currsize, 1) # test size two @fastcache.clru_cache(2) def f(x): #nonlocal f_cnt f_cnt[0] += 1 return x*10 self.assertEqual(f.cache_info().maxsize, 2) f_cnt[0] = 0 for x in 7, 9, 7, 9, 7, 9, 8, 8, 8, 9, 9, 9, 8, 8, 8, 7: # * * * * self.assertEqual(f(x), x*10) self.assertEqual(f_cnt, [4]) hits, misses, maxsize, currsize = f.cache_info() self.assertEqual(hits, 12) self.assertEqual(misses, 4) self.assertEqual(currsize, 2) def test_lru_with_maxsize_none(self): @fastcache.clru_cache(maxsize=None) def fib(n): if n < 2: return n return fib(n-1) + fib(n-2) self.assertEqual([fib(n) for n in range(16)], [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610]) self.assertEqual(fib.cache_info(), _CacheInfo(hits=28, misses=16, maxsize=None, currsize=16)) fib.cache_clear() self.assertEqual(fib.cache_info(), _CacheInfo(hits=0, misses=0, maxsize=None, currsize=0)) def test_lru_with_exceptions(self): # Verify that user_function exceptions get passed through without # creating a hard-to-read chained exception. # http://bugs.python.org/issue13177 for maxsize in (None, 128): @fastcache.clru_cache(maxsize) def func(i): return 'abc'[i] self.assertEqual(func(0), 'a') try: with self.assertRaises(IndexError) as cm: func(15) # Does not have this attribute in Py2 if hasattr(cm.exception,'__context__'): self.assertIsNone(cm.exception.__context__) # Verify that the previous exception did not result in a cached entry with self.assertRaises(IndexError): func(15) except TypeError: # py26 unittest wants assertRaises called with another arg if sys.version_info[:2] != (2, 6): raise else: pass def test_lru_with_types(self): for maxsize in (None, 128): @fastcache.clru_cache(maxsize=maxsize, typed=True) def square(x): return x * x self.assertEqual(square(3), 9) self.assertEqual(type(square(3)), type(9)) self.assertEqual(square(3.0), 9.0) self.assertEqual(type(square(3.0)), type(9.0)) self.assertEqual(square(x=3), 9) self.assertEqual(type(square(x=3)), type(9)) self.assertEqual(square(x=3.0), 9.0) self.assertEqual(type(square(x=3.0)), type(9.0)) self.assertEqual(square.cache_info().hits, 4) self.assertEqual(square.cache_info().misses, 4) def test_lru_with_keyword_args(self): @fastcache.clru_cache() def fib(n): if n < 2: return n return fib(n=n-1) + fib(n=n-2) self.assertEqual( [fib(n=number) for number in range(16)], [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610] ) self.assertEqual(fib.cache_info(), _CacheInfo(hits=28, misses=16, maxsize=128, currsize=16)) fib.cache_clear() self.assertEqual(fib.cache_info(), _CacheInfo(hits=0, misses=0, maxsize=128, currsize=0)) def test_lru_with_keyword_args_maxsize_none(self): @fastcache.clru_cache(maxsize=None) def fib(n): if n < 2: return n return fib(n=n-1) + fib(n=n-2) self.assertEqual([fib(n=number) for number in range(16)], [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610]) self.assertEqual(fib.cache_info(), _CacheInfo(hits=28, misses=16, maxsize=None, currsize=16)) fib.cache_clear() self.assertEqual(fib.cache_info(), _CacheInfo(hits=0, misses=0, maxsize=None, currsize=0)) def test_need_for_rlock(self): # This will deadlock on an LRU cache that uses a regular lock @fastcache.clru_cache(maxsize=10) def test_func(x): 'Used to demonstrate a reentrant lru_cache call within a single thread' return x class DoubleEq: 'Demonstrate a reentrant lru_cache call within a single thread' def __init__(self, x): self.x = x def __hash__(self): return self.x def __eq__(self, other): if self.x == 2: test_func(DoubleEq(1)) return self.x == other.x test_func(DoubleEq(1)) # Load the cache test_func(DoubleEq(2)) # Load the cache self.assertEqual(test_func(DoubleEq(2)), # Trigger a re-entrant __eq__ call DoubleEq(2)) # Verify the correct return value ================================================ FILE: fastcache/tests/test_thread.py ================================================ """The Python interpreter may switch between threads inbetween bytecode execution. Bytecode execution in fastcache may occur during: (1) Calls to make_key which will call the __hash__ methods of the args and (2) `PyDict_Get(Set)Item` calls rely on Python comparisons (i.e, __eq__) to determine if a match has been found A good test for threadsafety is then to cache a function which takes user defined Python objects that have __hash__ and __eq__ methods which live in Python land rather built-in land. The test should not only ensure that the correct result is acheived (and no segfaults) but also assess memory leaks. The thread switching interval can be altered using sys.setswitchinterval. """ class PythonInt: """ Wrapper for an integer with python versions of __eq__ and __hash__.""" def __init__(self, val): self.value = val def __hash__(self): return hash(self.value) def __eq__(self, other): # only compare with other instances of PythonInt if not isinstance(other, PythonInt): raise TypeError("PythonInt cannot be compared to %s" % type(other)) return self.value == other.value from random import randint import unittest from fastcache import clru_cache as lru_cache from threading import Thread try: from sys import setswitchinterval as setinterval except ImportError: from sys import setcheckinterval def setinterval(i): return setcheckinterval(int(i)) def run_threads(threads): for t in threads: t.start() for t in threads: t.join() CACHE_SIZE=301 FIB=CACHE_SIZE-1 RAND_MIN, RAND_MAX = 1, 10 @lru_cache(maxsize=CACHE_SIZE, typed=False) def fib(n): """Terrible Fibonacci number generator.""" v = n.value return v if v < 2 else fib(PythonInt(v-1)) + fib(PythonInt(v-2)) # establish correct result from single threaded exectution RESULT = fib(PythonInt(FIB)) def run_fib_with_clear(r): """ Run Fibonacci generator r times. """ for i in range(r): if randint(RAND_MIN, RAND_MAX) == RAND_MIN: fib.cache_clear() res = fib(PythonInt(FIB)) if RESULT != res: raise ValueError("Expected %d, Got %d" % (RESULT, res)) def run_fib_with_stats(r): """ Run Fibonacci generator r times. """ for i in range(r): res = fib(PythonInt(FIB)) if RESULT != res: raise ValueError("Expected %d, Got %d" % (RESULT, res)) class Test_Threading(unittest.TestCase): """ Threadsafety Tests for lru_cache. """ def setUp(self): setinterval(1e-6) self.numthreads = 4 self.repeat = 1000 def test_thread_random_cache_clears(self): """ randomly clear the cache during calls to fib. """ threads = [Thread(target=run_fib_with_clear, args=(self.repeat, )) for _ in range(self.numthreads)] run_threads(threads) # if we have gotten this far no exceptions have been raised self.assertEqual(0, 0) def test_thread_cache_info(self): """ Run thread safety test to make sure the cache statistics are correct.""" fib.cache_clear() threads = [Thread(target=run_fib_with_stats, args=(self.repeat, )) for _ in range(self.numthreads)] run_threads(threads) hits, misses, maxsize, currsize = fib.cache_info() self.assertEqual(misses, CACHE_SIZE) self.assertEqual(currsize, CACHE_SIZE) ================================================ FILE: meta.yaml ================================================ package: name: fastcache version: 0.4.0 source: git_url : https://github.com/pbrady/fastcache.git requirements: build: - python - setuptools run: - python test: # Python imports imports: - fastcache - fastcache.benchmark - fastcache.tests about: home: https://github.com/pbrady/fastcache.git license: MIT License summary: 'C implementation of Python 3 lru_cache' # See # http://docs.continuum.io/conda/build.html for # more information about meta.yaml ================================================ FILE: scripts/threadsafety.py ================================================ from __future__ import division """The Python interpreter may switch between threads inbetween bytecode execution. Bytecode execution in fastcache may occur during: (1) Calls to make_key which will call the __hash__ methods of the args and (2) `PyDict_Get(Set)Item` calls rely on Python comparisons (i.e, __eq__) to determine if a match has been found A good test for threadsafety is then to cache a function which takes user defined Python objects that have __hash__ and __eq__ methods which live in Python land rather built-in land. The test should not only ensure that the correct result is acheived (and no segfaults) but also assess memory leaks. The thread switching interval can be altered using sys.setswitchinterval. """ class PythonInt: """ Wrapper for an integer with python versions of __eq__ and __hash__.""" def __init__(self, val): self.value = val def __hash__(self): return hash(self.value) def __eq__(self, other): # only compare with other instances of PythonInt if not isinstance(other, PythonInt): raise TypeError("PythonInt cannot be compared to %s" % type(other)) return self.value == other.value from fastcache import clru_cache #from functools import lru_cache as clru_cache from random import randint CACHE_SIZE=301 FIB=CACHE_SIZE-1 RAND_MIN, RAND_MAX = 1, 10 @clru_cache(maxsize=CACHE_SIZE, typed=False) def fib(n): """Terrible Fibonacci number generator.""" v = n.value return v if v < 2 else fib2(PythonInt(v-1)) + fib(PythonInt(v-2)) @clru_cache(maxsize=CACHE_SIZE, typed=False) def fib2(n): """Terrible Fibonacci number generator.""" v = n.value return v if v < 2 else fib(PythonInt(v-1)) + fib2(PythonInt(v-2)) # establish correct result from single threaded exectution RESULT = fib(PythonInt(FIB)) def run_fib_with_clear(r): """ Run Fibonacci generator r times. """ for i in range(r): if randint(RAND_MIN, RAND_MAX) == RAND_MIN: fib.cache_clear() fib2.cache_clear() res = fib(PythonInt(FIB)) if RESULT != res: raise ValueError("Expected %d, Got %d" % (RESULT, res)) def run_fib_with_stats(r): """ Run Fibonacci generator r times. """ for i in range(r): res = fib(PythonInt(FIB)) if RESULT != res: raise ValueError("Expected %d, Got %d" % (RESULT, res)) from threading import Thread try: from sys import setswitchinterval as setinterval except ImportError: from sys import setcheckinterval def setinterval(i): return setcheckinterval(int(i)) def run_threads(threads): for t in threads: t.start() for t in threads: t.join() def run_test(n, r, i): """ Run thread safety test with n threads r times using interval i. """ setinterval(i) threads = [Thread(target=run_fib_with_clear, args=(r, )) for _ in range(n)] run_threads(threads) def run_test2(n, r, i): """ Run thread safety test to make sure the cache statistics are correct.""" fib.cache_clear() setinterval(i) threads = [Thread(target=run_fib_with_stats, args=(r, )) for _ in range(n)] run_threads(threads) hits, misses, maxsize, currsize = fib.cache_info() if misses != CACHE_SIZE//2+1: raise ValueError("Expected %d misses, Got %d" % (CACHE_SIZE//2+1, misses)) if maxsize != CACHE_SIZE: raise ValueError("Expected %d maxsize, Got %d" % (CACHE_SIZE, maxsize)) if currsize != CACHE_SIZE//2+1: raise ValueError("Expected %d currsize, Got %d" % (CACHE_SIZE//2+1, currsize)) import argparse def main(): parser = argparse.ArgumentParser(description='Run threadsafety test.') parser.add_argument('-n,--numthreads', type=int, default=2, dest='n', help='Number of threads.') parser.add_argument('-r,--repeat', type=int, default=5000, dest='r', help='Number of times to repeat test. Larger numbers '+ 'will make it easier to spot memory leaks.') parser.add_argument('-i,--interval', type=float, default=1e-6, dest='i', help='Time in seconds for sys.setswitchinterval.') run_test(**dict(vars(parser.parse_args()))) run_test2(**dict(vars(parser.parse_args()))) if __name__ == "__main__": main() ================================================ FILE: setup.cfg ================================================ [metadata] description-file = README.md ================================================ FILE: setup.py ================================================ import sys from os import getenv # use setuptools by default as per the official advice at: # packaging.python.org/en/latest/current.html#packaging-tool-recommendations use_setuptools = True # set the environment variable USE_DISTUTILS=True to force the use of distutils use_distutils = getenv('USE_DISTUTILS') if use_distutils is not None: if use_distutils.lower() == 'true': use_setuptools = False else: print("Value {} for USE_DISTUTILS treated as False".\ format(use_distutils)) from distutils.command.build import build as _build if use_setuptools: try: from setuptools import setup, Extension from setuptools.command.install import install as _install from setuptools.command.build_ext import build_ext as _build_ext except ImportError: use_setuptools = False if not use_setuptools: from distutils.core import setup, Extension from distutils.command.install import install as _install from distutils.command.build_ext import build_ext as _build_ext vinfo = sys.version_info[:2] if vinfo < (2, 6): print("Fastcache currently requires Python 2.6 or newer. "+ "Python {}.{} detected".format(*vinfo)) sys.exit(-1) if vinfo[0] == 3 and vinfo < (3, 2): print("Fastcache currently requires Python 3.2 or newer. "+ "Python {}.{} detected".format(*vinfo)) sys.exit(-1) classifiers = [ 'License :: OSI Approved :: MIT License', 'Operating System :: OS Independent', 'Programming Language :: Python', 'Programming Language :: Python :: 2', 'Programming Language :: Python :: 2.6', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.2', 'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: 3.4', 'Programming Language :: C', ] long_description = ''' C implementation of Python 3 functools.lru_cache. Provides speedup of 10-30x over standard library. Passes test suite from standard library for lru_cache. Provides 2 Least Recently Used caching function decorators: clru_cache - built-in (faster) >>> from fastcache import clru_cache, __version__ >>> __version__ '1.1.0' >>> @clru_cache(maxsize=325, typed=False) ... def fib(n): ... """Terrible Fibonacci number generator.""" ... return n if n < 2 else fib(n-1) + fib(n-2) ... >>> fib(300) 222232244629420445529739893461909967206666939096499764990979600 >>> fib.cache_info() CacheInfo(hits=298, misses=301, maxsize=325, currsize=301) >>> print(fib.__doc__) Terrible Fibonacci number generator. >>> fib.cache_clear() >>> fib.cache_info() CacheInfo(hits=0, misses=0, maxsize=325, currsize=0) >>> fib.__wrapped__(300) 222232244629420445529739893461909967206666939096499764990979600 >>> type(fib) >>> lru_cache - python wrapper around clru_cache >>> from fastcache import lru_cache >>> @lru_cache(maxsize=128, typed=False) ... def f(a, b): ... pass ... >>> type(f) >>> (c)lru_cache(maxsize=128, typed=False, state=None, unhashable='error') Least-recently-used cache decorator. If *maxsize* is set to None, the LRU features are disabled and the cache can grow without bound. If *typed* is True, arguments of different types will be cached separately. For example, f(3.0) and f(3) will be treated as distinct calls with distinct results. If *state* is a list or dict, the items will be incorporated into the argument hash. The result of calling the cached function with unhashable (mutable) arguments depends on the value of *unhashable*: If *unhashable* is 'error', a TypeError will be raised. If *unhashable* is 'warning', a UserWarning will be raised, and the wrapped function will be called with the supplied arguments. A miss will be recorded in the cache statistics. If *unhashable* is 'ignore', the wrapped function will be called with the supplied arguments. A miss will will be recorded in the cache statistics. View the cache statistics named tuple (hits, misses, maxsize, currsize) with f.cache_info(). Clear the cache and statistics with f.cache_clear(). Access the underlying function with f.__wrapped__. See: http://en.wikipedia.org/wiki/Cache_algorithms#Least_Recently_Used ''' # the overall logic here is that by default macros can be only be passed if # one does 'python setup.py build_ext --define=MYMACRO' # If one attempts 'build' or 'install' with the --define flag, an error will # appear saying that --define is not an option # To get around this issue, we subclass build and install to capture --define # as well as build_ext which will use the --define arguments passed to # build or install define_opts = [] class BuildWithDefine(_build): _build_opts = _build.user_options user_options = [ ('define=', 'D', "C preprocessor macros to define"), ] user_options.extend(_build_opts) def initialize_options(self): _build.initialize_options(self) self.define = None def finalize_options(self): _build.finalize_options(self) # The argument parsing will result in self.define being a string, but # it has to be a list of 2-tuples. All the preprocessor symbols # specified by the 'define' option without an '=' will be set to '1'. # Multiple symbols can be separated with commas. if self.define: defines = self.define.split(',') self.define = [(s.strip(), 1) if '=' not in s else tuple(ss.strip() for ss in s.split('=')) for s in defines] define_opts.extend(self.define) def run(self): _build.run(self) class InstallWithDefine(_install): _install_opts = _install.user_options user_options = [ ('define=', 'D', "C preprocessor macros to define"), ] user_options.extend(_install_opts) def initialize_options(self): _install.initialize_options(self) self.define = None def finalize_options(self): _install.finalize_options(self) # The argument parsing will result in self.define being a string, but # it has to be a list of 2-tuples. All the preprocessor symbols # specified by the 'define' option without an '=' will be set to '1'. # Multiple symbols can be separated with commas. if self.define: defines = self.define.split(',') self.define = [(s.strip(), 1) if '=' not in s else tuple(ss.strip() for ss in s.split('=')) for s in defines] define_opts.extend(self.define) def run(self): _install.run(self) class BuildExt(_build_ext): def initialize_options(self): _build_ext.initialize_options(self) def finalize_options(self): _build_ext.finalize_options(self) if self.define is not None: self.define.extend(define_opts) elif define_opts: self.define = define_opts def run(self): _build_ext.run(self) setup(name = "fastcache", version = "1.1.0", description = "C implementation of Python 3 functools.lru_cache", long_description = long_description, author = "Peter Brady", author_email = "petertbrady@gmail.com", license = "MIT", url = "https://github.com/pbrady/fastcache", packages = ["fastcache", "fastcache.tests"], ext_modules = [Extension("fastcache._lrucache",["src/_lrucache.c"])], classifiers = classifiers, cmdclass={ 'build' : BuildWithDefine, 'install' : InstallWithDefine, 'build_ext' : BuildExt, } ) ================================================ FILE: src/_lrucache.c ================================================ #include #include "structmember.h" #include "pythread.h" #ifdef __cplusplus extern "C" { #endif #if PY_MAJOR_VERSION == 2 #define _PY2 typedef long Py_hash_t; #endif #if PY_MAJOR_VERSION == 3 && PY_MINOR_VERSION == 2 #define _PY32 #endif #ifdef LLTRACE #define TBEGIN(x, line) printf("Beginning Trace of %s at lineno %d....", x); #define TEND(x) printf("Finished!\n") #else #define TBEGIN(x, line) #define TEND(x) #endif #ifdef WITH_THREAD #ifdef _PY2 typedef int PyLockStatus; static PyLockStatus PY_LOCK_FAILURE = 0; static PyLockStatus PY_LOCK_ACQUIRED = 1; static PyLockStatus PY_LOCK_INTR = -999999; #endif static int rlock_acquire(PyThread_type_lock lock, long* rlock_owner, unsigned long* rlock_count) { long tid; PyLockStatus r; tid = PyThread_get_thread_ident(); if (*rlock_count > 0 && tid == (*rlock_owner)) { unsigned long count = *rlock_count + 1; if (count <= *rlock_count) { PyErr_SetString(PyExc_OverflowError, "Internal lock count overflowed"); return -1; } *rlock_count = count; return 1; } /* do/while loop from acquire_timed */ do { /* first a simple non-blocking try without releasing the GIL */ #ifdef _PY2 r = PyThread_acquire_lock(lock, 0); #else r = PyThread_acquire_lock_timed(lock, 0, 0); #endif if (r == PY_LOCK_FAILURE) { Py_BEGIN_ALLOW_THREADS #ifdef _PY2 r = PyThread_acquire_lock(lock, 1); #else r = PyThread_acquire_lock_timed(lock, -1, 1); #endif Py_END_ALLOW_THREADS } if (r == PY_LOCK_INTR) { /* Run signal handlers if we were interrupted. Propagate * exceptions from signal handlers, such as KeyboardInterrupt, by * passing up PY_LOCK_INTR. */ if (Py_MakePendingCalls() < 0) { return -1; } } } while (r == PY_LOCK_INTR); /* Retry if we were interrupted. */ if (r == PY_LOCK_ACQUIRED) { *rlock_owner = tid; *rlock_count = 1; return 1; } return -1; } static int rlock_release(PyThread_type_lock lock, long* rlock_owner, unsigned long* rlock_count) { long tid = PyThread_get_thread_ident(); if (*rlock_count == 0 || *rlock_owner != tid) { PyErr_SetString(PyExc_RuntimeError, "cannot release un-acquired lock"); return -1; } if (--(*rlock_count) == 0) { *rlock_owner = 0; PyThread_release_lock(lock); } return 1; } #define ACQUIRE_LOCK(obj) rlock_acquire((obj)->lock, &((obj)->rlock_owner), &((obj)->rlock_count)) #define RELEASE_LOCK(obj) rlock_release((obj)->lock, &((obj)->rlock_owner), &((obj)->rlock_count)) #define FREE_LOCK(obj) PyThread_free_lock((obj)->lock) #else #define ACQUIRE_LOCK(obj) 1 #define RELEASE_LOCK(obj) 1 #define FREE_LOCK(obj) #endif #define INC_RETURN(op) return Py_INCREF(op), (op) // THREAD SAFETY NOTES: // Python bytecode instructions are atomic but the GIL may switch between // threads in between instructions. // To make this threadsafe care needs to be taken one such that global objects // are left in a consistent between calls to python bytecode. // The relevant global objects are co->root, and co->cache_dict // The stats are global as well but are modified in one line: stat++ /* HashedArgs -- internal *****************************************/ typedef struct { PyObject_HEAD PyObject *args; Py_hash_t hashvalue; } HashedArgs; static void HashedArgs_dealloc(HashedArgs *self) { Py_XDECREF(self->args); Py_TYPE(self)->tp_free(self); return; } /* return precomputed tuple hash for speed */ static Py_hash_t HashedArgs_hash(HashedArgs *self) { return self->hashvalue; } /* Delegate comparison to tuples */ static PyObject * HashedArgs_richcompare(PyObject *v, PyObject *w, int op) { HashedArgs *hv = (HashedArgs *) v; HashedArgs *hw = (HashedArgs *) w; PyObject *res = PyObject_RichCompare(hv->args, hw->args, op); return res; } static PyTypeObject HashedArgs_type = { PyVarObject_HEAD_INIT(NULL, 0) "_lrucache.HashedArgs", /* tp_name */ sizeof(HashedArgs), /* tp_basicsize */ 0, /* tp_itemsize */ (destructor)HashedArgs_dealloc, /* tp_dealloc */ 0, /* tp_print */ 0, /* tp_getattr */ 0, /* tp_setattr */ 0, /* tp_reserved */ 0, /* tp_repr */ 0, /* tp_as_number */ 0, /* tp_as_sequence */ 0, /* tp_as_mapping */ (hashfunc)HashedArgs_hash, /* tp_hash */ 0, /* tp_call */ 0, /* tp_str */ 0, /* tp_getattro */ 0, /* tp_setattro */ 0, /* tp_as_buffer */ Py_TPFLAGS_DEFAULT, /* tp_flags */ 0, /* tp_doc */ 0, /* tp_traverse */ 0, /* tp_clear */ HashedArgs_richcompare, /* tp_richcompare */ }; /*************************************************** End of HashedArgs ***************************************************/ /*********************************************************** circular doubly linked list ************************************************************/ typedef struct clist{ PyObject_HEAD struct clist *prev; struct clist *next; PyObject *key; PyObject *result; } clist; static void clist_dealloc(clist *co) { clist *prev = co->prev; clist *next = co->next; // THREAD SAFETY NOTES: // Calls to DECREF can result in bytecode and thread switching. // Do DECREF after the linked list has been modified and is in // an acceptable state. if(prev != co){ // adjust neighbor pointers prev->next = next; next->prev = prev; } co->prev = NULL; co->next = NULL; Py_XDECREF(co->key); Py_XDECREF(co->result); Py_TYPE(co)->tp_free(co); return; } static PyTypeObject clist_type = { PyVarObject_HEAD_INIT(NULL, 0) "_lrucache.clist", /* tp_name */ sizeof(clist), /* tp_basicsize */ 0, /* tp_itemsize */ (destructor)clist_dealloc, /* tp_dealloc */ 0, /* tp_print */ 0, /* tp_getattr */ 0, /* tp_setattr */ 0, /* tp_reserved */ 0, /* tp_repr */ 0, /* tp_as_number */ 0, /* tp_as_sequence */ 0, /* tp_as_mapping */ 0, /* tp_hash */ 0, /* tp_call */ 0, /* tp_str */ 0, /* tp_getattro */ 0, /* tp_setattro */ 0, /* tp_as_buffer */ Py_TPFLAGS_DEFAULT, /* tp_flags */ }; static int insert_first(clist *root, PyObject *key, PyObject *result){ // first element will be inserted at root->next clist *first = PyObject_New(clist, &clist_type); clist *oldfirst = root->next; if(!first) return -1; first->result = result; // This will be the only reference to key (HashedArgs), do not INCREF first->key = key; root->next = first; first->next = oldfirst; first->prev = root; oldfirst->prev = first; // INCREF result since it will be used by clist and returned to the caller return Py_INCREF(result), 1; } static PyObject * make_first(clist *root, clist *node){ // make node the first node and return new reference to result // save previous first position clist *oldfirst = root->next; if (oldfirst != node) { // first adjust pointers around node's position node->prev->next = node->next; node->next->prev = node->prev; root->next = node; node->next = oldfirst; node->prev = root; oldfirst->prev = node; } INC_RETURN(node->result); } /********************************************************** cachedobject is the actual function with the cached results ***********************************************************/ /* how will unhashable arguments be handled */ enum unhashable {FC_ERROR, FC_WARNING, FC_IGNORE, FC_FAIL}; typedef struct { PyObject_HEAD PyObject *fn ; // original function PyObject *func_module, *func_name, *func_qualname, *func_annotations; PyObject *func_dict; PyObject *cache_dict; PyObject *ex_state; int typed; enum unhashable err; PyObject *cinfo; // named tuple constructor Py_ssize_t maxsize, hits, misses; clist *root; // lock for cache access #ifdef WITH_THREAD PyThread_type_lock lock; long rlock_owner; unsigned long rlock_count; #endif } cacheobject ; #define OFF(x) offsetof(cacheobject, x) // attributes from wrapped function static PyMemberDef cache_memberlist[] = { {"__wrapped__", T_OBJECT, OFF(fn), RESTRICTED | READONLY}, {"__module__", T_OBJECT, OFF(func_module), RESTRICTED | READONLY}, {"__name__", T_OBJECT, OFF(func_name), RESTRICTED | READONLY}, {"__qualname__",T_OBJECT, OFF(func_qualname), RESTRICTED | READONLY}, {"__annotations__", T_OBJECT, OFF(func_annotations), RESTRICTED | READONLY}, {NULL} /* Sentinel */ }; // getsetters from wrapped function static PyObject * cache_get_doc(cacheobject * co, void *closure) { PyFunctionObject * fn = (PyFunctionObject *) co->fn; if (fn->func_doc == NULL) Py_RETURN_NONE; INC_RETURN(fn->func_doc); } #if defined(_PY2) || defined (_PY32) static int restricted(void) { #ifdef _PY2 if (!PyEval_GetRestricted()) #endif return 0; PyErr_SetString(PyExc_RuntimeError, "function attributes not accessible in restricted mode"); return 1; } static PyObject * func_get_dict(PyFunctionObject *op) { if (restricted()) return NULL; if (op->func_dict == NULL) { op->func_dict = PyDict_New(); if (op->func_dict == NULL) return NULL; } Py_INCREF(op->func_dict); return op->func_dict; } static int func_set_dict(PyFunctionObject *op, PyObject *value) { PyObject *tmp; if (restricted()) return -1; /* It is illegal to del f.func_dict */ if (value == NULL) { PyErr_SetString(PyExc_TypeError, "function's dictionary may not be deleted"); return -1; } /* Can only set func_dict to a dictionary */ if (!PyDict_Check(value)) { PyErr_SetString(PyExc_TypeError, "setting function's dictionary to a non-dict"); return -1; } tmp = op->func_dict; Py_INCREF(value); op->func_dict = value; Py_XDECREF(tmp); return 0; } static PyGetSetDef cache_getset[] = { {"__doc__", (getter)cache_get_doc, NULL, NULL, NULL}, {"__dict__", (getter)func_get_dict, (setter)func_set_dict}, {NULL} /* Sentinel */ }; #else static PyGetSetDef cache_getset[] = { {"__doc__", (getter)cache_get_doc, NULL, NULL, NULL}, {"__dict__", PyObject_GenericGetDict, PyObject_GenericSetDict}, {NULL} /* Sentinel */ }; #endif /* Bind a function to an object */ static PyObject * cache_descr_get(PyObject *func, PyObject *obj, PyObject *type) { if (obj == Py_None || obj == NULL) INC_RETURN(func); #ifdef _PY2 return PyMethod_New(func, obj, type); #else return PyMethod_New(func, obj); #endif } static void cache_dealloc(cacheobject *co) { Py_CLEAR(co->fn); Py_CLEAR(co->func_module); Py_CLEAR(co->func_name); Py_CLEAR(co->func_qualname); Py_CLEAR(co->func_annotations); Py_CLEAR(co->func_dict); Py_CLEAR(co->cache_dict); Py_CLEAR(co->ex_state); Py_CLEAR(co->cinfo); Py_CLEAR(co->root); FREE_LOCK(co); Py_TYPE(co)->tp_free(co); } /* * attempt to set hs->hashvalue to hash(hs->args) Does not do alter any * reference counts. Returns NULL on error. If hs->hashvalue==-1 on return * then hs->args is Unhashable */ static PyObject * set_hash_value(cacheobject *co, HashedArgs *hs) { if ((hs->hashvalue = PyObject_Hash(hs->args)) == -1) { // unhashable if (co->err == FC_ERROR) { return NULL; } // if error was something other than a TypeError, exit if (!PyErr_GivenExceptionMatches(PyErr_Occurred(), PyExc_TypeError)) { return NULL; } PyErr_Clear(); if (co->err == FC_WARNING) { // try to issue warning if( PyErr_WarnEx(PyExc_UserWarning, "Unhashable arguments cannot be cached",1) < 0){ // warning becomes exception PyErr_SetString(PyExc_TypeError, "Cached function arguments must be hashable"); return NULL; } } } // success! return (PyObject *) hs; } // compute the hash of function args and kwargs // THREAD SAFTEY NOTES: // We access global data: co->ex_state and co->typed. // These data are defined at co creation time and are not // changed so we do not need to worry about thread safety here static PyObject * make_key(cacheobject *co, PyObject *args, PyObject *kw) { PyObject *item, *keys, *key; Py_ssize_t ex_size = 0; Py_ssize_t arg_size = 0; Py_ssize_t kw_size = 0; Py_ssize_t i, size, off; HashedArgs *hs; int is_list = 1; // determine size of arguments and types if (PyList_Check(co->ex_state)) ex_size = Py_SIZE(co->ex_state); else if (PyDict_CheckExact(co->ex_state)){ is_list = 0; ex_size = PyDict_Size(co->ex_state); } if (args && PyTuple_CheckExact(args)) arg_size = PyTuple_GET_SIZE(args); if (kw && PyDict_CheckExact(kw)) kw_size = PyDict_Size(kw); // allocate HashedArgs Object if(!(hs = PyObject_New(HashedArgs, &HashedArgs_type))) return NULL; // total size if (co->typed) size = (2-is_list)*ex_size+2*arg_size+3*kw_size; else size = (2-is_list)*ex_size+arg_size+2*kw_size; // initialize new tuple if(!(hs->args = PyTuple_New(size))){ return NULL; } // incorporate extra state if(is_list){ for(i = 0; i < ex_size; i++){ PyObject *tmp = PyList_GET_ITEM(co->ex_state, i); PyTuple_SET_ITEM(hs->args, i, tmp); Py_INCREF(tmp); } } else if(ex_size > 0){ if(!(keys = PyDict_Keys(co->ex_state))){ Py_DECREF(hs); return NULL; } if( PyList_Sort(keys) < 0){ Py_DECREF(keys); Py_DECREF(hs); return NULL; } for(i = 0; i < ex_size; i++){ key = PyList_GET_ITEM(keys, i); Py_INCREF(key); PyTuple_SET_ITEM(hs->args, 2*i, key); if(!(item = PyDict_GetItem(co->ex_state, key))){ Py_DECREF(keys); Py_DECREF(hs); return NULL; } Py_INCREF(item); PyTuple_SET_ITEM(hs->args, 2*i+1, item); } Py_DECREF(keys); } off = (2-is_list)*ex_size; // incorporate arguments for(i = 0; i < arg_size; i++){ PyObject *tmp = PyTuple_GET_ITEM(args, i); PyTuple_SET_ITEM(hs->args, off+i, tmp); Py_INCREF(tmp); if(co->typed) { off += 1; tmp = (PyObject *)Py_TYPE(tmp); Py_INCREF(tmp); PyTuple_SET_ITEM(hs->args, off+i, tmp); } } off += arg_size; // incorporate keyword arguments if(kw_size > 0){ if(!(keys = PyDict_Keys(kw))){ Py_DECREF(hs); return NULL; } if( PyList_Sort(keys) < 0){ Py_DECREF(keys); Py_DECREF(hs); return NULL; } for(i = 0; i < kw_size; i++){ key = PyList_GET_ITEM(keys, i); Py_INCREF(key); PyTuple_SET_ITEM(hs->args, off+i, key); if(!(item = PyDict_GetItem(kw, key))){ Py_DECREF(keys); Py_DECREF(hs); return NULL; } off += 1; Py_INCREF(item); PyTuple_SET_ITEM(hs->args, off+i, item); if (co->typed){ off += 1; item = (PyObject *)Py_TYPE(item); Py_INCREF(item); PyTuple_SET_ITEM(hs->args, off+i, item); } } Py_DECREF(keys); } // check for an error we may have missed if( PyErr_Occurred() ){ Py_DECREF(hs); return NULL; } // set hash value if( !set_hash_value(co, hs) ) { Py_DECREF(hs); return NULL; } return (PyObject *)hs; } /*********************************************************** * All calls to the cached function go through cache_call * Handles: (1) Generation of key (via make_key) * (2) Maintenance of circular doubly linked list * (3) Actual updates to cache dictionary * THREAD SAFETY NOTES: * 1. The GIL may switch threads between all PyDict_Get/Set/DelItem * If another thread were to call cache_clear while the dict was in * an indetermined state, that could be very very bad. Must lock all * updates to cache_dict ***********************************************************/ static PyObject * cache_call(cacheobject *co, PyObject *args, PyObject *kw) { PyObject *key, *result, *link, *first; /* no cache, just update stats and return */ if (co->maxsize == 0) { co->misses++; return PyObject_Call(co->fn, args, kw); } // generate a key from hashing the arguments // THREAD SAFETY NOTES: // Computing the hash will result in many potential calls to __hash__ // methods, allowing the GIL to switch threads. Thus it is possible that // two threads have called this function with the exact same arguments // and are constructing keys key = make_key(co, args, kw); if (!key) return NULL; /* check for unhashable type */ if ( ((HashedArgs *)key)->hashvalue == -1){ // no locking neccessary here Py_DECREF(key); co->misses++; return PyObject_Call(co->fn, args, kw); } /* For an unbounded cache, link is simply the result of the function call * For an LRU cache, link is a pointer to a clist node */ if(ACQUIRE_LOCK(co) == -1){ Py_DECREF(key); return NULL; } link = PyDict_GetItem(co->cache_dict, key); if(PyErr_Occurred()){ RELEASE_LOCK(co); Py_XDECREF(link); Py_DECREF(key); return NULL; } if(RELEASE_LOCK(co) == -1){ Py_XDECREF(link); Py_DECREF(key); return NULL; } if (!link){ result = PyObject_Call(co->fn, args, kw); // result refcount is one if(PyErr_Occurred() || !result){ Py_XDECREF(result); Py_DECREF(key); return NULL; } /* Unbounded cache, no clist maintenance, no locks needed */ if (co->maxsize < 0){ if( PyDict_SetItem(co->cache_dict, key, result) == -1 || PyErr_Occurred()){ Py_DECREF(key); Py_DECREF(result); return NULL; } Py_DECREF(key); return co->misses++, result; } /* Least Recently Used cache */ /* Need to reacquire the lock here and make sure that the key,result were * not added to the cache while we were waiting */ if(ACQUIRE_LOCK(co) == -1){ Py_DECREF(key); Py_DECREF(result); return NULL; } #ifdef WITH_THREAD link = PyDict_GetItem(co->cache_dict, key); if(PyErr_Occurred()){ RELEASE_LOCK(co); Py_DECREF(key); Py_DECREF(result); Py_XDECREF(link); return NULL; } if(link){ Py_DECREF(key); if(RELEASE_LOCK(co) == -1){ Py_DECREF(result); return NULL; } return co->hits++, result; } #endif /* if cache is full, repurpose the last link rather than * passing it off to garbage collection. */ if (((PyDictObject *)co->cache_dict)->ma_used == co->maxsize){ /* Note that the old key will be used to delete the link from the dictionary * Be sure to INCREF old link so we don't lose it before * we add it when the PyDict_DelItem occurs */ clist *last = co->root->prev; PyObject *old_key = last->key; PyObject *old_res = last->result; // set new items last->key = key; last->result = result; // bump to the front (get back the result we just set). result = make_first(co->root, last); // Increase ref count of repurposed link so we don't trigger GC // save the first position since the global co->root->next may change first = (PyObject *) co->root->next; // Increases first->refcount to 2 if(PyDict_SetItem(co->cache_dict, key, first) == -1){ Py_DECREF(first); Py_DECREF(first); Py_DECREF(key); Py_DECREF(old_key); Py_DECREF(old_res); Py_DECREF(result); RELEASE_LOCK(co); return NULL; } // handle deletions if(PyDict_DelItem(co->cache_dict, old_key) == -1){ Py_DECREF(old_key); Py_DECREF(old_res); Py_DECREF(result); RELEASE_LOCK(co); return NULL; } // These would have been decrefed had we simply deleted the link Py_DECREF(old_key); Py_DECREF(old_res); if(PyErr_Occurred()){ Py_DECREF(result); RELEASE_LOCK(co); return NULL; } if(RELEASE_LOCK(co) == -1){ Py_DECREF(result); return NULL; } return co->misses++, result; } else { if(insert_first(co->root, key, result) < 0) { Py_DECREF(key); Py_DECREF(result); RELEASE_LOCK(co); return NULL; } first = (PyObject *) co->root->next; // insert_first sets refcount to 1 // key and first count++ if(PyDict_SetItem(co->cache_dict, key, first) == -1 || PyErr_Occurred()){ Py_DECREF(first); Py_DECREF(result); RELEASE_LOCK(co); return NULL; } Py_DECREF(first); // Don't DECREF key here since we want both the dict and the node 'first' // To be able to have a valid copy co->misses++; if(RELEASE_LOCK(co) == -1){ Py_DECREF(result); return NULL; } return result; } } // link != NULL else { if( co->maxsize < 0){ Py_DECREF(key); co->hits++; INC_RETURN(link); } /* bump link to the front of the list and get result from link */ result = make_first(co->root, (clist *) link); Py_DECREF(key); co->hits++; return result; } } PyDoc_STRVAR(cacheclear__doc__, "cache_clear(self)\n\ \n\ Clear the cache and cache statistics."); static PyObject * cache_clear(PyObject *self) { cacheobject *co = (cacheobject *)self; // delete dictionary - use a lock to keep dict in a fully determined state if(ACQUIRE_LOCK(co) == -1) return NULL; PyDict_Clear(co->cache_dict); co->hits = 0; co->misses = 0; if(RELEASE_LOCK(co) == -1) return NULL; Py_RETURN_NONE; } PyDoc_STRVAR(cacheinfo__doc__, "cache_info(self)\n\ \n\ Report cache statistics."); static PyObject * cache_info(PyObject *self) { cacheobject * co = (cacheobject *) self; if (co->maxsize >= 0) return PyObject_CallFunction(co->cinfo,"nnnn",co->hits, co->misses, co->maxsize, ((PyDictObject *)co->cache_dict)->ma_used); else return PyObject_CallFunction(co->cinfo,"nnOn",co->hits, co->misses, Py_None, ((PyDictObject *)co->cache_dict)->ma_used); } static PyMethodDef cache_methods[] = { {"cache_clear", (PyCFunction) cache_clear, METH_NOARGS, cacheclear__doc__}, {"cache_info", (PyCFunction) cache_info, METH_NOARGS, cacheinfo__doc__}, {NULL, NULL} /* sentinel */ }; PyDoc_STRVAR(fn_doc, "Cached function."); static PyTypeObject cache_type = { PyVarObject_HEAD_INIT(NULL, 0) "fastcache.clru_cache", /* tp_name */ sizeof(cacheobject), /* tp_basicsize */ 0, /* tp_itemsize */ /* methods */ (destructor)cache_dealloc, /* tp_dealloc */ 0, /* tp_print */ 0, /* tp_getattr */ 0, /* tp_setattr */ 0, /* tp_reserved */ 0, /* tp_repr */ 0, /* tp_as_number */ 0, /* tp_as_sequence */ 0, /* tp_as_mapping */ 0, /* tp_hash */ (ternaryfunc)cache_call, /* tp_call */ 0, /* tp_str */ 0, /* tp_getattro */ 0, /* tp_setattro */ 0, /* tp_as_buffer */ Py_TPFLAGS_DEFAULT , /* tp_flags */ fn_doc, /* tp_doc */ 0, /* tp_traverse */ 0, /* tp_clear */ 0, /* tp_richcompare */ 0, /* tp_weaklistoffset */ 0, /* tp_iter */ 0, /* tp_iternext */ cache_methods, /* tp_methods */ cache_memberlist, /* tp_members */ cache_getset, /* tp_getset */ 0, /* tp_base */ 0, /* tp_dict */ cache_descr_get, /* tp_descr_get */ 0, /* tp_descr_set */ OFF(func_dict), /* tp_dictoffset */ 0, /* tp_init */ 0, /* tp_alloc */ 0, /* tp_new */ 0, /* tp_free */ }; /* lruobject - * the callable object returned by lrucache(all, my, cache, args) * [lrucache is known as clru_cache in python land] * records arguments to clru_cache and passes them along to the * cacheobject created when lruobject is called with a function * as an argument */ typedef struct { PyObject_HEAD Py_ssize_t maxsize; PyObject *state; int typed; enum unhashable err; } lruobject; static void lru_dealloc(lruobject *lru) { Py_CLEAR(lru->state); Py_TYPE(lru)->tp_free(lru); } static PyObject * get_func_attr(PyObject *fo, const char *name) { if( !PyObject_HasAttrString(fo,name)) Py_RETURN_NONE; else{ PyObject *attr = PyObject_GetAttrString(fo, name); if (attr == NULL) return NULL; return attr; } } /* takes a function as an argument and returns a cacheobject */ static PyObject * lru_call(lruobject *lru, PyObject *args, PyObject *kw) { PyObject *fo, *mod, *nt; cacheobject *co; if(! PyArg_ParseTuple(args, "O", &fo)) return NULL; if(! PyCallable_Check(fo)){ PyErr_SetString(PyExc_TypeError, "Argument must be callable."); return NULL; } co = PyObject_New(cacheobject, &cache_type); if (co == NULL) return NULL; #ifdef WITH_THREAD if ((co->lock = PyThread_allocate_lock()) == NULL){ Py_DECREF(co); return NULL; } // We need to initialize the rlock count and owner here co->rlock_count = 0; co->rlock_owner = 0; #endif if ((co->cache_dict = PyDict_New()) == NULL){ Py_DECREF(co); return NULL; } // initialize circular doubly linked list co->root = PyObject_New(clist, &clist_type); if(co->root == NULL){ Py_DECREF(co); return NULL; } // get namedtuple for cache_info() mod = PyImport_ImportModule("collections"); if (mod == NULL){ Py_DECREF(co); return NULL; } nt = PyObject_GetAttrString(mod, "namedtuple"); if (nt == NULL){ Py_DECREF(co); return NULL; } co->cinfo = PyObject_CallFunction(nt,"ss","CacheInfo", "hits misses maxsize currsize"); if (co->cinfo == NULL){ Py_DECREF(co); return NULL; } co->func_dict = get_func_attr(fo, "__dict__"); co->fn = fo; // __wrapped__ Py_INCREF(co->fn); co->func_module = get_func_attr(fo, "__module__"); co->func_annotations = get_func_attr(fo, "__annotations__"); co->func_name = get_func_attr(fo, "__name__"); co->func_qualname = get_func_attr(fo, "__qualname__"); co->ex_state = lru->state; Py_INCREF(co->ex_state); co->maxsize = lru->maxsize; co->hits = 0; co->misses = 0; co->typed = lru->typed; co->err = lru->err; // start with self-referencing root node co->root->prev = co->root; co->root->next = co->root; co->root->key = Py_None; co->root->result = Py_None; Py_INCREF(co->root->key); Py_INCREF(co->root->result); return (PyObject *)co; } static PyTypeObject lru_type = { PyVarObject_HEAD_INIT(NULL, 0) "fastcache.lru", /* tp_name */ sizeof(lruobject), /* tp_basicsize */ 0, /* tp_itemsize */ /* methods */ (destructor)lru_dealloc, /* tp_dealloc */ 0, /* tp_print */ 0, /* tp_getattr */ 0, /* tp_setattr */ 0, /* tp_reserved */ 0, /* tp_repr */ 0, /* tp_as_number */ 0, /* tp_as_sequence */ 0, /* tp_as_mapping */ 0, /* tp_hash */ (ternaryfunc)lru_call, /* tp_call */ 0, /* tp_str */ 0, /* tp_getattro */ 0, /* tp_setattro */ 0, /* tp_as_buffer */ Py_TPFLAGS_DEFAULT , /* tp_flags */ }; /* helper function for processing 'unhashable' */ enum unhashable process_uh(PyObject *arg, PyObject *(*f)(const char *)) { PyObject *uh[3] = {f("error"), f("warning"), f("ignore")}; int i, j; if (arg != NULL){ enum unhashable vals[3] = {FC_ERROR, FC_WARNING, FC_IGNORE}; for(i=0; i<3; i++){ int k = PyObject_RichCompareBool(arg, uh[i], Py_EQ); if (k < 0){ for(j=0; j<3; j++) Py_DECREF(uh[j]); return FC_FAIL; } if (k){ /* DECREF objects and return value */ for(j=0; j<3; j++) Py_DECREF(uh[j]); return vals[i]; } } } for(j=0; j<3; j++) Py_DECREF(uh[j]); PyErr_SetString(PyExc_TypeError, "Argument must be 'error', 'warning', or 'ignore'"); return FC_FAIL; } /* LRU cache decorator */ PyDoc_STRVAR(lrucache__doc__, "clru_cache(maxsize=128, typed=False, state=None, unhashable='error')\n\n" "Least-recently-used cache decorator.\n\n" "If *maxsize* is set to None, the LRU features are disabled and the\n" "cache can grow without bound.\n\n" "If *typed* is True, arguments of different types will be cached\n" "separately. For example, f(3.0) and f(3) will be treated as distinct\n" "calls with distinct results.\n\n" "If *state* is a list or dict, the items will be incorporated into the\n" "argument hash.\n\n" "The result of calling the cached function with unhashable (mutable)\n" "arguments depends on the value of *unhashable*:\n\n" " If *unhashable* is 'error', a TypeError will be raised.\n\n" " If *unhashable* is 'warning', a UserWarning will be raised, and\n" " the wrapped function will be called with the supplied arguments.\n" " A miss will be recorded in the cache statistics.\n\n" " If *unhashable* is 'ignore', the wrapped function will be called\n" " with the supplied arguments. A miss will will be recorded in\n" " the cache statistics.\n\n" "View the cache statistics named tuple (hits, misses, maxsize, currsize)\n" "with f.cache_info(). Clear the cache and statistics with\n" "f.cache_clear(). Access the underlying function with f.__wrapped__.\n\n" "See: http://en.wikipedia.org/wiki/Cache_algorithms#Least_Recently_Used"); static PyObject * lrucache(PyObject *self, PyObject *args, PyObject *kwargs) { PyObject *state = Py_None; int typed = 0; PyObject *omaxsize = Py_False; PyObject *oerr = Py_None; Py_ssize_t maxsize = 128; static char *kwlist[] = {"maxsize", "typed", "state", "unhashable", NULL}; lruobject *lru; enum unhashable err; #if defined(_PY2) || defined (_PY32) PyObject *otyped = Py_False; if(! PyArg_ParseTupleAndKeywords(args, kwargs, "|OOOO:lrucache", kwlist, &omaxsize, &otyped, &state, &oerr)) return NULL; typed = PyObject_IsTrue(otyped); if (typed < -1) return NULL; #else if(! PyArg_ParseTupleAndKeywords(args, kwargs, "|OpOO:lrucache", kwlist, &omaxsize, &typed, &state, &oerr)) return NULL; #endif if (omaxsize != Py_False){ if (omaxsize == Py_None) maxsize = -1; #ifdef _PY2 else if (PyInt_Check(omaxsize)){ maxsize = PyInt_AsSsize_t(omaxsize); if (maxsize < 0) maxsize = -1; } #endif else { if( ! PyLong_Check(omaxsize)){ PyErr_SetString(PyExc_TypeError, "Argument must be an int."); return NULL; } maxsize = PyLong_AsSsize_t(omaxsize); if (maxsize < 0) maxsize = -1; } } // ensure state is a list or dict if (state != Py_None && !(PyList_Check(state) || PyDict_CheckExact(state))){ PyErr_SetString(PyExc_TypeError, "Argument must be a list or dict."); return NULL; } // check unhashable if (oerr == Py_None) err = FC_ERROR; else{ #ifdef _PY2 if(PyString_Check(oerr)) err = process_uh(oerr, PyString_FromString); else #endif if(PyUnicode_Check(oerr)) err = process_uh(oerr, PyUnicode_FromString); else err = process_uh(NULL, NULL); // set error properly } if (err == FC_FAIL) return NULL; lru = PyObject_New(lruobject, &lru_type); if (lru == NULL) return NULL; lru->maxsize = maxsize; lru->state = state; lru->typed = typed; lru->err = err; Py_INCREF(lru->state); return (PyObject *) lru; } static PyMethodDef lrucachemethods[] = { {"clru_cache", (PyCFunction) lrucache, METH_VARARGS | METH_KEYWORDS, lrucache__doc__}, {NULL, NULL} /* sentinel */ }; #ifndef _PY2 static PyModuleDef lrucachemodule = { PyModuleDef_HEAD_INIT, "_lrucache", "Least Recently Used cache", -1, lrucachemethods, NULL, NULL, NULL, NULL }; #endif #ifndef PyMODINIT_FUNC /* declarations for DLL import/export */ #define PyMODINIT_FUNC void #endif PyMODINIT_FUNC #ifdef _PY2 init_lrucache(void) { #define _PYINIT_ERROR_RET return #else PyInit__lrucache(void) { PyObject *m; #define _PYINIT_ERROR_RET return NULL #endif lru_type.tp_new = PyType_GenericNew; if (PyType_Ready(&lru_type) < 0) _PYINIT_ERROR_RET; cache_type.tp_new = PyType_GenericNew; if (PyType_Ready(&cache_type) < 0) _PYINIT_ERROR_RET; HashedArgs_type.tp_new = PyType_GenericNew; if (PyType_Ready(&HashedArgs_type) < 0) _PYINIT_ERROR_RET; clist_type.tp_new = PyType_GenericNew; if (PyType_Ready(&clist_type) < 0) _PYINIT_ERROR_RET; #ifdef _PY2 Py_InitModule3("_lrucache", lrucachemethods, "Least recently used cache."); #else m = PyModule_Create(&lrucachemodule); if (m == NULL) return NULL; #endif Py_INCREF(&lru_type); Py_INCREF(&cache_type); Py_INCREF(&HashedArgs_type); Py_INCREF(&clist_type); #ifndef _PY2 return m; #endif } #ifdef __cplusplus } #endif