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
=========
[](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)
>>> <class 'fastcache.clru_cache'>
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)
>>> <class 'function'>
"""
__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)
>>> <class 'fastcache.clru_cache'>
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)
>>> <class 'function'>
(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 <Python.h>
#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 <unhashable> 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 <maxsize> 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 <state> 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
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
SYMBOL INDEX (99 symbols across 8 files)
FILE: fastcache/__init__.py
function lru_cache (line 30) | def lru_cache(maxsize=128, typed=False, state=None, unhashable='error'):
function test (line 77) | def test(*args):
FILE: fastcache/benchmark.py
function _untyped (line 19) | def _untyped(*args, **kwargs):
function _typed (line 22) | def _typed(*args, **kwargs):
function _arg_gen (line 31) | def _arg_gen(min=1, max=100, repeat=3):
function _print_speedup (line 37) | def _print_speedup(results):
function _print_single_speedup (line 50) | def _print_single_speedup(res=None, init=False):
function run (line 58) | def run():
FILE: fastcache/tests/test_clrucache.py
function count (line 10) | def count(start=0, step=1):
function arg_gen (line 15) | def arg_gen(min=1, max=100, repeat=3):
function cache (line 23) | def cache(request):
function test_function_attributes (line 28) | def test_function_attributes(cache):
function test_function_cache (line 41) | def test_function_cache(cache):
function test_memory_leaks (line 82) | def test_memory_leaks(cache):
function test_warn_unhashable_args (line 93) | def test_warn_unhashable_args(cache, recwarn):
function test_ignore_unhashable_args (line 110) | def test_ignore_unhashable_args(cache):
function test_default_unhashable_args (line 119) | def test_default_unhashable_args(cache):
function test_state_type (line 133) | def test_state_type(cache):
function test_typed_False (line 141) | def test_typed_False(cache):
function test_typed_True (line 157) | def test_typed_True(cache):
function test_dynamic_attribute (line 170) | def test_dynamic_attribute(cache):
FILE: fastcache/tests/test_functools.py
class TestLRU (line 19) | class TestLRU(unittest.TestCase):
method test_lru (line 21) | def test_lru(self):
method test_lru_with_maxsize_none (line 111) | def test_lru_with_maxsize_none(self):
method test_lru_with_exceptions (line 125) | def test_lru_with_exceptions(self):
method test_lru_with_types (line 150) | def test_lru_with_types(self):
method test_lru_with_keyword_args (line 166) | def test_lru_with_keyword_args(self):
method test_lru_with_keyword_args_maxsize_none (line 182) | def test_lru_with_keyword_args_maxsize_none(self):
method test_need_for_rlock (line 196) | def test_need_for_rlock(self):
FILE: fastcache/tests/test_thread.py
class PythonInt (line 17) | class PythonInt:
method __init__ (line 20) | def __init__(self, val):
method __hash__ (line 23) | def __hash__(self):
method __eq__ (line 26) | def __eq__(self, other):
function setinterval (line 40) | def setinterval(i):
function run_threads (line 44) | def run_threads(threads):
function fib (line 55) | def fib(n):
function run_fib_with_clear (line 63) | def run_fib_with_clear(r):
function run_fib_with_stats (line 72) | def run_fib_with_stats(r):
class Test_Threading (line 80) | class Test_Threading(unittest.TestCase):
method setUp (line 83) | def setUp(self):
method test_thread_random_cache_clears (line 88) | def test_thread_random_cache_clears(self):
method test_thread_cache_info (line 97) | def test_thread_cache_info(self):
FILE: scripts/threadsafety.py
class PythonInt (line 19) | class PythonInt:
method __init__ (line 22) | def __init__(self, val):
method __hash__ (line 25) | def __hash__(self):
method __eq__ (line 28) | def __eq__(self, other):
function fib (line 43) | def fib(n):
function fib2 (line 49) | def fib2(n):
function run_fib_with_clear (line 59) | def run_fib_with_clear(r):
function run_fib_with_stats (line 69) | def run_fib_with_stats(r):
function setinterval (line 81) | def setinterval(i):
function run_threads (line 85) | def run_threads(threads):
function run_test (line 91) | def run_test(n, r, i):
function run_test2 (line 97) | def run_test2(n, r, i):
function main (line 118) | def main():
FILE: setup.py
class BuildWithDefine (line 139) | class BuildWithDefine(_build):
method initialize_options (line 148) | def initialize_options(self):
method finalize_options (line 152) | def finalize_options(self):
method run (line 165) | def run(self):
class InstallWithDefine (line 168) | class InstallWithDefine(_install):
method initialize_options (line 177) | def initialize_options(self):
method finalize_options (line 181) | def finalize_options(self):
method run (line 194) | def run(self):
class BuildExt (line 197) | class BuildExt(_build_ext):
method initialize_options (line 199) | def initialize_options(self):
method finalize_options (line 202) | def finalize_options(self):
method run (line 209) | def run(self):
FILE: src/_lrucache.c
type Py_hash_t (line 11) | typedef long Py_hash_t;
type PyLockStatus (line 28) | typedef int PyLockStatus;
function rlock_acquire (line 34) | static int
function rlock_release (line 86) | static int
type HashedArgs (line 124) | typedef struct {
function HashedArgs_dealloc (line 131) | static void
function Py_hash_t (line 141) | static Py_hash_t
function PyObject (line 149) | static PyObject *
type clist (line 193) | typedef struct clist{
function clist_dealloc (line 202) | static void
function insert_first (line 250) | static int
function PyObject (line 272) | static PyObject *
type unhashable (line 296) | enum unhashable {FC_ERROR, FC_WARNING, FC_IGNORE, FC_FAIL}
type cacheobject (line 299) | typedef struct {
function PyObject (line 333) | static PyObject *
function restricted (line 345) | static int
function PyObject (line 358) | static PyObject *
function func_set_dict (line 372) | static int
function PyObject (line 416) | static PyObject *
function cache_dealloc (line 430) | static void
function PyObject (line 454) | static PyObject *
function PyObject (line 488) | static PyObject *
function PyObject (line 631) | static PyObject *
function PyObject (line 818) | static PyObject *
function PyObject (line 838) | static PyObject *
type lruobject (line 916) | typedef struct {
function lru_dealloc (line 925) | static void lru_dealloc(lruobject *lru)
function PyObject (line 932) | static PyObject *
function PyObject (line 947) | static PyObject *
function process_uh (line 1058) | enum unhashable
function PyObject (line 1115) | static PyObject *
function PyInit__lrucache (line 1229) | PyInit__lrucache(void)
Condensed preview — 19 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (85K chars).
[
{
"path": ".gitignore",
"chars": 539,
"preview": "# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n\n# C extensions\n*.so\n\n# Distribution / packaging\n.Python\n"
},
{
"path": ".travis.yml",
"chars": 463,
"preview": "language: python\nmatrix:\n include:\n - arch: arm64\n python: 2.7\n - arch: amd64\n python: 2.7\n - arch: "
},
{
"path": "CHANGELOG",
"chars": 1123,
"preview": "*1.0.2*\n- use pytest for testing\n- Bug fix for windows compatibility\n\n*1.0.1*\n- better error checking so fastcache now p"
},
{
"path": "LICENSE",
"chars": 1077,
"preview": "The MIT License (MIT)\n\nCopyright (c) 2014 Peter Brady\n\nPermission is hereby granted, free of charge, to any person obtai"
},
{
"path": "MANIFEST.in",
"chars": 16,
"preview": "include LICENSE\n"
},
{
"path": "README.md",
"chars": 5624,
"preview": "fastcache\n=========\n[](https://gitter.im/pbrady/fastcache?utm_source=ba"
},
{
"path": "bin/test_travis.sh",
"chars": 199,
"preview": "#! /usr/bin/env bash\n\n# Exit on error\nset -e\n# Echo each command\nset -x\n\nmkdir -p empty\ncd empty\ncat << EOF | python\nimp"
},
{
"path": "build.sh",
"chars": 37,
"preview": "#!/bin/bash\n$PYTHON setup.py install\n"
},
{
"path": "fastcache/__init__.py",
"chars": 2670,
"preview": "\"\"\" C implementation of LRU caching.\n\nProvides 2 LRU caching function decorators:\n\nclru_cache - built-in (faster)\n "
},
{
"path": "fastcache/benchmark.py",
"chars": 3690,
"preview": "\"\"\" Benchmark against functools.lru_cache.\n\n Benchmark script from http://bugs.python.org/file28400/lru_cache_bench.p"
},
{
"path": "fastcache/tests/__init__.py",
"chars": 0,
"preview": ""
},
{
"path": "fastcache/tests/test_clrucache.py",
"chars": 4436,
"preview": "import pytest\nimport fastcache\nimport itertools\nimport warnings\n\ntry:\n itertools.count(start=0, step=-1)\n count = "
},
{
"path": "fastcache/tests/test_functools.py",
"chars": 8022,
"preview": "# Copied from python src Python-3.4.0/Lib/test/test_functools.py\n\nimport abc\nimport collections\nfrom itertools import pe"
},
{
"path": "fastcache/tests/test_thread.py",
"chars": 3465,
"preview": "\"\"\"The Python interpreter may switch between threads inbetween bytecode\nexecution. Bytecode execution in fastcache may "
},
{
"path": "meta.yaml",
"chars": 508,
"preview": "package:\n name: fastcache\n version: 0.4.0\n\nsource:\n git_url : https://github.com/pbrady/fastcache.git\n\nrequirements:"
},
{
"path": "scripts/threadsafety.py",
"chars": 4640,
"preview": "from __future__ import division\n\n\"\"\"The Python interpreter may switch between threads inbetween bytecode\nexecution. Byt"
},
{
"path": "setup.cfg",
"chars": 39,
"preview": "[metadata]\ndescription-file = README.md"
},
{
"path": "setup.py",
"chars": 8254,
"preview": "import sys\nfrom os import getenv\n\n# use setuptools by default as per the official advice at:\n# packaging.python.org/en/l"
},
{
"path": "src/_lrucache.c",
"chars": 35648,
"preview": "#include <Python.h>\n#include \"structmember.h\"\n#include \"pythread.h\"\n\n#ifdef __cplusplus\nextern \"C\" {\n#endif\n\n#if PY_MAJO"
}
]
About this extraction
This page contains the full source code of the pbrady/fastcache GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 19 files (78.6 KB), approximately 21.5k tokens, and a symbol index with 99 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.