gocept.cache 6.0
Cache descriptors for Python and Zope
License: MIT
Requires Python: >=3.8
Keywords: cache transaction bound zope

.. contents::

============
gocept.cache
============

Method cache
============

Memoize with timeout
--------------------

Memoize with timeout caches methods with a certain timeout:

>>> import math
>>> import gocept.cache.method
>>>
>>> class Point(object):
...
...     def __init__(self, x, y):
...         self.x, self.y = x, y
...
...     @gocept.cache.method.Memoize(0.1)
...     def distance(self, x, y):
...         print('computing distance')
...         return math.sqrt((self.x - x)**2 + (self.y - y)**2)
...
...     @gocept.cache.method.Memoize(0.1, ignore_self=True)
...     def add_one(self, i):
...         if not isinstance(i, int):
...             print("I want an int")
...         else:
...             print('adding one')
...             return i + 1
...
>>> point = Point(1.0, 2.0)

When we first ask for the distance it is computed:

>>> point.distance(2, 2)
computing distance
1.0

The second time the distance is not computed but returned from the cache:

>>> point.distance(2, 2)
1.0

Now, let's wait 0.1 secondes, the value we set as cache timeout. After that the
distance is computed again:

>>> import time
>>> time.sleep(0.5)
>>> point.distance(2, 2)
computing distance
1.0


When we create a new instance, the new instance gets its own cache:

>>> p2 = Point(1.0, 2.0)
>>> p2.distance(2, 2)
computing distance
1.0

It's also possible to explicitly ignore self. We did this for the `add_one`
method:

>>> point.add_one(3)
adding one
4

The second time it's not computed as you would expect:
>>> point.add_one(3)
4

If we ask `p2` now the result is not computed as well:

>>> p2.add_one(3)
4

If we put a non hashable argument into a memoized function it will not be
cached:

>>> point.add_one({'a': 1})
I want an int
>>> point.add_one({'a': 1})
I want an int


The decorated method can be inspected and yields the same results as the
original:

>>> from inspect import signature
>>> Point.distance.__name__
'distance'
>>> tuple(signature(Point.distance).parameters.keys())
('self', 'x', 'y')


Explicitly exclude caching of a certain value
---------------------------------------------

Especially when talking to external systems, you want to handle
errors (i.e. by returning an emtpy result). But normally you
do not want to cache this special return value.

This is our database.

>>> class DB(object):
...
...     call_count = 0
...
...     def get_country(self, zip):
...         self.call_count += 1
...         if self.call_count % 2 == 0:
...             raise ValueError('Some strange response')
...         return 'Somecountry'
...

It will throw an exception with every 2nd call:

>>> db = DB()
>>> db.get_country(12345)
'Somecountry'

>>> db.get_country(12345)
Traceback (most recent call last):
...
ValueError: Some strange response

>>> db.get_country(12345)
'Somecountry'

>>> db.get_country(12345)
Traceback (most recent call last):
...
ValueError: Some strange response


Now we use do_not_cache_and_return to specify that we do
not want to cache if there was en error.

>>> import gocept.cache.method
>>>
>>> class Country(object):
...
...     db = DB()
...
...     @gocept.cache.method.Memoize(1000)
...     def by_zip(self, zip):
...         try:
...             return self.db.get_country(zip)
...         except ValueError:
...             return gocept.cache.method.do_not_cache_and_return(
...                     'DB is down.')
...

>>> country = Country()

First call will get cached, so we get the correct country with every call:

>>> country.by_zip(12345)
'Somecountry'

>>> country.by_zip(12345)
'Somecountry'

>>> country.by_zip(12345)
'Somecountry'

By using a new zip, the get_country method is called the second time, and
there will be an exception, which is not cached:

>>> country.by_zip(54321)
'DB is down.'

Calling it again will call get_country, because special return value is not
cached:

>>> country.by_zip(54321)
'Somecountry'

Now we always get the cached value:

>>> country.by_zip(54321)
'Somecountry'


Store memoizations on an attribute
----------------------------------

If you want more control over the cache used by gocept.cache.method.Memoize
(e. g. you want to associate it with a gocept.cache.property.CacheDataManager
to invalidate it on transaction boundaries), you can use the @memoize_on_attribute
decorator to retrieve the cache-dictionary from the instance:

>>> class Bar(object):
...     cache = {}
...
...     @gocept.cache.method.memoize_on_attribute('cache', 10)
...     def echo(self, x):
...         print('miss')
...         return x

>>> bar = Bar()
>>> bar.echo(5)
miss
5
>>> bar.echo(5)
5
>>> bar.cache.clear()
>>> bar.echo(5)
miss
5

