Full Code of pbrady/fastcache for AI

master 6b7bbed5076f cached
19 files
78.6 KB
21.5k tokens
99 symbols
1 requests
Download .txt
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)
           >>> <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
Download .txt
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
Download .txt
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[![Gitter](https://badges.gitter.im/Join Chat.svg)](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.

Copied to clipboard!