This decorator should be used on methods, not on plain functions, since it must
be able to retrieve the cache-dictionary from the first argument of the function
(which is 'self' for methods):

>>> @gocept.cache.method.memoize_on_attribute('cache', 10)
... def bar():
...     print('foo')
>>> bar()
Traceback (most recent call last):
TypeError: gocept.cache.method.memoize_on_attribute could not retrieve cache attribute 'cache' for function <function bar at 0x...>

>>> @gocept.cache.method.memoize_on_attribute('cache', 10)
... def baz(x):
...     print('foo')
>>> baz(5)
Traceback (most recent call last):
TypeError: gocept.cache.method.memoize_on_attribute could not retrieve cache attribute 'cache' for function <function baz at 0x...>


Cached Properties
=================

Transaction Bound Cache
-----------------------

The transaction bound cache is invalidated on transaction boundaries.

Create a class and set some data:

>>> import gocept.cache.property
>>> class Foo(object):
...
...     cache = gocept.cache.property.TransactionBoundCache('_v_cache', dict)
...

(NOTE: You probably want to use a "volatile" attribute name for the cache
storage, otherwise a read-only access of the cache triggers a write.)

>>> foo = Foo()
>>> foo.cache
{}
>>> foo.cache['A'] = 1
>>> foo.cache
{'A': 1}

If we commit the transaction the cache is empty again:

>>> import transaction
>>> transaction.commit()
>>> foo.cache
{}


The same happens on abort:

>>> foo.cache['A'] = 1
>>> foo.cache
{'A': 1}
>>> transaction.abort()
>>> foo.cache
{}


=======
Changes
=======

6.0 (2025-03-10)
================

- Add support for Python 3.11 and 3.12.

- Drop support for Python 3.7.


5.0 (2021-08-31)
================

Backwards incompatible changes
------------------------------

- Change license from ZPL to MIT.

- Drop support for Python 2.7 and 3.6.

Features
--------

- Add support for Python 3.10.

Other changes
-------------

- Use Github actions as CI.


4.0 (2020-02-17)
================

- Add support for Python 3.7, 3.8 and 3.9a3.

- Drop support for Python 3.4 and 3.5.

- Migrate to github.


3.1 (2018-11-20)
================

- Add support for Python 3.7.

- Remove DeprecationWarnings concerning inspect.


3.0 (2017-11-20)
================

- Remove use of ``__file__`` in setup.py, to accommodate recent setuptools.

- Add support for Python 3.6.

- Drop support for Python 3.3.


2.1 (2016-11-17)
================

- Bugfix: ``.property.CacheDataManager`` no longer invalidates the cache in
  ``tpc_vote()`` and ``commit()`` but in ``tpc_finish()``.

- Raise `TransactionJoinError` if joining the transaction failed in
  ``.property.TransactionBoundCache``.


2.0 (2016-03-18)
================

- Drop support of Python 2.6.

- Declare support of PyPy and PyPy3.


1.0 (2015-09-25)
================

- Now testing against currently newest versions of dependencies.

- Drop support of Python 3.2.

- Declare Support of Python 3.4 and 3.5.


0.6.1 (2013-09-13)
==================

- Finish Python 3 compatibility


0.6 (2013-09-13)
================

- Changes not recored, sorry.


0.6b2 (2013-09-05)
==================

- Changes not recored, sorry.


0.6b1 (2013-09-05)
==================

- Python3 compatibility


0.5.2 (2012-06-22)
==================

- Added ``gocept.cache.method.do_not_cache_and_return(value)`` in memoized
  methods/functions which will return the given value, without caching it.

0.5.1 (2012-03-10)
==================

- Prevent race condition which caused values of ``gocept.cache.method.Memoize``
  not to be stored when collect was called during the Memoize call
  (in multi threaded environments).

- Pin test versions to ZTK 1.1

0.5 (2011-03-15)
================

- Replace dependency on ZODB with a dependency on transaction.

0.4 (2009-06-18)
================

- Registered clearing the cache with zope.testing.cleanup.

0.3 (2008-12-19)
================

- Added @memoize_on_attribute to retrieve the memoization cache from the
  instance instead of using gocept.cache.method's built-in cache.

0.2.2 (2007-12-17)
==================

- Fixed the bug in `TransactionBoundCache` where the cache was not invalidated
  on transaction abort.

0.2.1 (2007-10-17)
==================

- Fixed a bug in `TransactionBoundCache` which yielded an error in the log:
  `TypeError: <lambda>() takes exactly 1 argument (2 given)`